Search code examples
.netangularsignalr-hubasp.net-core-signalr

SignalR direct message to single user in angular


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?


Solution

  • 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!)