A fast mediator implementation for .NET 9 with background processing and notification persistence.
Inspired by MediatR, this library was created as a free alternative with similar patterns but optimized for performance and includes built-in persistence and background processing.
The name "SwartBerg" means "Black Mountain" in Afrikaans, it is a combination of my surname and my wife's maiden name. If you like to thank me for the library buy me a coffee. Link is at the bottom of this readme.
- High Performance: Precompiled generic delegates (no runtime expression compilation)
- Background Processing: Non-blocking notification dispatch with worker pool
- Pipeline Behaviors: Plug-in cross-cutting concerns
- Configurable Persistence: Pluggable store & serializer
- Retry Logic: Exponential backoff with precomputed delays
- Modern Async Patterns: Optional global ConfigureAwait(false)
- Lightweight: Low allocations, minimal deps
- AOT Friendly: No Expression.Compile, predictable startup
- .NET 9 Ready: Utilizes latest runtime improvements
- .NET 9.0 or later
- Works with:
- .NET 9+ applications
- .NET MAUI applications
- Blazor applications
- ASP.NET Core 9+ applications
- Console applications
- WPF applications
- WinForms applications
Install-Package SwartBerg.Mediatordotnet add package SwartBerg.Mediator<PackageReference Include="SwartBerg.Mediator" Version="1.0.0" />public class GetUserQuery : IRequest<User>
{
public int UserId { get; set; }
}
public class GetUserHandler : IRequestHandler<GetUserQuery, User>
{
public Task<User> Handle(GetUserQuery request, CancellationToken cancellationToken)
{
return Task.FromResult(new User { Id = request.UserId, Name = "John Doe" });
}
}
public class CreateUserCommand : IRequest
{
public string Name { get; set; }
public string Email { get; set; }
}
public class CreateUserHandler : IRequestHandler<CreateUserCommand>
{
public Task Handle(CreateUserCommand request, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
public class UserCreatedNotification : INotification
{
public int UserId { get; set; }
public string Name { get; set; }
}
public class SendWelcomeEmailHandler : INotificationHandler<UserCreatedNotification>
{
public Task Handle(UserCreatedNotification notification, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}builder.Services.AddMediator(typeof(Program).Assembly);public class UserController : ControllerBase
{
private readonly IMediator _mediator;
public UserController(IMediator mediator)
{
_mediator = mediator;
}
[HttpGet("{id}")]
public async Task<User> GetUser(int id)
{
return await _mediator.Send(new GetUserQuery { UserId = id });
}
[HttpPost]
public async Task CreateUser(CreateUserCommand command)
{
await _mediator.Send(command);
await _mediator.Publish(new UserCreatedNotification { UserId = 1, Name = command.Name });
}
}public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
ValidateRequest(request);
return await next();
}
}
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));services.AddSingleton<INotificationPersistence, RedisNotificationPersistence>();
services.AddSingleton<INotificationPersistence, SqlServerNotificationPersistence>();
services.AddMediator(options => options.EnablePersistence = false, typeof(Program).Assembly);services.AddMediator(options =>
{
options.NotificationWorkerCount = 4;
options.EnablePersistence = true;
options.ProcessingInterval = TimeSpan.FromSeconds(30);
options.ProcessingBatchSize = 50;
options.MaxRetryAttempts = 3;
options.InitialRetryDelay = TimeSpan.FromMinutes(2);
options.RetryDelayMultiplier = 2.0;
options.CleanupRetentionPeriod = TimeSpan.FromHours(24);
options.CleanupInterval = TimeSpan.FromHours(1);
// options.UseConfigureAwaitGlobally = false; // only if you need sync ctx
}, typeof(Program).Assembly);Channel-first with optional persistence:
- In-memory channel dispatch
- Optional persistence backup
- Periodic recovery loop
- Periodic cleanup loop
Publish() → Channel → Background Workers
↘
Persist() → Storage
↘
Recovery → Channel
Latest request benchmark excerpt (.NET 9.0.11, Intel i7-13620H, single run):
| Method | Mean (ns) | Allocated (B) | Approx Throughput (ops/sec) |
|---|---|---|---|
| SimpleRequestResponse | 94.66 | 672 | ~10.56 M |
| ComplexRequestResponse | 120.36 | 688 | ~8.31 M |
| SimpleRequestWithoutResponse | 98.39 | 240 | ~10.16 M |
| RequestWith1000Iterations* | 103.45 | 644 | ~9.66 M (avg/op) |
*Batch of 1000 requests total ≈103,448 ns (≈103.45 ns each).
Latest notification benchmark excerpt (.NET 9.0.11, Intel i7-13620H, single run):
| Method | Mean (ms) | Allocated (B) |
|---|---|---|
| PublishSimpleNotification | 15.85 | 950 |
| PublishComplexNotification | 16.00 | 936 |
| Publish100Notifications | 56.09 | 27,261 |
| Publish1000Notifications | 105.54 | 270,549 |
Notes:
- Notification benchmarks include enqueue + background processing overhead.
- Allocation scales primarily with handler count and batch size.
- Single notification latency dominated by worker scheduling (intentionally decoupled).
- Precompiled delegates (AOT-friendly)
- Immutable work item struct
- Pooled task arrays for multi-handler fan-out
- Fast-path synchronous completions
- Exponential retry with precomputed delays
Numbers from a single run; re-run on your hardware for authoritative results.
Run benchmarks:
cd benchmarks
DOTNET_TieredPGO=1 DOTNET_ReadyToRun=0 dotnet run -c Release --filter *Request*
DOTNET_TieredPGO=1 DOTNET_ReadyToRun=0 dotnet run -c Release --filter *Publish*dotnet test- Fork the repository
- Create a feature branch:
git checkout -b feature/amazing-feature - Add changes + tests
- Run benchmarks
- Commit:
git commit -m 'Add amazing feature' - Push:
git push origin feature/amazing-feature - Open PR
MIT License - see LICENSE.
Open issues for bugs or features. Provide clear reproduction steps.
Free forever. If it helps you and you want to buy a coffee:
Always free. No premium features, no paid support.