A lightweight state management library for Blazor applications (WebAssembly and Server) implementing the Mediator pattern with CQRS support, global handlers for cross-cutting concerns, UI message bus for component coordination, Result monad pattern, and async result tracking. Features thread-safe operations, pub/sub messaging, pipeline behaviors, and functional error handling.
$ dotnet add package BlazorFlowA lightweight state management library for Blazor WebAssembly and Blazor Server applications implementing the Mediator pattern with CQRS support and async result tracking.
Install via NuGet Package Manager:
dotnet add package BlazorFlow --version 0.1.0-preview.1
Or via Package Manager Console:
Install-Package BlazorFlow -Version 0.1.0-preview.1
Program.csusing BlazorFlow.Extensions;
using System.Reflection;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
// Register BlazorFlow with automatic handler discovery
builder.Services.AddBlazorFlowStateMediator(Assembly.GetExecutingAssembly());
await builder.Build().RunAsync();
using BlazorFlow.Abstractions;
// Query that returns data
public record GetWeatherForecastQuery : IBlazorCommandRequest<WeatherForecast[]>;
// Command that performs an action
public record IncrementCounterCommand(int Amount) : IBlazorCommandRequest<int>;
using BlazorFlow.Abstractions;
public class GetWeatherForecastHandler : IBlazorCommand<GetWeatherForecastQuery, WeatherForecast[]>
{
public async Task<WeatherForecast[]> HandleAsync(
GetWeatherForecastQuery request,
ICommandContext context,
CancellationToken cancellationToken = default)
{
// Simulate API call
await Task.Delay(1000, cancellationToken);
return new[]
{
new WeatherForecast(DateOnly.FromDateTime(DateTime.Now), 20, "Sunny")
};
}
}
@page "/weather"
@using BlazorFlow.Abstractions
@inject IStateMessageBus MessageBus
<h3>Weather Forecast</h3>
@if (weatherResult == null)
{
<button @onclick="LoadWeather">Load Weather</button>
}
else if (weatherResult.IsLoading)
{
<p>Loading...</p>
}
else if (weatherResult.IsSuccess && weatherResult.Value != null)
{
<ul>
@foreach (var forecast in weatherResult.Value)
{
<li>@forecast.Date: @forecast.TemperatureC°C - @forecast.Summary</li>
}
</ul>
}
else if (weatherResult.IsError)
{
<div class="alert alert-danger">
Error: @weatherResult.ErrorMessage
</div>
}
@code {
private IAsyncResult<WeatherForecast[]>? weatherResult;
private void LoadWeather()
{
weatherResult = MessageBus.Publish<GetWeatherForecastQuery, WeatherForecast[]>(
new GetWeatherForecastQuery());
weatherResult.StateChanged += StateHasChanged;
}
}
Navigate from command handlers safely in both Blazor WASM and Server:
public class LoginHandler : IBlazorCommand<LoginCommand, LoginResult>
{
public async Task<LoginResult> HandleAsync(
LoginCommand request,
ICommandContext context,
CancellationToken cancellationToken = default)
{
// Authenticate user
var user = await AuthenticateAsync(request.Username, request.Password);
if (user != null)
{
// Thread-safe navigation - works in both WASM and Server!
context.NavigateTo("/dashboard");
return new LoginResult { Success = true };
}
return new LoginResult { Success = false, Error = "Invalid credentials" };
}
}
Key Benefits:
context.NavigateTo() instead of injecting NavigationManagerHandlers can notify the UI during long-running operations:
public class ProcessFilesHandler : IBlazorCommand<ProcessFilesCommand, FileProcessingResult>
{
public async Task<FileProcessingResult> HandleAsync(
ProcessFilesCommand request,
ICommandContext context,
CancellationToken cancellationToken = default)
{
for (int i = 0; i < files.Count; i++)
{
await ProcessFileAsync(files[i], cancellationToken);
// Notify UI to re-render with progress
context.StateHasChanged();
}
return new FileProcessingResult(files.Count);
}
}
Chain operations with reactive callbacks:
var result = MessageBus.Publish<AddTodoCommand, TodoItem>(new AddTodoCommand("New Task"));
result.StateChanged += StateHasChanged;
result.OnSuccess(todo =>
{
Console.WriteLine($"Todo added: {todo.Title}");
LoadTodos(); // Refresh the list
});
result.OnError(ex =>
{
Console.WriteLine($"Error: {ex.Message}");
});
Use the Match method for functional-style result handling:
var message = result.Match(
onSuccess: value => $"Success: {value}",
onError: ex => $"Error: {ex.Message}",
onLoading: () => "Loading..."
);
NEW in v0.3.0: Add cross-cutting concerns like logging, validation, auditing, and error handling to all commands using global handlers.
Global handlers execute in a pipeline around your command handlers:
OnRequest → Main Handler → OnSuccess/OnError
Register global handlers in Program.cs:
using BlazorFlow.Extensions;
builder.Services.AddBlazorFlowMediator(options =>
{
// Add global request handlers (execute before command)
options.AddRequestHandler<LoggingRequestHandler>();
options.AddRequestHandler<ValidationRequestHandler>();
// Add global success handlers (execute after success)
options.AddSuccessHandler<AuditingSuccessHandler>();
// Add global error handlers (execute on errors)
options.AddErrorHandler<ErrorNotificationHandler>();
}, Assembly.GetExecutingAssembly());
public class LoggingRequestHandler : IBlazorCommandRequestHandler
{
private readonly ILogger<LoggingRequestHandler> _logger;
public LoggingRequestHandler(ILogger<LoggingRequestHandler> logger)
{
_logger = logger;
}
public Task OnRequestAsync(RequestContext context, CancellationToken cancellationToken = default)
{
_logger.LogInformation("Executing command: {RequestType} at {Timestamp}",
context.RequestType?.Name ?? "Unknown",
context.Timestamp);
// Access request data if needed:
// var request = context.GetRequest<YourRequestType>();
return Task.CompletedTask;
}
}
public class ErrorNotificationHandler : IBlazorCommandErrorHandler
{
private readonly ILogger<ErrorNotificationHandler> _logger;
public ErrorNotificationHandler(ILogger<ErrorNotificationHandler> logger)
{
_logger = logger;
}
public Task OnErrorAsync(RequestContext context, CancellationToken cancellationToken = default)
{
_logger.LogError(context.Exception, "Command {CommandType} failed",
context.RequestType?.Name ?? "Unknown");
// Access request data if needed:
// var request = context.GetRequest<YourRequestType>();
// You could also publish error notifications to the UI here
return Task.CompletedTask;
}
}
context.GetRequest<T>() and context.GetResponse<T>()RequestContext with ICommandContext for UI operationsBlazorFlow implements the Mediator pattern with CQRS principles:
Blazor Component
↓ (Publishes request)
IStateMessageBus
↓ (Routes to handler)
IBlazorCommand Handler
↓ (Executes logic)
IAsyncResult<T>
↓ (Tracks state: Loading → Success/Error)
Blazor Component (Re-renders via StateChanged event)
For more examples and detailed documentation, see the samples directory.
This project is licensed under the MIT License - see the LICENSE file for details.
Contributions are welcome! Please feel free to submit a Pull Request.
Note: This is a preview release. APIs may change in future versions.