Dispatcher Server
The Arbiter.Dispatcher.Server package provides the server-side infrastructure for receiving and processing dispatcher requests sent by Blazor WebAssembly clients. It exposes a single HTTP endpoint that accepts both JSON and MessagePack payloads and routes them through the mediator pipeline.
Overview
The DispatcherEndpoint class handles incoming POST /api/dispatcher/send requests. It:
- Reads the
X-Message-Request-Typeheader to determine the CLR type to deserialize - Deserializes the request body using either JSON (
application/json) or MessagePack (application/x-msgpack) - Applies the current
ClaimsPrincipalto any request that implementsIRequestPrincipal - Forwards the deserialized request to
IMediator - Returns the response serialized in the same format as the request
Installation
Install-Package Arbiter.Dispatcher.Server
OR
dotnet add package Arbiter.Dispatcher.Server
Service registration
Register the DispatcherEndpoint singleton and MessagePack serializer options using the AddDispatcherService extension method:
builder.Services.AddDispatcherService();
Endpoint mapping
Map the /api/dispatcher/send route using MapDispatcherService. Apply authorization policies using standard ASP.NET Core endpoint conventions:
// Returns IEndpointConventionBuilder for further customization
app.MapDispatcherService().RequireAuthorization();
This maps a single POST /api/dispatcher/send endpoint that accepts both JSON and MessagePack bodies.
Complete Program.cs example
The following example is adapted from samples/EntityFramework/src/Tracker.Web/Program.cs:
using Arbiter.CommandQuery.Endpoints;
using Arbiter.Dispatcher.Server;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services
.AddRazorComponents()
.AddInteractiveServerComponents()
.AddInteractiveWebAssemblyComponents();
// Register entity command/query endpoints
builder.Services.AddEndpointRoutes();
// Register the DispatcherEndpoint and MessagePack serializer options
builder.Services.AddDispatcherService();
// Configure JSON serializer options for domain types
builder.Services.ConfigureHttpJsonOptions(options =>
options.SerializerOptions.AddDomainOptions());
var app = builder.Build();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode()
.AddInteractiveWebAssemblyRenderMode()
.AddAdditionalAssemblies(typeof(Client.Routes).Assembly);
// Map REST endpoints for entity operations
app.MapEndpointRoutes();
// Map the dispatcher endpoint and require authorization
app.MapDispatcherService().RequireAuthorization();
app.Run();
Request type header
Because the dispatcher endpoint accepts requests for any registered handler type, the server needs to know which CLR type to deserialize the request body into. The client sets the X-Message-Request-Type HTTP header to the assembly-qualified type name of the request before posting:
X-Message-Request-Type: Arbiter.CommandQuery.Queries.EntityIdentifierQuery`2[[System.Int32, ...],[Tracker.Domain.UserReadModel, ...]], Arbiter.CommandQuery
The server resolves this value using Type.GetType(requestTypeName). If the header is missing, empty, or references an unknown type, the endpoint returns HTTP 400 with a Problem Details body.
Note: The
X-Message-Request-Typeheader is set automatically byJsonDispatcherandMessagePackDispatcher. Application code does not need to set it manually. See Client for details.
Serialization formats
The endpoint selects a format based on the Content-Type header of the incoming request:
| Content-Type | Format | Client dispatcher |
|---|---|---|
application/json |
JSON (System.Text.Json) | JsonDispatcher |
application/x-msgpack |
MessagePack (MessagePack-CSharp) | MessagePackDispatcher |
When no Content-Type header is present the endpoint defaults to JSON.
MessagePack response type header
For MessagePack responses the server also writes the X-Message-Response-Type header containing the portable type name of the response object. The MessagePackDispatcher client reads this header when deserializing error responses (Problem Details).
Security
Authorization
Apply authorization policies on the endpoint convention builder returned by MapDispatcherService():
// Require any authenticated user
app.MapDispatcherService().RequireAuthorization();
// Require a specific policy
app.MapDispatcherService().RequireAuthorization("RequireAdminRole");
Principal injection
Requests that implement IRequestPrincipal automatically receive the current ClaimsPrincipal from the HTTP context. This means server-side handler logic—audit field population, tenant filtering, soft-delete visibility—behaves identically to a direct mediator call from a controller or minimal API endpoint.
Antiforgery
The DispatcherEndpoint does not apply antiforgery validation by default. If your application requires antiforgery protection, enable the standard ASP.NET Core antiforgery middleware and call UseAntiforgery() before the dispatcher endpoint is mapped:
builder.Services.AddAntiforgery();
// ...
app.UseAntiforgery();
app.MapDispatcherService().RequireAuthorization();
Error handling
All errors are returned as RFC 7807 Problem Details responses. The format (JSON or MessagePack) matches the Content-Type of the original request.
| Scenario | HTTP status | Detail |
|---|---|---|
Missing X-Message-Request-Type header |
400 | "Missing X-Message-Request-Type header" |
| Unknown type name in header | 400 | "Unable to resolve request type: <name>" |
| Deserialization failure | 400 | "Failed to deserialize request of type: <name>" |
DomainException in handler |
Configured status code | Exception message |
| Unhandled exception | 500 | Exception message |
Performance considerations
MessagePack vs JSON
MessagePack serialization produces smaller payloads and faster serialize/deserialize times compared to JSON. For applications with high request volumes or large response payloads, prefer MessagePackDispatcher on the client side.
HTTP/2
Both JsonDispatcher and MessagePackDispatcher prefer HTTP/2 by default (HttpVersion.Version20 with RequestVersionOrLower). Ensure the server is configured to accept HTTP/2 (the default in Kestrel). HTTP/2 multiplexing reduces latency for concurrent dispatcher calls.
Response compression
Enable response compression in the host application to reduce payload sizes over the wire, especially for JSON responses:
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>();
});
// ...
app.UseResponseCompression();
Logging and diagnostics
The DispatcherEndpoint uses structured logging at the following levels:
| Level | Event |
|---|---|
Warning |
Missing or empty X-Message-Request-Type header |
Warning |
Unable to resolve request type from header value |
Warning |
Failed to deserialize request body |
Error |
Unhandled exception during dispatch |
Enable mediator tracing and metrics via Arbiter.Mediation.OpenTelemetry to gain visibility into handler execution times and throughput:
builder.Services.AddMediatorDiagnostics();
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddMediatorInstrumentation()
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
)
.WithMetrics(metrics => metrics
.AddMediatorInstrumentation()
.AddAspNetCoreInstrumentation()
);
End-to-end example — WASM mode
The following trace illustrates how a query flows from a Blazor WASM component through the dispatcher endpoint to an Entity Framework handler.
Client (WASM)
// IDispatcher injected into a component or service
var query = new EntityIdentifierQuery<int, UserReadModel>(principal, userId);
var user = await dispatcher.Send<EntityIdentifierQuery<int, UserReadModel>, UserReadModel>(
query,
cancellationToken);
HTTP request
POST /api/dispatcher/send HTTP/2
Content-Type: application/x-msgpack
X-Message-Request-Type: Arbiter.CommandQuery.Queries.EntityIdentifierQuery`2[[System.Int32,...],[Tracker.Domain.UserReadModel,...]], Arbiter.CommandQuery
<MessagePack-encoded body>
Server (DispatcherEndpoint)
- Reads
X-Message-Request-Type→ resolvesEntityIdentifierQuery<int, UserReadModel> - Deserializes MessagePack body → populates the query object
- Applies
ClaimsPrincipal→query.Principal = httpContext.User - Calls
IMediator.Send(query)→ routes toEntityIdentifierQueryHandler
Handler
// EntityIdentifierQueryHandler (registered via AddEntityQueries<TContext, TEntity, TKey, TReadModel>)
// Queries the database, applies tenant/soft-delete filters, maps to UserReadModel
// Returns UserReadModel? to the endpoint
HTTP response
HTTP/2 200 OK
Content-Type: application/x-msgpack
X-Message-Response-Type: Tracker.Domain.UserReadModel, Tracker.Shared
<MessagePack-encoded body>