Search code examples
c#.netsignalrc#-10.0azure-functions-isolated

SignalR C#: Send message to a specific user or group


BackEnd: .NET 6, C#10, Azure Isolated Function project

NuGet: Microsoft.Azure.Functions.Worker.Extensions.SignalRService v1.7.0

How to send a message to a specific user or group?

I found that adding a value to the SignalRConnectionInfoInput.UserId is working but this class is sealed and I couldn't find a way to dynamicly add the UserId value. Here's the working Negotiate function that allow me to broadcast to all users:

[Function(nameof(Negotiate))]
public static HttpResponseData Negotiate([HttpTrigger(AuthorizationLevel.Anonymous)] HttpRequestData req,
    [SignalRConnectionInfoInput(HubName = "MyHubName")] string connectionInfo)
{
    var response = req.CreateResponse(HttpStatusCode.OK);
    response.Headers.Add("Content-Type", "application/json");
    response.WriteString(connectionInfo);
    return response;
}

I can send a message to all users with this BroadcastAll function:

    [Function(nameof(BroadcastToAll))]
    public static SignalRMessageAction BroadcastToAll([HttpTrigger(AuthorizationLevel.Anonymous,   
       "post", Route = nameof(BroadcastToAll))] HttpRequestData req)
    {
        using var bodyReader = new StreamReader(req.Body);
        var body = bodyReader.ReadToEnd();
        return new SignalRMessageAction("MyTargetName")
        {
            Arguments = new object[] { body },
        };
    }

But I didn't find a way to send it to specific user correctly other then using the UserId attribute hardcoded like in this Negociate function example which make it works:

[Function(nameof(Negotiate))]
public static HttpResponseData Negotiate([HttpTrigger(AuthorizationLevel.Anonymous)] HttpRequestData req,
    [SignalRConnectionInfoInput(HubName = "MyHubName", **UserId = "12345"**)] string connectionInfo)
{
    var response = req.CreateResponse(HttpStatusCode.OK);
    response.Headers.Add("Content-Type", "application/json");
    response.WriteString(connectionInfo);
    return response;
}

Then I can use the UserId property from the SignalRMessageAction class to specify the UserId = "12345" I want to send the message to and in the frontend, only this user will receive this message.

    [Function(nameof(SendToUser))]
    public static async Task<SignalRMessageAction> SendToUser(
        [HttpTrigger(AuthorizationLevel.Anonymous, "post", 
            Route = nameof(SendToUser))] HttpRequestData req)
    {
        using var bodyReader = new StreamReader(req.Body);
        var requestBody = await bodyReader.ReadToEndAsync();
        return new SignalRMessageAction("MyTargetName")
        {
            Arguments = new object[] { requestBody },
            UserId = "12345"
        };
    }

What I found is that when you set the UserId in the attribute, the accessToken returned by Azure SignalR Service, in the connectionInfo, contains a claim that identify the user: "asrs.s.uid": "12345"

Here's an example of my frontend code: FrontEnd: Angular, TypeScript, MSAL (@azure/msal-angular": "^2.3.0"), SignalR (@microsoft/signalr": "^7.0.5")

public ConnectSignalR(): void {
    this.hubConnection = new signalR.HubConnectionBuilder().withUrl(this.basePath).build();
    
    this.hubConnection
    .start()
    .then(() => console.log('Connection started'))
    .catch((err) => console.log('Error while starting connection: ' + err));
    
    this.hubConnection.on('MyTargetName', (mySignalRMessage: string) => {
        console.log(mySignalRMessage)
    });
}

public DisconnectSignalR(): void {
    if (this.hubConnection) {
      this.hubConnection.stop();
    }
}

Thank you for your help and I hope it can helps other in the future.

I tried to add the UserId = "{headers.x-ms-client-principal-id}" like what the documentation says at section Authenticated tokens but as I'm using MSAL, I don't have this header and I would like to set the value myself in the backend. Doc


Solution

  • Just found a solution Here, hope this helps others: First, you need to add this NuGet: Microsoft.Azure.SignalR.Management

        [Function("negotiate")]
        public async Task<HttpResponseData> Negotiate([HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestData req)
        {
            var negotiateResponse = await MessageHubContext.NegotiateAsync(new() { UserId = "12345" });
            var response = req.CreateResponse();
            await response.WriteAsJsonAsync(negotiateResponse, JsonObjectSerializer);
            return response;
        }