Search code examples
c#azure.net-coreazure-functionsazure-signalr

Serverless Azure SignalR + sending message via. HubConnection


I have an Azure SignalR serverless instance; I've created an Azure Function negotiation endpoint which is working as expected. I followed the "negotiation server" guidance provided here to set this up using an isolated Azure Function.

https://github.com/aspnet/AzureSignalR-samples/tree/main/samples/Management

I then used guidance under "SignalR client" from same link to create a client application - mainly the subscription to the event, i.e., hubConnection.On("HubMethodName", action => { ... }). Using the HubConnection object here is ideal because the HubConnectionBuilder is executed against the proxy URI - i.e., https://myfunctionapp.com/hubName, which triggers the negotiation function and returns the connection details for the HubConnection to actually use. At this point I'll also call out that a HubConnection has some methods relating to the sending of messages - i.e., SendAsync(...) and InvokeAsync(...).

When it comes to message publishing, the documentation seems to ignore these aforementioned methods and refers to initializing a ServiceManager to create a HubContext, and then utilize the proxy methods provided on the hub context to perform message sending (i.e., hubContext.Clients.All.SendAsync(...)). My issue with this is that this ServiceManager / HubContext feel very server-side - they are the objects I've used on my Azure Function to achieve negotiation, provide much more management functionality than just send / subscribe, and do not bode well with the concept of my negotiation endpoint (the ServiceBuilder accepts the Azure SignalR connection string and does not leverage the negotiation endpoint).

With that said, I'm extremely inclined to exclude the ServiceManager / HubContext from my client side code and very eager to figure out how to get this to work using strictly HubConnection instances. However, utilizing the SendAsync / InvokeAsync methods do not result in success. Firstly, I have confirmed that my subscription is working successfully by creating an additional Azure Function endpoint that sends a message via. _context.Clients.All.SendAsync("method", "some text") which successfully triggers my client side subscription. However, when I use my hubConnection.SendAsync("method", "some text") approach from the client side, my subscription is not triggered.

Investigating, I discovered that no "message" is actually being sent. In Azure SignalR logs, I can see the connection being initialized and the connection count increasing; however message count statistics do not change when I call SendAsync(...) on the HubConnection. By changing the transport mode to long polling and using Fiddler, I have confirmed that an HTTP post is occurring when I call the hubConnection.SendAsync(...) method - it does indeed perform a POST against my Azure SignalR service.

POST https://xxx.signalr.net/client/?hub=testhub&id=xxx HTTP/1.1
Host: xxx.service.signalr.net
User-Agent: Microsoft SignalR/6.0 (6.0.9+3fe12b935c03138f76364dc877a7e069e254b5b2; Windows NT; .NET; .NET 6.0.9)
X-Requested-With: XMLHttpRequest
Authorization: Bearer xxx
Content-Length: 88

{"type":1,"target":"method","arguments":["test message"]}

To make a long story short, this POST is not what should occur when a message is being broadcast - at least not what my intention is. Cutting to the chase and what happens in my "successful test case" - when the server calls hubContext.Clients.All.SendAsync(...), it uses completely different logic than the HubConnection's SendAsync(...) method. In this case, we see the RestApiProvider influencing the endpoint being called (https://github.com/Azure/azure-signalr/blob/180cf1923e828c19967e2ad44bd5e3693bff4218/src/Microsoft.Azure.SignalR.Management/RestApiProvider.cs#L45).

public Task<RestApiEndpoint> GetBroadcastEndpointAsync(string appName, string hubName, TimeSpan? lifetime = null, IReadOnlyList<string> excluded = null)
{
    var queries = excluded == null ? null : new Dictionary<string, StringValues>() { { "excluded", excluded.ToArray() } };
    return GenerateRestApiEndpointAsync(appName, hubName, "/:send", lifetime, queries);
}

In the working case (using HubContext.Clients.All.SendAsync), we end up performing a POST against the Azure SignalR service with a path "api/testhub/:send?api-version=2022-06-01" and in the non-working case (using HubConnetion.SendAsync), we end up performing a POST against the Azure SignalR service with a path "client/?hub=testhub&id=xxx" - most notable difference being the /api/ vs. /client/ endpoint paths.

This is further confirmed by capturing some live logs using Azure SignalR's live trace logging. Azure logs

The difference between the two seems quite fundamental since the HubConnection.SendAsync(...) method seems to forego all the logic contained in the RestApiProvider and the serverless SignalR implementation does not have "concrete" hub methods that the default SignalR service mode does.

Has anyone been able to successfully send messages using the HubConnection.SendAsync(...) from a client in a serverless Azure SignalR environment?


Solution

  • The following three links provided me the knowledge to achieve what I needed using Azure Functions and serverless Azure SignalR.

    https://github.com/aspnet/AzureSignalR-samples/tree/main/samples/BidirectionChat https://github.com/aspnet/AzureSignalR-samples/tree/main/samples/DotnetIsolated-BidirectionChat https://github.com/Azure/azure-signalr/issues/969

    Basically you do need to define that "hub method" in the serverless implementation. With isolated functions it looks as such.

        [Function("Broadcast")]
        [SignalROutput(HubName = "TestHub")]
        public SignalRMessageAction Broadcast(
            [SignalREndpointsInput("TestHub")] SignalREndpoint[] endpoints,
            [SignalRTrigger("TestHub", "messages", "Broadcast", "message")] SignalRInvocationContext invocationContext, 
            string message)
        {
            return new SignalRMessageAction("Broadcast")
            {
                Arguments = new object[] { message },
                Endpoints = endpoints
            };
        }