This POC explores state management with Fluxor (Redux) and handling all service calls through a single gRPC endpoint, essentially MediatR over gRPC.
To run, configure solution to start multiple projects: Forecasts, Gateway, and UI.
The Fluxor portion is adapted from the Advanced Blazor State Management Using Fluxor by Eric King. This POC shows how Fluxor can be used to not only manage state, but also create complex interactions between components.
This was adapted from a talk by Jeffrey Palermo on Blazor Architecure Patterns.
This is the single gRPC endpoint for the entire UI.
service Gateway {
rpc Publish (GatewayEnvelope) returns (GatewayEnvelope);
}
message GatewayEnvelope {
string Type = 1;
bytes Body = 2;
}
Body
is basically a serialized MediatR request. The Gateway deserializes the request and uses MediatR to send it to the appropriate handler. The handler is responsible for
orchestrating service calls, mapping protobuf back to Models, etc. The response from the handler is serialized and returned back to the caller as a GatewayEnvelope
.
The Gateway knows about all of the available services, but the UI only knows about the Gateway.
Instead of calling the gRPC service directly, a Bus is used.
// ForecastQuery.cs
public record ForecastQuery(DateTime StartDate) : IRemoteableRequest<IEnumerable<WeatherForecast>>;
// elsewhere
var forecasts = await _bus.Send(new ForecastQuery(DateTime.UtcNow));
The Bus on the UI side calls the GatwayPublisher, which is a simple wrapper around the gRPC client. This could be extended to include identity, claims, roles, etc.
public class RemoteableBus : IBus
{
private readonly GatewayPublisher _gateway;
public RemoteableBus(GatewayPublisher gateway)
{
_gateway = gateway;
}
public async Task<TResponse> Send<TResponse>(IMessage<TResponse> request, CancellationToken cancellationToken = default)
{
if (request is IRemoteableRequest<TResponse> remoteableRequest)
{
// Call the Gateway
var response = await _gateway.Publish(remoteableRequest);
// Extract body from response
var result = Deserializer.Deserialize<TResponse>(response);
return result;
}
throw new Exception("Request is not remoteable");
}
}
public class GatewayPublisher
{
private readonly Gateway.GatewayClient _client;
public GatewayPublisher(Gateway.GatewayClient client)
{
_client = client;
}
public async Task<GatewayEnvelope> Publish<TResponse>(IRemoteableRequest<TResponse> request)
{
GatewayEnvelope message = Serializer.Serialize(request);
return await _client.PublishAsync(message);
}
}