ModernMediator 0.2.2-alpha
dotnet add package ModernMediator --version 0.2.2-alpha
NuGet\Install-Package ModernMediator -Version 0.2.2-alpha
<PackageReference Include="ModernMediator" Version="0.2.2-alpha" />
<PackageVersion Include="ModernMediator" Version="0.2.2-alpha" />
<PackageReference Include="ModernMediator" />
paket add ModernMediator --version 0.2.2-alpha
#r "nuget: ModernMediator, 0.2.2-alpha"
#:package ModernMediator@0.2.2-alpha
#addin nuget:?package=ModernMediator&version=0.2.2-alpha&prerelease
#tool nuget:?package=ModernMediator&version=0.2.2-alpha&prerelease
ModernMediator
A modern, feature-rich mediator library for .NET 8 that combines the best of pub/sub and request/response patterns with advanced features for real-world applications.
Status: Alpha
Core features tested. Edge cases may exist. Please report issues on GitHub.
Documentation
Features
Core Patterns
- Request/Response - Send requests and receive typed responses
- Streaming -
IAsyncEnumerablesupport for large datasets - Pub/Sub (Notifications) - Fire-and-forget event broadcasting
- Pub/Sub with Callbacks - Collect responses from multiple subscribers
Pipeline
- Pipeline Behaviors - Wrap handler execution for cross-cutting concerns
- Pre-Processors - Run logic before handlers execute
- Post-Processors - Run logic after handlers complete
- Exception Handlers - Clean, typed exception handling separate from business logic
Source Generators & AOT
- Source Generators - Compile-time code generation eliminates reflection
- Native AOT Compatible - Full support for ahead-of-time compilation
- Compile-Time Diagnostics - Catch missing handlers during build, not runtime
- Zero Reflection - Generated
AddModernMediatorGenerated()for maximum performance - CachingMode - Eager (default) or Lazy initialization for cold start optimization
Advanced Capabilities
- Weak References - Handlers can be garbage collected, preventing memory leaks
- Strong References - Opt-in for handlers that must persist
- Runtime Subscribe/Unsubscribe - Dynamic handler registration (perfect for plugins)
- Predicate Filters - Filter messages at subscription time
- Covariance - Subscribe to base types, receive derived messages
- String Key Routing - Topic-based subscriptions alongside type-based
Async-First Design
- True Async Handlers -
SubscribeAsyncwith properTask.WhenAllaggregation - Cancellation Support - All async operations respect
CancellationToken - Parallel Execution - Notifications execute handlers concurrently
Error Handling
- Three Policies -
ContinueAndAggregate,StopOnFirstError,LogAndContinue - HandlerError Event - Hook for logging and monitoring
- Exception Unwrapping - Clean stack traces without reflection noise
UI Thread Support
- Built-in Dispatchers - WPF, WinForms, MAUI, ASP.NET Core, Avalonia
- SubscribeOnMainThread - Automatic UI thread marshalling
Modern .NET Integration
- Dependency Injection -
services.AddModernMediator() - Assembly Scanning - Auto-discover handlers, behaviors, and processors
- Multi-target -
net8.0andnet8.0-windows - Interface-first -
IMediatorfor testability and mocking
Installation
dotnet add package ModernMediator
Quick Start
Setup with Dependency Injection (Recommended)
IMediator is registered as Scoped by default, allowing handlers to resolve scoped dependencies like DbContext.
// Program.cs - with assembly scanning (uses reflection)
services.AddModernMediator(config =>
{
config.RegisterServicesFromAssemblyContaining<Program>();
});
// Or use source-generated registration (AOT-compatible, no reflection)
services.AddModernMediatorGenerated();
// Or with configuration
services.AddModernMediator(config =>
{
config.RegisterServicesFromAssemblyContaining<Program>();
config.ErrorPolicy = ErrorPolicy.LogAndContinue;
config.Configure(m => m.SetDispatcher(new WpfDispatcher()));
});
Setup without DI
// Singleton (shared instance) - ideal for Pub/Sub across the application
IMediator mediator = Mediator.Instance;
// Or create isolated instance
IMediator mediator = Mediator.Create();
Source Generators
ModernMediator includes a source generator that discovers handlers at compile time and generates registration code. This provides:
- Zero reflection at runtime - All handler discovery happens during compilation
- Native AOT support - Works with ahead-of-time compilation
- Faster startup - No assembly scanning at runtime
- Compile-time diagnostics - Missing or duplicate handlers detected during build
Generated Code
The source generator creates two files:
ModernMediator.Generated.g.cs - DI registration without reflection:
// Auto-generated - use instead of assembly scanning
services.AddModernMediatorGenerated();
// With configuration (ErrorPolicy, CachingMode, Dispatcher)
services.AddModernMediatorGenerated(config =>
{
config.ErrorPolicy = ErrorPolicy.LogAndContinue;
config.CachingMode = CachingMode.Lazy;
config.Configure(m => m.SetDispatcher(new WpfDispatcher()));
});
ModernMediator.SendExtensions.g.cs - Strongly-typed Send methods:
// Generated extension methods bypass reflection entirely
var user = await mediator.Send(new GetUserQuery(42)); // No reflection!
Diagnostics
| Code | Description |
|---|---|
| MM001 | Duplicate handler - multiple handlers for same request |
| MM002 | No handler found - request type has no registered handler |
| MM003 | Abstract handler - handler class cannot be abstract |
CachingMode
Control when handler wrappers and lookups are initialized:
services.AddModernMediator(config =>
{
// Eager (default) - initialize everything on first mediator access
// Best for long-running applications where startup cost is amortized
config.CachingMode = CachingMode.Eager;
// Lazy - initialize handlers on-demand as messages are processed
// Best for serverless, Native AOT, or cold start scenarios
config.CachingMode = CachingMode.Lazy;
});
Usage
Request/Response
// Define request and response
public record GetUserQuery(int UserId) : IRequest<UserDto>;
public record UserDto(int Id, string Name, string Email);
// Define handler
public class GetUserHandler : IRequestHandler<GetUserQuery, UserDto>
{
public async Task<UserDto> Handle(GetUserQuery request, CancellationToken ct = default)
{
var user = await _db.Users.FindAsync(request.UserId, ct);
return new UserDto(user.Id, user.Name, user.Email);
}
}
// Send request
var user = await mediator.Send(new GetUserQuery(42));
Commands (No Return Value)
// Define command
public record CreateUserCommand(string Name, string Email) : IRequest;
// Define handler
public class CreateUserHandler : IRequestHandler<CreateUserCommand, Unit>
{
public async Task<Unit> Handle(CreateUserCommand request, CancellationToken ct = default)
{
await _db.Users.AddAsync(new User(request.Name, request.Email), ct);
await _db.SaveChangesAsync(ct);
return Unit.Value;
}
}
// Send command
await mediator.Send(new CreateUserCommand("John", "john@example.com"));
Streaming
// Define stream request
public record GetAllUsersRequest(int PageSize) : IStreamRequest<UserDto>;
// Define stream handler
public class GetAllUsersHandler : IStreamRequestHandler<GetAllUsersRequest, UserDto>
{
public async IAsyncEnumerable<UserDto> Handle(
GetAllUsersRequest request,
[EnumeratorCancellation] CancellationToken ct = default)
{
await foreach (var user in _db.Users.AsAsyncEnumerable().WithCancellation(ct))
{
yield return new UserDto(user.Id, user.Name, user.Email);
}
}
}
// Consume stream
await foreach (var user in mediator.CreateStream(new GetAllUsersRequest(100), ct))
{
Console.WriteLine(user.Name);
}
Pipeline Behaviors
Pipeline behaviors wrap handler execution for cross-cutting concerns like logging, validation, and transactions.
Open Generic Behaviors (Apply to All Requests)
Open generic behaviors must be registered explicitly with AddOpenBehavior():
// Logging behavior that wraps all requests
public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken ct)
{
_logger.LogInformation("Handling {Request}", typeof(TRequest).Name);
var response = await next();
_logger.LogInformation("Handled {Request}", typeof(TRequest).Name);
return response;
}
}
// Validation behavior
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken ct)
{
var errors = await _validator.ValidateAsync(request, ct);
if (errors.Any()) throw new ValidationException(errors);
return await next();
}
}
// Register open generic behaviors explicitly
services.AddModernMediator(config =>
{
config.RegisterServicesFromAssemblyContaining<Program>();
config.AddOpenBehavior(typeof(LoggingBehavior<,>));
config.AddOpenBehavior(typeof(ValidationBehavior<,>));
});
Note: Assembly scanning skips open generic types. Always use
AddOpenBehavior()for behaviors that apply to all request types.
Closed Generic Behaviors (Apply to Specific Requests)
Behaviors for specific request types are discovered by assembly scanning:
// Behavior for a specific request type
public class GetUserCachingBehavior : IPipelineBehavior<GetUserQuery, UserDto>
{
public async Task<UserDto> Handle(
GetUserQuery request,
RequestHandlerDelegate<UserDto> next,
CancellationToken ct)
{
if (_cache.TryGet(request.UserId, out var cached))
return cached;
var result = await next();
_cache.Set(request.UserId, result);
return result;
}
}
// Auto-discovered by assembly scanning
services.AddModernMediator(config =>
{
config.RegisterServicesFromAssemblyContaining<Program>();
});
Pre/Post Processors
// Pre-processor runs before handler
public class AuthorizationPreProcessor<TRequest> : IRequestPreProcessor<TRequest>
{
public Task Process(TRequest request, CancellationToken ct)
{
if (!_auth.IsAuthorized(request))
throw new UnauthorizedException();
return Task.CompletedTask;
}
}
// Post-processor runs after handler
public class CachingPostProcessor<TRequest, TResponse> : IRequestPostProcessor<TRequest, TResponse>
{
public Task Process(TRequest request, TResponse response, CancellationToken ct)
{
_cache.Set(request, response);
return Task.CompletedTask;
}
}
// Register processors
services.AddModernMediator(config =>
{
config.RegisterServicesFromAssemblyContaining<Program>();
config.AddRequestPreProcessor<AuthorizationPreProcessor>();
config.AddRequestPostProcessor<CachingPostProcessor>();
});
Exception Handlers
Exception handlers provide clean, typed exception handling separate from your business logic. They can return an alternate response or let the exception bubble up.
// Define an exception handler for a specific exception type
public class NotFoundExceptionHandler : RequestExceptionHandler<GetUserQuery, UserDto, NotFoundException>
{
protected override Task<ExceptionHandlingResult<UserDto>> Handle(
GetUserQuery request,
NotFoundException exception,
CancellationToken ct)
{
// Return an alternate response
return Handled(new UserDto(0, "Unknown User"));
// Or let the exception bubble up
// return NotHandled;
}
}
// Register exception handlers
services.AddModernMediator(config =>
{
config.RegisterServicesFromAssemblyContaining<Program>();
config.AddExceptionHandler<NotFoundExceptionHandler>();
});
Exception handlers walk the exception type hierarchy, so a handler for Exception will catch all exceptions if no more specific handler is registered.
Pub/Sub (Notifications)
// Define a message
public record OrderCreatedEvent(int OrderId, decimal Total);
// Subscribe
mediator.Subscribe<OrderCreatedEvent>(e =>
Console.WriteLine($"Order {e.OrderId} created: ${e.Total}"));
// Subscribe with filter
mediator.Subscribe<OrderCreatedEvent>(
e => NotifyVipTeam(e),
filter: e => e.Total > 10000);
// Publish
mediator.Publish(new OrderCreatedEvent(123, 599.99m));
Pub/Sub and DI Scoping
When using dependency injection, IMediator is registered as Scoped. This means Pub/Sub subscriptions are per-scope:
// Subscriptions on DI-injected IMediator are scoped to that request/scope
public class MyService
{
public MyService(IMediator mediator)
{
// This subscription lives only as long as this scope
mediator.Subscribe<SomeEvent>(HandleEvent);
}
}
// For application-wide shared subscriptions, use the static singleton:
Mediator.Instance.Subscribe<OrderCreatedEvent>(e => GlobalHandler(e));
Async Handlers
// Subscribe async
mediator.SubscribeAsync<OrderCreatedEvent>(async e =>
{
await SaveToDatabase(e);
await SendEmailNotification(e);
});
// Publish and await all handlers
await mediator.PublishAsyncTrue(new OrderCreatedEvent(123, 599.99m));
Pub/Sub with Callbacks
Collect responses from multiple subscribers - perfect for confirmation dialogs, validation, or aggregating data from multiple sources:
// Define message and response types
public record ConfirmationRequest(string Message);
public record ConfirmationResponse(bool Confirmed, string Source);
// Subscribe with a response
mediator.Subscribe<ConfirmationRequest, ConfirmationResponse>(
request => new ConfirmationResponse(
Confirmed: ShowDialog(request.Message),
Source: "DialogService"),
weak: false);
// Publish and collect all responses
var responses = mediator.Publish<ConfirmationRequest, ConfirmationResponse>(
new ConfirmationRequest("Delete this item?"));
if (responses.Any(r => r.Confirmed))
{
DeleteItem();
}
Async Callbacks
// Multiple async validators
mediator.SubscribeAsync<ValidateRequest, ValidationResult>(
async request => await ValidateLengthAsync(request));
mediator.SubscribeAsync<ValidateRequest, ValidationResult>(
async request => await ValidateFormatAsync(request));
// Publish and await all responses
var results = await mediator.PublishAsync<ValidateRequest, ValidationResult>(
new ValidateRequest("user input"));
var errors = results.Where(r => !r.IsValid).ToList();
Key Differences from Request/Response
| Pattern | Handlers | Use Case |
|---|---|---|
Send<TResponse> |
Exactly 1 | CQRS commands/queries |
Publish<TMsg, TResp> |
0 to N | Collect responses from multiple subscribers |
Covariance (Polymorphic Dispatch)
public record AnimalEvent(string Name);
public record DogEvent(string Name, string Breed) : AnimalEvent(Name);
public record CatEvent(string Name, int LivesRemaining) : AnimalEvent(Name);
// This handler receives ALL animal events
mediator.Subscribe<AnimalEvent>(e => Console.WriteLine($"Animal: {e.Name}"));
// These are also delivered to the AnimalEvent handler
mediator.Publish(new DogEvent("Rex", "German Shepherd"));
mediator.Publish(new CatEvent("Whiskers", 9));
String Key Routing
// Subscribe to specific topics
mediator.Subscribe<OrderEvent>("orders.created", e => HandleNewOrder(e));
mediator.Subscribe<OrderEvent>("orders.shipped", e => HandleShippedOrder(e));
mediator.Subscribe<OrderEvent>("orders.cancelled", e => HandleCancelledOrder(e));
// Publish to specific topic
mediator.Publish("orders.created", new OrderEvent(...));
Weak vs Strong References
// Weak reference (default) - handler can be GC'd
mediator.Subscribe<Event>(handler.OnEvent, weak: true);
// Strong reference - handler persists until unsubscribed
mediator.Subscribe<Event>(handler.OnEvent, weak: false);
Unsubscribing
// Subscribe returns a disposable token
var subscription = mediator.Subscribe<Event>(OnEvent);
// Unsubscribe when done
subscription.Dispose();
// Or use with 'using' for scoped subscriptions
using (mediator.Subscribe<Event>(OnEvent))
{
// Handler is active here
}
// Handler is automatically unsubscribed
UI Thread Dispatching
// Set dispatcher once at startup
mediator.SetDispatcher(new WpfDispatcher()); // WPF
mediator.SetDispatcher(new WinFormsDispatcher()); // WinForms
mediator.SetDispatcher(new MauiDispatcher()); // MAUI
mediator.SetDispatcher(new AvaloniaDispatcher()); // Avalonia (see below)
// Subscribe to receive on UI thread
mediator.SubscribeOnMainThread<DataChangedEvent>(e =>
UpdateUI(e)); // Safe to update UI here
// Async version
mediator.SubscribeAsyncOnMainThread<DataChangedEvent>(async e =>
{
await ProcessData(e);
UpdateUI(e); // Safe to update UI
});
Avalonia Support
For Avalonia, copy the AvaloniaDispatcher.cs file into your project:
// In your Avalonia App.axaml.cs or startup
mediator.SetDispatcher(new AvaloniaDispatcher());
Note: The Avalonia dispatcher is community-tested. Please report any issues on GitHub.
Error Handling
// Set error policy
mediator.ErrorPolicy = ErrorPolicy.LogAndContinue;
// Subscribe to errors
mediator.HandlerError += (sender, args) =>
{
logger.LogError(args.Exception,
"Handler error for {MessageType}",
args.MessageType.Name);
};
Comparisons
ModernMediator vs MediatR
| Feature | ModernMediator | MediatR |
|---|---|---|
| Request/Response | ✅ Yes | ✅ Yes |
| Notifications (Pub/Sub) | ✅ Yes | ✅ Yes |
| Pub/Sub with Callbacks | ✅ Yes | ❌ No |
| Pipeline Behaviors | ✅ Yes | ✅ Yes |
| Streaming | ✅ Yes | ✅ Yes |
| Assembly Scanning | ✅ Yes | ✅ Yes |
| Exception Handlers | ✅ Yes | ❌ No |
| Source Generators | ✅ Yes | ❌ No |
| Native AOT | ✅ Yes | ❌ No |
| Weak References | ✅ Yes | ❌ No |
| Runtime Subscribe/Unsubscribe | ✅ Yes | ❌ No |
| UI Thread Dispatch | ✅ Built-in | ❌ Manual |
| Covariance | ✅ Yes | ❌ No |
| Predicate Filters | ✅ Yes | ❌ No |
| String Key Routing | ✅ Yes | ❌ No |
| Parallel Notifications | ✅ Default | ❌ Sequential |
| MIT License | ✅ Yes | ❌ Commercial* |
*MediatR moved to commercial licensing in July 2025
ModernMediator vs Prism EventAggregator
For desktop developers using Prism, ModernMediator can replace EventAggregator while adding MediatR-style request/response:
| Feature | Prism EventAggregator | ModernMediator |
|---|---|---|
| Pub/Sub | ✅ PubSubEvent<T> |
✅ Publish<T> / Subscribe<T> |
| Pub/Sub w/ Callbacks | ❌ Manual (callback in payload) | ✅ Publish<TMsg, TResp> |
| Weak References | ✅ keepSubscriberReferenceAlive: false |
✅ weak: true (default) |
| Strong References | ✅ keepSubscriberReferenceAlive: true |
✅ weak: false |
| Filter Subscriptions | ✅ .Subscribe(handler, filter) |
✅ filter: predicate |
| UI Thread | ✅ ThreadOption.UIThread |
✅ SubscribeOnMainThread |
| Background Thread | ✅ ThreadOption.BackgroundThread |
✅ PublishAsync |
| Unsubscribe | ✅ SubscriptionToken |
✅ IDisposable |
| Request/Response | ❌ No | ✅ Send<TResponse> |
| Pipeline Behaviors | ❌ No | ✅ Yes |
| Exception Handlers | ❌ No | ✅ Yes |
| Streaming | ❌ No | ✅ CreateStream |
| Source Generators | ❌ No | ✅ Yes |
| Native AOT | ❌ No | ✅ Yes |
Bottom line: If you're using Prism EventAggregator for pub/sub AND MediatR for CQRS, ModernMediator replaces both with one library.
Use Cases
Plugin Systems
ModernMediator excels at plugin architectures where plugins load/unload at runtime:
- Weak references prevent memory leaks when plugins unload
- Runtime subscribe/unsubscribe for dynamic registration
- String key routing for topic-based communication
Desktop Applications (WPF, WinForms, MAUI)
- Built-in UI thread dispatchers
- Memory-efficient with weak references
- Easy decoupling of components
- Replaces both EventAggregator and MediatR
ASP.NET Core
- Full DI integration with proper scoped service support
- Request/response for CQRS patterns
- Pipeline behaviors for cross-cutting concerns
- Handlers can inject scoped services like
DbContext
Large Dataset Processing
- Streaming with
IAsyncEnumerablefor memory efficiency - Cancellation support for long-running operations
- Backpressure-friendly enumeration
Serverless & Native AOT
- Source generators eliminate reflection overhead
CachingMode.Lazyfor fast cold start times- Full Native AOT compatibility
- Compile-time handler discovery
Known Limitations
- In-process only - No distributed messaging. For microservices, combine with MassTransit or Wolverine for transport.
- One handler per request - Request/Response expects exactly one handler per request type.
- No generic request handlers - Each closed generic type needs its own handler.
- Exception handlers for Request/Response only - Pub/Sub notifications use
ErrorPolicyinstead. - Pipeline behaviors don't wrap streaming - Behaviors wrap
Send(), notCreateStream(). - Weak references + lambdas - Closures capture
this, which may prevent GC. Use method references orweak: false. - Behavior order = registration order - First registered behavior executes first (outermost).
- Native AOT requires source generator - Use
AddModernMediatorGenerated()instead of assembly scanning. - Open generics require explicit registration - Assembly scanning skips open generic behaviors; use
AddOpenBehavior(). - Scoped IMediator for DI - Pub/Sub subscriptions via DI are per-scope; use
Mediator.Instancefor shared subscriptions.
License
MIT License - see LICENSE file.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net8.0-windows7.0 is compatible. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
-
net8.0-windows7.0
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.2.2-alpha | 46 | 1/8/2026 |
| 0.2.1-alpha | 62 | 12/28/2025 |
| 0.2.0-alpha | 128 | 12/24/2025 |
| 0.1.0-alpha | 128 | 12/23/2025 |
v0.2.1-alpha:
- Fix: Handlers can now resolve scoped dependencies (e.g., DbContext)
- Fix: Assembly scanning skips open generic types (use AddOpenBehavior instead)
- Breaking: IMediator registered as Scoped (was Singleton). For shared Pub/Sub, use Mediator.Instance.
- Added: AddOpenBehavior() and AddOpenExceptionHandler() for open generic registration
See CHANGELOG.md for full details.