12
votes

I'm trying to make a connection between my ASP.NET Core 3.0 Blazor (server-side) application and the Azure SignalR Service. I'll end up injecting my SignalR client (service) in to a few Blazor components so they'll update my UI/DOM in realtime.

My issue is that I'm receiving the following message when I call my .StartAsync() method on the hub connection:

Response status code does not indicate success: 404 (Not Found).

BootstrapSignalRClient.cs

This file loads my configuration for the SignalR Service including the URL, connection string, key, method name, and hub name. These settings are captured in the static class SignalRServiceConfiguration and used later.

public static class BootstrapSignalRClient
{
    public static IServiceCollection AddSignalRServiceClient(this IServiceCollection services, IConfiguration configuration)
    {
        SignalRServiceConfiguration signalRServiceConfiguration = new SignalRServiceConfiguration();
        configuration.Bind(nameof(SignalRServiceConfiguration), signalRServiceConfiguration);

        services.AddSingleton(signalRServiceConfiguration);
        services.AddSingleton<ISignalRClient, SignalRClient>();

        return services;
    }
}

SignalRServiceConfiguration.cs

public class SignalRServiceConfiguration
{
    public string ConnectionString { get; set; }
    public string Url { get; set; }
    public string MethodName { get; set; }
    public string Key { get; set; }
    public string HubName { get; set; }
}

SignalRClient.cs

public class SignalRClient : ISignalRClient
{
    public delegate void ReceiveMessage(string message);
    public event ReceiveMessage ReceiveMessageEvent;

    private HubConnection hubConnection;

    public SignalRClient(SignalRServiceConfiguration signalRConfig)
    {
        hubConnection = new HubConnectionBuilder()
            .WithUrl(signalRConfig.Url + signalRConfig.HubName)
            .Build();            
    }

    public async Task<string> StartListening(string id)
    {
        // Register listener for a specific id
        hubConnection.On<string>(id, (message) => 
        {
            if (ReceiveMessageEvent != null)
            {
                ReceiveMessageEvent.Invoke(message);
            }
        });

        try
        {
            // Start the SignalR Service connection
            await hubConnection.StartAsync(); //<---I get an exception here
            return hubConnection.State.ToString();
        }
        catch (Exception ex)
        {
            return ex.Message;
        }            
    }

    private void ReceiveMessage(string message)
    {
        response = JsonConvert.DeserializeObject<dynamic>(message);
    }
}

I have experience using SignalR with .NET Core where you add it so the Startup.cs file using .AddSignalR().AddAzureSignalR() and map a hub in the app config and doing it this way requires certain 'configuration' parameters to be established (i.e. connection string).

Given my situation, where does HubConnectionBuilder get the connection string or a key to authenticate to the SignalR Service?

Is it possible the 404 message is a result of the missing key/connection string?

2
.WithUrl(signalRConfig.Url + signalRConfig.HubName) Can you verify this is resulting in the correct Url? (By breakpoint or logging?)Fildor
I found it useful to have the base Uri as Uri and construct the complete one via Uri(Uri, string)Fildor
interestingly it was a 'red herring' and had nothing to do with the 404.Jason Shave

2 Answers

10
votes

Okay so it turns out the documentation is lacking a key piece of information here. If you're using the .NET SignalR Client connecting to the Azure SignalR Service, you need to request a JWT token and present it when creating the hub connection.

If you need to authenticate on behalf of a user you can use this example.

Otherwise, you can set up a "/negotiate" endpoint using a web API such as an Azure Function to retrive a JWT token and client URL for you; this is what I ended up doing for my use case. Information about creating an Azure Function to get your JWT token and URL can be found here.

I created a class to hold these two values as such:

SignalRConnectionInfo.cs

public class SignalRConnectionInfo
{
    [JsonProperty(PropertyName = "url")]
    public string Url { get; set; }
    [JsonProperty(PropertyName = "accessToken")]
    public string AccessToken { get; set; }
}

I also created a method inside my SignalRService to handle the interaction with the web API's "/negotiate" endpoint in Azure, the instantiation of the hub connection, and the use of an event + delegate for receiving messages as follows:

SignalRClient.cs

public async Task InitializeAsync()
{
    SignalRConnectionInfo signalRConnectionInfo;
    signalRConnectionInfo = await functionsClient.GetDataAsync<SignalRConnectionInfo>(FunctionsClientConstants.SignalR);

    hubConnection = new HubConnectionBuilder()
        .WithUrl(signalRConnectionInfo.Url, options =>
        {
           options.AccessTokenProvider = () => Task.FromResult(signalRConnectionInfo.AccessToken);
        })
        .Build();
}

The functionsClient is simply a strongly typed HttpClient pre-configured with a base URL and the FunctionsClientConstants.SignalR is a static class with the "/negotiate" path which is appended to the base URL.

Once I had this all set up I called the await hubConnection.StartAsync(); and it "connected"!

After all this I set up a static ReceiveMessage event and a delegate as follows (in the same SignalRClient.cs):

public delegate void ReceiveMessage(string message);
public static event ReceiveMessage ReceiveMessageEvent;

Lastly, I implemented the ReceiveMessage delegate:

await signalRClient.InitializeAsync(); //<---called from another method

private async Task StartReceiving()
{
    SignalRStatus = await signalRClient.ReceiveReservationResponse(Response.ReservationId);
    logger.LogInformation($"SignalR Status is: {SignalRStatus}");

    // Register event handler for static delegate
    SignalRClient.ReceiveMessageEvent += signalRClient_receiveMessageEvent;
}

private async void signalRClient_receiveMessageEvent(string response)
{
    logger.LogInformation($"Received SignalR mesage: {response}");
    signalRReservationResponse = JsonConvert.DeserializeObject<SignalRReservationResponse>(response);
    await InvokeAsync(StateHasChanged); //<---used by Blazor (server-side)
}

I've provided documentation updates back to the Azure SignalR Service team and sure hope this helps someone else!

0
votes

Update: the sample with the serverless sample is deprecated for the management SDK (sample). The management SDK uses a negotiation server.