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.
- Blazor WebAssembly app that sends a POST/PUT request to the Web API.
- The Web API controller gets the SignalR connection ID (called "client ID" in my code) and includes it in a
Mediatrrequest - A
Mediatrpipeline behaviour takes this "client ID" and stores it in the HTTP context via theIHttpContextAccessor - A
SaveChangesInterceptorreads the "client ID" from theIHttpContextAccessorand stores it against the domain event in the database. - The domain event dispatcher publishes the event that includes the "client ID".
- 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.