Mediator (Source Generated)
Overview
The Mediator pattern provides a standalone, source-generated mediator for decoupling communication between components. This implementation handles:
- Commands (request → response)
- Notifications (fan-out to multiple handlers)
- Streams (request → async stream of items)
- Pipelines (pre/around/post hooks)
Critical Features
- Zero runtime dependency on PatternKit - the generated code is fully independent
- AOT-friendly - no reflection-based dispatch
- Async-first - all operations use
ValueTaskandIAsyncEnumerable<T> - Dual-mode API - supports both class-based and fluent registration
Basic Usage
1. Mark your assembly for code generation
using PatternKit.Generators.Messaging;
[assembly: GenerateDispatcher(
Namespace = "MyApp.Messaging",
Name = "AppDispatcher",
IncludeStreaming = true)]
2. Define your messages
// Commands
public record CreateUser(string Username, string Email);
public record UserCreated(int UserId, string Username);
// Notifications
public record UserRegistered(int UserId, string Username, string Email);
// Stream requests
public record SearchQuery(string Term, int MaxResults);
public record SearchResult(string Title, string Url);
3. Register handlers using the fluent API
var dispatcher = AppDispatcher.Create()
// Command handler
.Command<CreateUser, UserCreated>((req, ct) =>
new ValueTask<UserCreated>(new UserCreated(1, req.Username)))
// Notification handlers (multiple allowed)
.Notification<UserRegistered>((n, ct) =>
{
Console.WriteLine($"User {n.Username} registered");
return ValueTask.CompletedTask;
})
// Stream handler
.Stream<SearchQuery, SearchResult>(SearchAsync)
.Build();
4. Use the dispatcher
// Send a command
var result = await dispatcher.Send<CreateUser, UserCreated>(
new CreateUser("alice", "alice@example.com"),
cancellationToken);
// Publish a notification
await dispatcher.Publish(
new UserRegistered(1, "alice", "alice@example.com"),
cancellationToken);
// Stream results
await foreach (var result in dispatcher.Stream<SearchQuery, SearchResult>(
new SearchQuery("pattern", 10),
cancellationToken))
{
Console.WriteLine(result.Title);
}
Pipelines
Add cross-cutting concerns with pipelines:
var dispatcher = AppDispatcher.Create()
// Pre-execution hook
.Pre<CreateUser>((req, ct) =>
{
Console.WriteLine($"Validating: {req.Username}");
return ValueTask.CompletedTask;
})
// Command handler
.Command<CreateUser, UserCreated>((req, ct) =>
new ValueTask<UserCreated>(new UserCreated(1, req.Username)))
// Post-execution hook
.Post<CreateUser, UserCreated>((req, res, ct) =>
{
Console.WriteLine($"Created user: {res.UserId}");
return ValueTask.CompletedTask;
})
.Build();
Pipeline execution order:
- Pre hooks (in registration order)
- Command handler
- Post hooks (in registration order)
Generated Code Structure
The generator creates three files:
AppDispatcher.g.cs- Main dispatcher withSend,Publish, andStreammethodsAppDispatcher.Builder.g.cs- Fluent builder for registrationAppDispatcher.Contracts.g.cs- Handler interfaces and pipeline delegates
All generated code is independent of PatternKit and contains only BCL dependencies.
Configuration Options
[assembly: GenerateDispatcher(
Namespace = "MyApp.Messaging", // Target namespace
Name = "AppDispatcher", // Class name
IncludeStreaming = true, // Enable streaming support
IncludeObjectOverloads = false, // Add object-based overloads
Visibility = GeneratedVisibility.Public // Public or Internal
)]
Behavior
Commands
- Exactly one handler per request type (runtime exception if missing)
- Async-first with
ValueTask<TResponse> - Pipeline support (Pre/Post)
Notifications
- Zero or more handlers per notification type
- Zero handlers = no-op (does not throw)
- Sequential execution (deterministic order)
Streams
- Exactly one handler per request type (runtime exception if missing)
- Lazy enumeration (pull-based)
- Cancellation flows through enumeration
Examples
See PatternKit.Examples/Messaging/DispatcherExample.cs for complete working examples.
What is the Mediator Pattern?
The Mediator pattern is a behavioral design pattern that reduces coupling between components by having them communicate through a central mediator object instead of directly with each other. This pattern:
- Centralizes communication logic: All message routing happens in one place
- Reduces dependencies: Components don't need to know about each other
- Simplifies maintenance: Changes to message flow are isolated to the mediator
- Enables cross-cutting concerns: Behaviors like logging, validation, and metrics can be applied uniformly
Source-Generated vs Runtime Mediator
PatternKit offers two Mediator implementations:
- Runtime Mediator (docs) - A pre-built, allocation-light mediator using
PatternKit.Behavioral.Mediator - Source-Generated Mediator (this document) - A compile-time generated mediator with zero PatternKit runtime dependency
Choose the source-generated variant when:
- You need zero runtime dependencies (for libraries/NuGet packages)
- You want AOT compatibility without reflection
- You prefer compile-time verification of message flows
- You need maximum performance with no abstraction overhead
Choose the runtime mediator when:
- You want immediate use without code generation setup
- You need dynamic handler registration at runtime
- You're building an application (not a library)
Production Example with Dependency Injection
For a complete production-ready example, see the comprehensive demo at:
src/PatternKit.Examples/MediatorComprehensiveDemo/ComprehensiveDemo.cs
This 780+ line example demonstrates:
DI Integration with IServiceCollection
// Setup DI container
var services = new ServiceCollection();
// Register infrastructure
services.AddSingleton<ILogger, InMemoryLogger>();
services.AddSingleton<ICustomerRepository, InMemoryCustomerRepository>();
// Register mediator and handlers
services.AddSourceGeneratedMediator();
services.AddHandlersFromAssembly(Assembly.GetExecutingAssembly());
// Register pipeline behaviors
services.AddTransient(typeof(LoggingBehavior<,>));
services.AddTransient(typeof(ValidationBehavior<,>));
services.AddTransient(typeof(PerformanceBehavior<,>));
var provider = services.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<ProductionDispatcher>();
Extension Methods Provided
The comprehensive demo includes these MediatR-style extension methods:
AddSourceGeneratedMediator()
Registers the dispatcher and wires up all handlers with DI container:
services.AddSourceGeneratedMediator();
AddHandlersFromAssembly()
Automatically discovers and registers all handlers from an assembly:
services.AddHandlersFromAssembly(Assembly.GetExecutingAssembly());
This scans for and registers:
ICommandHandler<TRequest, TResponse>implementationsINotificationHandler<TNotification>implementationsIStreamHandler<TRequest, TItem>implementations
AddBehavior<TRequest, TResponse, TBehavior>()
Registers specific pipeline behaviors:
services.AddBehavior<CreateCustomerCommand, Customer, LoggingBehavior<CreateCustomerCommand, Customer>>();
Real-World Domain Model
The comprehensive demo implements a complete e-commerce domain:
Commands (Write Operations):
CreateCustomerCommand- Create new customersPlaceOrderCommand- Place orders with line itemsProcessPaymentCommand- Process payments
Queries (Read Operations):
GetCustomerQuery- Retrieve customer by IDGetOrdersByCustomerQuery- Get all orders for a customer
Events/Notifications:
CustomerCreatedEvent- Fan out to welcome email, audit log, stats updateOrderPlacedEvent- Notify inventory, send confirmationPaymentProcessedEvent- Record audit trail
Streams:
SearchProductsQuery- Async enumerable product search results
Pipeline Behaviors Demonstrated
LoggingBehavior: Logs all commands before and after execution
public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : ICommand<TResponse>
{
public async ValueTask<TResponse> Handle(
TRequest request,
CancellationToken ct,
RequestHandlerDelegate<TResponse> next)
{
_logger.Log($"[Logging] Handling {typeof(TRequest).Name}");
var response = await next();
_logger.Log($"[Logging] Handled {typeof(TRequest).Name}");
return response;
}
}
ValidationBehavior: Validates commands before execution
PerformanceBehavior: Tracks execution time metrics
TransactionBehavior: Wraps commands in transactions (skips queries)
Complete Usage Flow
The demo shows 6 complete end-to-end scenarios:
// 1. Create Customer (Command + Event)
var customer = await dispatcher.Send<CreateCustomerCommand, Customer>(
new CreateCustomerCommand("John Doe", "john@example.com", 5000m),
default);
await dispatcher.Publish(
new CustomerCreatedEvent(customer.Id, customer.Name, customer.Email),
default);
// 2. Query Customer
var queriedCustomer = await dispatcher.Send<GetCustomerQuery, Customer?>(
new GetCustomerQuery(customer.Id),
default);
// 3. Place Order (Command + Event)
var order = await dispatcher.Send<PlaceOrderCommand, Order>(
new PlaceOrderCommand(customer.Id, items),
default);
// 4. Process Payment
var success = await dispatcher.Send<ProcessPaymentCommand, bool>(
new ProcessPaymentCommand(order.Id, order.Total),
default);
// 5. Stream Search Results
await foreach (var product in dispatcher.Stream<SearchProductsQuery, ProductSearchResult>(
new SearchProductsQuery("laptop", 10),
default))
{
Console.WriteLine($"Product: {product.Name} - ${product.Price}");
}
// 6. Query Orders
var orders = await dispatcher.Send<GetOrdersByCustomerQuery, List<Order>>(
new GetOrdersByCustomerQuery(customer.Id),
default);
Repository Pattern Integration
The demo shows how to integrate with repositories:
public class GetCustomerHandler : ICommandHandler<GetCustomerQuery, Customer?>
{
private readonly ICustomerRepository _repository;
public GetCustomerHandler(ICustomerRepository repository)
{
_repository = repository;
}
public ValueTask<Customer?> Handle(GetCustomerQuery request, CancellationToken ct)
{
var customer = _repository.GetById(request.CustomerId);
return new ValueTask<Customer?>(customer);
}
}
CQRS Pattern
The demo demonstrates Command Query Responsibility Segregation:
- Commands modify state:
CreateCustomerCommand,PlaceOrderCommand - Queries read state:
GetCustomerQuery,GetOrdersByCustomerQuery - Both use the same mediator but have different semantics
Key Takeaways from the Demo
- Full DI Integration: All components are resolved from the DI container
- Handler Discovery: Automatic registration via assembly scanning
- Pipeline Composition: Multiple behaviors compose around handlers
- Event Fan-Out: Single event triggers multiple handlers
- Repository Pattern: Clean separation between domain and infrastructure
- Zero PatternKit Dependency: Generated code only uses BCL types
Best Practices
- Use value types for messages when possible (records with value semantics)
- Keep handlers focused - one responsibility per handler
- Use pipelines for cross-cutting concerns (logging, validation, metrics)
- Test handlers independently before composing into dispatcher
- Use cancellation tokens consistently throughout your message flow
- Register handlers via DI - avoid manual instantiation for better testability
- Use CQRS - separate commands (write) from queries (read) for clarity
Performance
- Zero allocations in dispatch path for value types
- No reflection - all dispatch is compile-time generated
- Deterministic - no runtime scanning or dynamic discovery
- Minimal overhead - direct delegate invocation
Related Patterns
- Runtime Mediator - Pre-built mediator with PatternKit runtime dependency
- Observer - For simpler pub/sub scenarios without request/response
- Command - For encapsulating requests as objects
Future Enhancements
Future versions may include:
- Object-based overloads (for dynamic scenarios)
- Parallel notification execution
- Around hooks for full pipeline wrapping
- OnError handlers for exception handling
- Module registration for organizing handlers