4

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.

5
  • After my analysis, I think we should ensure that this project can run normally in VS2022 when docker is not applicable. Commented Mar 7, 2023 at 9:56
  • I think the key point should be that the docker container limits the connection of signalr, you can continue to investigate in this direction. Commented Mar 7, 2023 at 9:58
  • Thanks Jason, I'll give it a go without Docker Commented Mar 8, 2023 at 18:06
  • 1
    Tried without running the API in a container, no luck. The Blazor WASM app still doesn't receive the SignalR message. Commented Mar 10, 2023 at 9:22
  • Hi, I debug the application step by step, and found something and also do some test, pls verify it, I believe it's the root cause, until now I am not sure what is the ClientId in notification, if you want to know, I am willing to check next week. Commented Mar 10, 2023 at 15:14

1 Answer 1

2
+100

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

Sign up to request clarification or add additional context in comments.

8 Comments

I did what you said and sent the message to all clients instead of the specific one and the message was never received by the MessageReceiverInitializer (I tried with and without Docker, neither worked). I've added some extra code to the question; it's how the controller initially gets the SignalR connection ID to pass down to the Mediatr request.
Hi @TheMagnificent11 Everything seems to be working fine for me, or please let me know if I have misunderstood something.
Hi @Jason, what happens for you if you click the "Use" button? Does the SignalR message come back to the client? That doesn't work for me and its way the sample application should be used.
Hi @Jason, could you please edit your answer and say that the the issue is in the API controller and how it gets the SignalR connection ID because it doesn't match what the hub is reporting for the connection ID? Then I will mark your answer verified.
Marked ths answer as verified... hopefully it was before the bounty expired
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.