ModernMediator 0.2.2-alpha

This is a prerelease version of ModernMediator.
dotnet add package ModernMediator --version 0.2.2-alpha
                    
NuGet\Install-Package ModernMediator -Version 0.2.2-alpha
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ModernMediator" Version="0.2.2-alpha" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ModernMediator" Version="0.2.2-alpha" />
                    
Directory.Packages.props
<PackageReference Include="ModernMediator" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add ModernMediator --version 0.2.2-alpha
                    
#r "nuget: ModernMediator, 0.2.2-alpha"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package ModernMediator@0.2.2-alpha
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=ModernMediator&version=0.2.2-alpha&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=ModernMediator&version=0.2.2-alpha&prerelease
                    
Install as a Cake Tool

ModernMediator

NuGet

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

📖 Interactive Tutorial

Features

Core Patterns

  • Request/Response - Send requests and receive typed responses
  • Streaming - IAsyncEnumerable support 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 - SubscribeAsync with proper Task.WhenAll aggregation
  • 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.0 and net8.0-windows
  • Interface-first - IMediator for testability and mocking

Installation

dotnet add package ModernMediator

Quick Start

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 IAsyncEnumerable for memory efficiency
  • Cancellation support for long-running operations
  • Backpressure-friendly enumeration

Serverless & Native AOT

  • Source generators eliminate reflection overhead
  • CachingMode.Lazy for 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 ErrorPolicy instead.
  • Pipeline behaviors don't wrap streaming - Behaviors wrap Send(), not CreateStream().
  • Weak references + lambdas - Closures capture this, which may prevent GC. Use method references or weak: 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.Instance for shared subscriptions.

License

MIT License - see LICENSE file.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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.