TheMagnificent11
TheMagnificent11

Reputation: 1456

SignalR message from an ASP.Net Core Web API to a Blazor WebAssembly app

I'm working in .Net 7 for both my Blazor WebAssembly app and my server-side ASP.Net Core Web API.

I'm having trouble receiving the SignalR events in the Blazor WebAssembly app.

The reason I'm using SignalR is because I need to inform a requesting Blazor WebAssembly client that a query projection has been created/updated. The client cannot query the data after directly after making the original HTTP request because the query projection might not be ready (its a CQRS flow).

I think I'm doing the sending the SignalR message correctly as my sender code appears to find the appropriate SignalR client from its connection ID.

Here's the flow of things that take place.

  1. Blazor WebAssembly app that sends a POST/PUT request to the Web API.
  2. The Web API controller gets the SignalR connection ID (called "client ID" in my code) and includes it in a Mediatr request
  3. A Mediatr pipeline behaviour takes this "client ID" and stores it in the HTTP context via the IHttpContextAccessor
  4. A SaveChangesInterceptor reads the "client ID" from the IHttpContextAccessor and stores it against the domain event in the database.
  5. The domain event dispatcher publishes the event that includes the "client ID".
  6. The domain event handler creates/updates the query projection and attempts to

However, my Blazor WebAssembly client does not appear to be receiving the message.

The full code is available on a Git tag I created.

Here's what I think are the relevant bits.

Blazor WebAssembly Program.cs extract

var builder = WebAssemblyHostBuilder.CreateDefault(args);

var serverApiUrl = builder.Configuration["ServerApiUrl"];
if (string.IsNullOrWhiteSpace(serverApiUrl))
{
    throw new ApplicationException("Could not find API URL");
}

...


builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");

builder.Services.AddFluxor(options =>
{
    options.ScanAssemblies(typeof(Program).Assembly);

#if DEBUG
    options.UseReduxDevTools();
#endif
});

builder.Services.ConfigureMessageReceiver<MessageToActionMapper>(serverApiUrl);

...

await builder.Build().RunAsync();

MessageReceiverConfiguration.cs

public static class MessageReceiverConfiguration
{
    public static IServiceCollection ConfigureMessageReceiver<TMapper>(
        this IServiceCollection services,
        string serverApiUrl)
        where TMapper : class, IMessageToActionMapper
    {
        services.AddSingleton(new HubConnectionBuilder()
            .WithUrl($"{serverApiUrl}/events")
            .WithAutomaticReconnect()
            .Build());

        services.AddTransient<IMessageToActionMapper, TMapper>();
        services.AddTransient<MessageDeserializer>();

        return services;
    }
}

MessageReceiverInitializer.cs (Blazor component)

public class MessageReceiverInitializer : ComponentBase
{
    [Inject]
    private HubConnection HubConnection { get; set; }

    [Inject]
    private MessageDeserializer MessageDeserializer { get; set; }

    [Inject]
    private IMessageToActionMapper MessageToActionMapper { get; set; }

    [Inject]
    private IDispatcher Dispatcher { get; set; }

    [Inject]
    private ILogger<MessageReceiverInitializer> Logger { get; set; }

    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();
        await this.HubConnection.StartAsync();

        this.HubConnection.On<ClientMessage>(nameof(ClientMessage), message =>
        {
            var (messageBody, correlationId) = this.MessageDeserializer.Deserialize(message);
            if (messageBody == null)
            {
                return;
            }

            var action = this.MessageToActionMapper.Map(messageBody, correlationId ?? Guid.Empty);
            if (action == null)
            {
                this.Logger.LogInformation("No action mapped to {@MessageBody}", messageBody);
                return;
            }

            this.Dispatcher.Dispatch(action);
            this.Logger.LogInformation(
                "Action Type {Action} dispatched (Message Body: {@MessageBody})",
                action.GetType().Name,
                messageBody);
        });
    }
}

App.razor

<Fluxor.Blazor.Web.StoreInitializer />
<MessageReceiverInitializer />

<Router AppAssembly="@typeof(App).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
        <FocusOnNavigate RouteData="@routeData" Selector="h1" />
    </Found>
    <NotFound>
        <PageTitle>Not found</PageTitle>
        <LayoutView Layout="@typeof(MainLayout)">
            <p role="alert">Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

Extract from Web API Program.cs

...

builder.Services.ConfigureSignalR();

...

app.UseResponseCompression();
app.MapControllers();
app.MapHub<ClientEventHub>("/events");

...

SignalRConfiguration.cs

public static class SignalRConfiguration
{
    public static IServiceCollection ConfigureSignalR(this IServiceCollection services)
    {
        services.AddSignalR();
        services.AddResponseCompression(opts =>
        {
            opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
                new[] { "application/octet-stream" });
        });
        services.AddHttpContextAccessor();
        services.AddSingleton<IClientService, SignalRClientService>();
        services.AddMediatR(config => config.RegisterServicesFromAssemblies(
            typeof(ClientEvent).Assembly,
            typeof(ClientEventHandler).Assembly));

        return services;
    }
}

**Extract from BaseApiController.cs (how the SignalR connection ID is read and passed through as my "client ID")

[ApiController]
public abstract class BaseApiController : ControllerBase
{
    protected string? ClientId
    {
        get
        {
            var connectionIdFeature = this.HttpContext.Features.Get<IConnectionIdFeature>();
            return connectionIdFeature?.ConnectionId;
        }
    }
}

ClientEventHandler.cs (server-side code that sends the message to the client

internal class ClientEventHandler : INotificationHandler<ClientEvent>
{
    private readonly IHubContext<ClientEventHub> hubContext;
    private readonly ILogger logger;

    public ClientEventHandler(IHubContext<ClientEventHub> hubContext, ILogger logger)
    {
        this.hubContext = hubContext;
        this.logger = logger.ForContext<ClientEventHandler>();
    }

    public async Task Handle(ClientEvent notification, CancellationToken cancellationToken)
    {
        using (LogContext.PushProperty(LoggingConsts.CorrelationId, notification.CorrelationId))
        {
            var clientMessage = notification.ToClientMessage();
            var client = this.hubContext.Clients.Client(notification.ClientId);
            if (client == null)
            {
                this.logger.Information("Could not find SignalR client {ClientId}", notification.ClientId);
                return;
            }

            await client.SendAsync(nameof(ClientMessage), clientMessage, cancellationToken);
            this.logger.Information("Published message to client");
        }
    }
}

ClientEventHub.cs (SignalR hub)

public class ClientEventHub : Hub
{
}

Edit: Not sure about unauthenticated clients (like this question), but this is the recommended way to manage authenticated clients.

Upvotes: 4

Views: 2167

Answers (1)

Jason Pan
Jason Pan

Reputation: 22019

UPDATE

By debugging and looking at the source code, it is found that the connectionID when forwarding messages in the signalr server side does not match the connectionId when the connection is established in the OnConnectedAsync method.

After further investigation by Op, the issue is in the API controller and it gets the SignalR connection ID doesn't match what the hub is reporting for the connection ID.


I investigated the issue, and found the this.Context.ConnectionId not equals to notification.ClientId.

When client side connect to the hub, it will hit the build-in method OnConnectedAsync. I override the method and get the ConnectionId like below.

using Microsoft.AspNetCore.SignalR;
namespace Lewee.Infrastructure.AspNet.SignalR;

public class ClientEventHub : Hub
{
    public override async Task OnConnectedAsync()
    {
        Console.WriteLine(this.Context.ConnectionId);
        await base.OnConnectedAsync();
    }
}

notification.ClientId I found it in ClientEventHandler class, and I found the ClientId != ConnectionId. Then I use below code send the message to all connected user for test, it works well.

await this.hubContext.Clients.All.SendAsync(nameof(ClientMessage), clientMessage, cancellationToken);

So I think it's why your blazor webassembly client side not get message.

According my understanding, we need information about the ConnectionId that manages the user connection.

How to manage client connections in a Blazor Server + ASP.NET Core API + SignalR project

Upvotes: 2

Related Questions