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: Uses expression trees and caching to avoid reflection overhead
- Background Processing: Handles notifications in the background without blocking your app
- Pipeline Behaviors: Add logging, validation, and other cross-cutting concerns easily
- Configurable Persistence: File-based persistence with JSON serialization (can be replaced)
- Retry Logic: Automatically retries failed notifications with exponential backoff
- Modern Async Patterns: Built-in ConfigureAwait(false) for optimal performance
- Lightweight: Minimal dependencies, optimized for performance
- .NET 9 Ready: Takes advantage of .NET 9 performance 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 });
}
}Add cross-cutting concerns like validation, logging, or caching:
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<,>));By default, the mediator uses file-based persistence for crash recovery. The channel handles processing for high throughput.
Replace with custom persistence:
services.AddSingleton<INotificationPersistence, RedisNotificationPersistence>();
services.AddSingleton<INotificationPersistence, SqlServerNotificationPersistence>();
services.AddMediator(options => options.EnablePersistence = false, typeof(Program).Assembly);Example Redis implementation:
public class RedisNotificationPersistence : INotificationPersistence
{
private readonly IDatabase _database;
public RedisNotificationPersistence(IConnectionMultiplexer redis)
{
_database = redis.GetDatabase();
}
public async Task<string> PersistAsync(NotificationWorkItem workItem, CancellationToken cancellationToken = default)
{
var id = Guid.NewGuid().ToString();
var persistedItem = new PersistedNotificationWorkItem
{
Id = id,
WorkItem = workItem,
CreatedAt = DateTime.UtcNow,
AttemptCount = 0
};
var key = $"mediator:notifications:{id}";
var value = JsonSerializer.Serialize(persistedItem);
await _database.StringSetAsync(key, value);
return id;
}
// implement other interface methods
}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);
// ConfigureAwait is enabled by default for optimal performance
// Set to false if you need to preserve synchronization context
// options.UseConfigureAwaitGlobally = false;
}, typeof(Program).Assembly);The mediator uses a channel-first approach with optional persistence backup:
- Primary Processing: In-memory channels for fast, reliable processing
- Persistence: Optional backup that saves notifications to disk/storage
- Recovery: On startup/timer, recovers persisted notifications back into the channel
- Cleanup: Removes old persisted items periodically
Publish() ? Channel (immediate) ? Background Workers
?
Persist() (async backup) ? Storage
?
Recovery Timer ? Load from Storage ? Back to Channel
BenchmarkDotNet results on .NET 9 (Intel Core i7-13620H):
| Method | Mean | Error | StdDev | Allocated | Throughput |
|---|---|---|---|---|---|
| SingleRequest | 92.80 ns | 0.78 ns | 0.73 ns | 680 B | ~10.8M req/sec |
| BatchRequests100 | 9.14 ?s | 0.11 ?s | 0.10 ?s | 64 KB | ~109K batches/sec |
| Method | Mean | Error | StdDev | Allocated | Throughput |
|---|---|---|---|---|---|
| SingleNotification | 377.45 ns | 5.13 ns | 4.80 ns | 726 B | ~2.6M notifs/sec |
| BatchNotifications100 | 41.72 ?s | 0.62 ?s | 0.55 ?s | 80 KB | ~24K batches/sec |
- Blazing requests: 93ns per request - one of the fastest mediators available
- Ultra-fast notifications: 377ns with background processing
- Outstanding throughput: 10.8 million requests per second capability
- Efficient batch processing: 100 requests in 9.1?s
- Low memory usage: Optimized allocations with compiled delegates
- Pipeline behavior support: Full hot path optimization for behaviors too
- ConfigureAwait overhead: Zero performance impact when not used
- Enterprise-grade performance: Perfect for hyperscale production systems
Run benchmarks:
cd benchmarks
dotnet run -c Releasedotnet test
cd benchmarks
dotnet run -c Release- Fork the repository
- Create a feature branch:
git checkout -b feature/amazing-feature - Make your changes and add tests
- Run benchmarks to ensure performance
- Commit your changes:
git commit -m 'Add amazing feature' - Push to the branch:
git push origin feature/amazing-feature - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
- Create an issue for bug reports or feature requests
- Check existing issues before creating new ones
- Provide clear reproduction steps for bugs
SwartBerg.Mediator is completely free and always will be. Use it, modify it, distribute it - no strings attached!
If this library happens to save you time or makes your project better, and you feel like buying me a coffee out of the goodness of your heart, that's awesome but totally optional:
Remember: This library will always be free, regardless of donations. No premium features, no paid support, no strings attached.