I have the following signalR service on Angular to communicate with my signalR server:
export class SignalService {
private hubConnection: signalR.HubConnection;
private messageSubject = new Subject<ConversationSignalMessage>();
public messageReceived$ = this.messageSubject.asObservable();
constructor(private authService: AuthService) {
this.hubConnection = new signalR.HubConnectionBuilder()
.withUrl(`https://localhost:5001/sampleHub?userId=${user.id}`, { withCredentials: false})
.build();
this.hubConnection.on('MessageReceived', (chatId: string, message: Message) => {
if (message.userSender.id == user.id)
return;
this.messageSubject.next(new ConversationSignalMessage(chatId, message));
});
}
startConnection(): Promise<any> {
return this.hubConnection.start();
}
joinChat(chatId: string, participant: Participant): void {
this.hubConnection.invoke('Join', chatId, participant)
.catch(err => console.error(`Error joining chat: ${err}`));
}
sendMessage(chatId: string, message: Message): void {
this.hubConnection.invoke('SendMessage', chatId, message)
.catch(err => console.error(`Error sending message: ${err}`));
}
createConversation(users: string[], chatId: string, message: Message): void {
this.hubConnection.invoke('CreateConversation', users, chatId, message)
.catch(err => console.error(`Error creating conversation and joining users: ${err}`));
}
getMessageStream(): Observable<ConversationSignalMessage> {
return this.messageSubject.asObservable();
}
stopConnection(): Promise<any> {
return this.hubConnection.stop();
}
}
And this is my signalR c# hub:
private static readonly Dictionary<string, string> _connectedUsers = new();
private readonly Dictionary<Guid, List<string>> _conversationParticipants = new();
private readonly Dictionary<Guid, List<Message>> _conversationMessages = new();
public async Task SendMessage(Guid chatId, Message message)
{
Console.WriteLine($"{conversationId}: {message.Text}");
await Clients.Groups(chatId.ToString()).SendAsync("MessageReceived", chatId, message).ConfigureAwait(false);
}
public async Task Join(Guid chatId, Participant participant)
{
Console.WriteLine($"{participant.UserName} joined {chatId}.");
await Groups.AddToGroupAsync(Context.ConnectionId, chatId.ToString()).ConfigureAwait(false);
}
public async Task LeaveConversation(Guid chatId) => await Groups.RemoveFromGroupAsync(Context.ConnectionId, chatId.ToString()).ConfigureAwait(false);
public override async Task OnConnectedAsync()
{
string userId = Context.GetHttpContext().Request.Query["userId"];
_connectedUsers[Context.ConnectionId] = userId;
await base.OnConnectedAsync().ConfigureAwait(false);
Console.WriteLine($"Client `{userId}` connected.");
}
public override async Task OnDisconnectedAsync(Exception exception)
{
if (_connectedUsers.TryGetValue(Context.ConnectionId, out string userId))
{
_ = _connectedUsers.Remove(Context.ConnectionId);
Console.WriteLine($"Client `{userId}` disconnected.");
}
await base.OnDisconnectedAsync(exception).ConfigureAwait(false);
}
public async Task CreateConversation(List<string> users, Guid chatId, Message message)
{
string creatorConnectionId = Context.ConnectionId;
_conversationParticipants[conversationId] = userIds;
if (!_conversationMessages.ContainsKey(conversationId))
{
_conversationMessages[conversationId] = new List<Message>();
}
_conversationMessages[conversationId].Add(message);
foreach (var userId in users)
{
if (_connectedUsers.ContainsValue(userId.ToString()))
{
await Groups.AddToGroupAsync(creatorConnectionId, chatId.ToString()).ConfigureAwait(false);
Console.WriteLine($"{userId} joined {chatId}.");
}
}
// Frontend not listening this call.
await Clients.Groups(chatId.ToString()).SendAsync("ConversationCreated", chatId).ConfigureAwait(false);
await Clients.Groups(chatId.ToString()).SendAsync("MessageReceived", chatId, message).ConfigureAwait(false);
}
At the moment I can start a new conversation between me and another user. I can send a message, but the other user doesn't receive it until he refreshes the full page (basically it's loading the chat from the database). After this refresh the Join method is invoked and now I can send a message and he will receive it (via SendMessage), this is working fine.
What I'm trying to do is once that I create a new conversation between me, and another user automatically create a new conversation channel (hub... maybe?) and create the connection (similar to what "Join" does) and finally, once that both users are connected a listening to the new conversation channel, I want to start sharing messages between them (similar to what "SendMessage" does).
At the moment I created the code from CreateConversation and I can see the Console.WriteLine($"{userId} joined {chatId}."); message on my console but after I send a message the other user never receives the message. What I'm doing wrong or what can I do to accomplish what I'm trying here.
Update
Try to implement the feature like below.
public async Task CreateConversation(List<string> users, Guid chatId, Message message)
{
// creator
string creatorConnectionId = Context.ConnectionId;
// get all the userIds from this chat channel by conversationId
_conversationParticipants[conversationId] = userIds;
// route all the message to the correct chat channel
if (!_conversationMessages.ContainsKey(conversationId))
{
_conversationMessages[conversationId] = new List<Message>();
}
// add the message to the correct chat channel
_conversationMessages[conversationId].Add(message);
// foreach the users and get all the connectedUsers by userid(s)
// like below
foreach (var userId in users)
{
List<string>() tempConnectionsListForSingleUser = new List<string>();
// get all the connections by userid, one user could have a lot of connectionid
tempConnectionsListForSingleUser = _connectedUsers.TryGetValue(userid, out existUserConnectionIds);
// foreach tempConnectionsListForSingleUser
// add all the connections into the chat channel
foreach (string item in tempConnectionsListForSingleUser)
{
await Groups.AddToGroupAsync(item, chatId.ToString()).ConfigureAwait(false);
}
//if (_connectedUsers.ContainsValue(userId.ToString()))
//{
//await Groups.AddToGroupAsync(creatorConnectionId, chatId.ToString()).ConfigureAwait(false);
//Console.WriteLine($"{userId} joined {chatId}.");
//}
}
await Clients.Groups(chatId.ToString()).SendAsync("ConversationCreated", chatId).ConfigureAwait(false);
await Clients.Groups(chatId.ToString()).SendAsync("MessageReceived", chatId, message).ConfigureAwait(false);
}
The root casue is not listening the ConversationCreated
event in front-end.
To put it simply,
await Clients.Groups(chatId.ToString()).SendAsync("ConversationCreated", chatId).ConfigureAwait(false);
This method on the backend will call hubConnection.on('ConversationCreated')
in frontend. The front and back ends need to be matched one by one.
Change below code
foreach (var userId in users)
{
if (_connectedUsers.ContainsValue(userId.ToString()))
{
await Groups.AddToGroupAsync(creatorConnectionId, chatId.ToString()).ConfigureAwait(false);
Console.WriteLine($"{userId} joined {chatId}.");
}
}
To
private static ConcurrentDictionary<string, List<string>> ConnectedUsers = new ConcurrentDictionary<string, List<string>>();
...
//use below code in your OnConnectedAsync and other method
List<string>? existUserConnectionIds;
ConnectedUsers.TryGetValue(userid, out existUserConnectionIds);
if (existUserConnectionIds == null)
{
existUserConnectionIds = new List<string>();
}
existUserConnectionIds.Add(Context!.ConnectionId);
ConnectedUsers.TryAdd(userid, existUserConnectionIds);
It has logic issue here. Every time the method is been excuted, it also add one ConnectionId into group.