SignalR direct message to single user in angular

188 Views Asked by At

I am trying to implement direct message to specific user using SignalR(.NET 6) and Angular ~13 and so far I got so many information regarding implementation but none of them work.

So in client side (Angular) I have method to send message

export interface IChatMessage {
    id?: number;
    message: string;
    from?: string;
    to?: string;
    created?: Date | null;
} 

where sending to hub I send only message and to and to is a receiver of that message. My chat.service.ts is

export class ChatService {
    private hubConnection: HubConnection;
    private receivedMessageSubject = new Subject<IChatMessage>();
    public connectionId: string;
    receivedMessage$: Observable<IChatMessage> = this.receivedMessageSubject.asObservable();

    constructor(private apiService: ApiService) {
        this.hubConnection = new HubConnectionBuilder()
            .withUrl(environment.apiUrl + API_ROUTES.CHAT, {accessTokenFactory: () => localStorage.getItem('token')})
            .build();
        this.hubConnection.start().then(() => console.log('connection started')).then(() => this.getConnectionId()).catch((err) => console.error(err));

        this.hubConnection.on('ReceiveMessage', (message) => {
            console.log('Received message from server:', message);
            this.receivedMessageSubject.next(message);
        });
    }

    private getConnectionId = () => {
        this.hubConnection.invoke('getconnectionid')
            .then((data) => {
                this.connectionId = data;
            });
    }

    getMessages(id: string) {
        return this.apiService.post(environment.apiUrl + API_ROUTES.CHAT_MESSAGES, {userId: id});
    }

    sendMessage(message: IChatMessage): void {
        this.hubConnection.invoke('SendMessage', message, this.connectionId)
            .then((response) => console.log('Server response:', response))
            .catch(error => console.error('Error sending message through SignalR:', error));
    }
}

On server side I have hub ChatHub

        public async Task SendMessage(ChatMessage message, string connectionId)
        {
            try
            {
                var headers = Context.GetHttpContext().Request.Headers;
                var user = Context.GetHttpContext().User.Identity;
                Console.WriteLine(JsonSerializer.Serialize(Context.ConnectionId));
                await Clients.User(connectionId).SendAsync("ReceiveMessage", message);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error in SendMessage: {ex.Message}");
                throw;
            }
        }

and I get the connectionId but it's different for each user and cannot map this to connect sender with receiver. In Program.cs I have

using IUserIdProvider = Microsoft.AspNetCore.SignalR.IUserIdProvider;
...
builder.Services.AddSingleton<IUserIdProvider, CustomUserIdProvider>();
builder.Services.AddSignalR();

var app = builder.Build();
...
app.UseAuthentication();
...
app.UseEndpoints(endpoints =>
{
    endpoints.MapHub<ChatHub>("/api/chat");
    endpoints.MapControllers();
});

and I also have CustomUserIdProvider : IUserIdProvider

    public class CustomUserIdProvider : IUserIdProvider
    {
        public string GetUserId(HubConnectionContext connection)
        {
            var uniqueId = connection.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
            return uniqueId;
        }

    }

but uniqueId in CustomUserIdProvider is empty. For all controllers I have var user = _userClaims.GetUserId(); which takes uniqueId from token from User entity which is string and is unique. And it works. If I do await Clients.All.SendAsync("ReceiveMessage", message); it send message to all users, 'connectionId` is available but only of the sender so receiver does not receive this message. And I have no clue how to implement this in .NET 6 + Angular 13 because like I said I have tried for the last 5 days basically everything that is available in the internet but still no luck. Does anyone have any idea how to implement this so it could be sent in 1-to-1 manner using receiver's uniqueId identyfier?

2

There are 2 best solutions below

0
On BEST ANSWER

Finally solved this.

In Program.cs in AddAuthentication in AddJwtBearer (because authentication is with JWT) I added

    config.Events = new JwtBearerEvents
    {
        OnMessageReceived = context =>
        {
            var accessToken = context.Request.Query["access_token"];
            var path = context.HttpContext.Request.Path;
            if (!string.IsNullOrEmpty(accessToken) && (path.StartsWithSegments("/api/chat")))
            {
                context.Token = accessToken;
            }
            return Task.CompletedTask;
        }
    };

next added builder.Services.AddSingleton<IUserIdProvider, CustomUserIdProvider>(); and after Useauthorization I added

var idProvider = new CustomUserIdProvider();
GlobalHost.DependencyResolver.Register(typeof(IUserIdProvider), () => idProvider);

I have also created CustomUserIdPrvider

    public class CustomUserIdProvider : IUserIdProvider
    {
        public string GetUserId(HubConnectionContext connection)
        {
            var uniqueId = connection.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
            return uniqueId;
        }

    }

and SendMessage in ChatHub`

        public async Task SendMessage(ChatMessage message)
        {
            var httpContext = Context.GetHttpContext();
            var user = httpContext.User.Claims.Where(user => user.Type == ClaimTypes.NameIdentifier).FirstOrDefault()?.Value;
            var token = httpContext.Request.Query["access_token"];
            message.From = user;
            message.Created = DateTime.Now.ToLocalTime();
            await Clients.Users(message.To, user).SendAsync("ReceiveMessage", message);
        }

so with Clients.Users(message.To, user) I am able to send message content to specific user with uniqueId (NOT! connectionId!)

0
On

When a user (browser or other devices) connect to the Hub endpoint, the generated connectionId is randomly generated by signalr itself, if the application stop/crash, the connectionIds will be disappeared, we can't use it any more.

A user has only one uniqueId, but he can open many web pages and use multiple mobile devices with colleagues, so we need to maintain the relationship between them ourselves. For more details, you can check my answer in this thread.

Here is the sample I am using for test.

namespace SignalRMiddleawre.Hubs
{
    /// <summary>
    /// </summary>
    [Authorize]
    public partial class MainHub : Hub
    {
        #region Connection
        /// <summary>
        /// Manage Connected Users
        /// </summary>
        private static ConcurrentDictionary<string?, List<string>>? ConnectedUsers = new ConcurrentDictionary<string?, List<string>>();
        /// <summary>
        /// OnConnect Event
        /// </summary>
        /// <param name="userid"></param>
        /// <returns></returns>
        ///
        public override async Task OnConnectedAsync()
        {
            // Get HttpContext In asp.net core signalr
            //IHttpContextFeature hcf = (IHttpContextFeature)this.Context.Features[typeof(IHttpContextFeature)];
            //HttpContext hc = hcf.HttpContext;
            //string uid = hc.Request.Path.Value.Split(new string[] { "=", "" }, StringSplitOptions.RemoveEmptyEntries)[1].ToString();

            string? userid = Context.User?.Identity?.Name;
            if (userid == null || userid.Equals(string.Empty))
            {
                Trace.TraceInformation("user not loged in, can't connect signalr service");
                return;
            }
            Trace.TraceInformation(userid + "connected");
            // save connection
            List<string>? existUserConnectionIds;
            ConnectedUsers.TryGetValue(userid, out existUserConnectionIds);
            if (existUserConnectionIds == null)
            {
                existUserConnectionIds = new List<string>();
            }
            existUserConnectionIds.Add(Context.ConnectionId);
            ConnectedUsers.TryAdd(userid, existUserConnectionIds);

            await Clients.All.SendAsync("ServerInfo", userid, userid + " connected, connectionId = " + Context.ConnectionId);
            await base.OnConnectedAsync();
        }

        /// <summary>
        /// OnDisconnected event
        /// </summary>
        /// <param name="userid"></param>
        /// <returns></returns>
        public override async Task OnDisconnectedAsync(Exception? exception)
        {
            string? userid = Context.User?.Identity?.Name;
            // save connection
            List<string>? existUserConnectionIds;
            ConnectedUsers.TryGetValue(userid, out existUserConnectionIds);

            existUserConnectionIds.Remove(Context.ConnectionId);

            if (existUserConnectionIds.Count == 0)
            {
                List<string> garbage;
                ConnectedUsers.TryRemove(userid, out garbage);
            }

            await base.OnDisconnectedAsync(exception);
        }
        #endregion

        #region Message
        /// <summary>
        /// Send msg to all user
        /// </summary>
        /// <param name="userid"></param>
        /// <param name="message"></param>
        /// <returns></returns>
        public async Task SendMessage(string msgType, string message)
        {
            await Clients.All.SendAsync("ReceiveMessage", msgType, message);
        }

        /// <summary>
        /// Send msg to user by userid
        /// </summary>
        /// <param name="connectionId"></param>
        /// <param name="message">message format : type-message </param>
        /// <returns></returns>
        public async Task SendToSingleUser(string userid, string message)
        {
            List<string>? existUserConnectionIds;
            // find all the connectionids by userid
            ConnectedUsers.TryGetValue(userid, out existUserConnectionIds);
            if (existUserConnectionIds == null)
            {
                existUserConnectionIds = new List<string>();
            }
            existUserConnectionIds.Add(Context.ConnectionId);
            ConnectedUsers.TryAdd(userid, existUserConnectionIds);
            await Clients.Clients(existUserConnectionIds).SendAsync("ReceiveMessage", message);
        }
        #endregion

    }
}