A lightweight, high-performance mediator pattern implementation for .NET 9+. Supports commands, queries, and domain events with built-in event deferral, validation, pipeline behaviors, and optional transaction scope support. Zero external dependencies.
$ dotnet add package AsyncMediatorA lightweight, high-performance mediator for .NET 9/10 with compile-time handler discovery.
CommandHandler<T> base class with Validate() + DoHandle() flowdotnet add package AsyncMediator
dotnet add package AsyncMediator.SourceGenerator
Recommended: Always install both packages. The source generator eliminates manual handler registration and catches missing handlers at compile time.
builder.Services.AddAsyncMediator();
That's it. All handlers are discovered automatically.
public record CreateOrderCommand(Guid CustomerId, List<OrderItem> Items) : ICommand;
public class CreateOrderHandler(IMediator mediator, IOrderRepository repo)
: CommandHandler<CreateOrderCommand>(mediator)
{
protected override Task Validate(ValidationContext ctx, CancellationToken ct)
{
if (Command.Items.Count == 0)
ctx.AddError(nameof(Command.Items), "Order must have items");
return Task.CompletedTask;
}
protected override async Task<ICommandWorkflowResult> DoHandle(ValidationContext ctx, CancellationToken ct)
{
var order = await repo.Create(Command.CustomerId, Command.Items, ct);
Mediator.DeferEvent(new OrderCreatedEvent(order.Id));
return CommandWorkflowResult.Ok();
}
}
var result = await mediator.Send(new CreateOrderCommand(customerId, items), ct);
if (result.Success)
// Order created, events fired
Commands change state. Handlers return ICommandWorkflowResult with validation support.
public record CreateOrderCommand(Guid CustomerId) : ICommand;
Queries read data without side effects.
// With criteria
public class OrderQuery(IOrderRepository repo) : IQuery<OrderSearchCriteria, List<Order>>
{
public Task<List<Order>> Query(OrderSearchCriteria c, CancellationToken ct) =>
repo.Search(c.CustomerId, ct);
}
var orders = await mediator.Query<OrderSearchCriteria, List<Order>>(criteria, ct);
// Without criteria
public class AllOrdersQuery(IOrderRepository repo) : ILookupQuery<List<Order>>
{
public Task<List<Order>> Query(CancellationToken ct) => repo.GetAll(ct);
}
var orders = await mediator.LoadList<List<Order>>(ct);
Events fire after successful command execution. They're automatically skipped if validation fails or an exception occurs.
public record OrderCreatedEvent(Guid OrderId) : IDomainEvent;
// Defer in handler
Mediator.DeferEvent(new OrderCreatedEvent(order.Id));
// Handle elsewhere
public class SendEmailHandler : IEventHandler<OrderCreatedEvent>
{
public Task Handle(OrderCreatedEvent e, CancellationToken ct) =>
emailService.SendConfirmation(e.OrderId, ct);
}
Add cross-cutting concerns without modifying handlers:
builder.Services.AddAsyncMediator(cfg => cfg
.AddOpenGenericBehavior(typeof(LoggingBehavior<,>))
.AddOpenGenericBehavior(typeof(ValidationBehavior<,>)));
public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct)
{
Console.WriteLine($"Handling {typeof(TRequest).Name}");
var response = await next();
Console.WriteLine($"Handled {typeof(TRequest).Name}");
return response;
}
}
| Operation | Latency | Memory |
|---|---|---|
| Send command | ~163 ns | ~488 B |
| Query | ~105 ns | ~248 B |
| Defer event | ~575 ns | 0 B |
Pipeline behaviors add zero overhead when not registered.
Good fit:
Not a fit:
| Resource | Description |
|---|---|
| Working Demo | Run it locally and see the flow |
| Architecture | Design decisions and internals |
| Pipeline Behaviors | Logging, validation, unit of work examples |
| Migration Guide | Upgrading from v2.x |
var result = CommandWorkflowResult.Ok();
result.SetResult(order);
return result;
// Caller
var order = result.Result<Order>();
public class TransferHandler(IMediator mediator) : CommandHandler<TransferCommand>(mediator)
{
protected override bool UseTransactionScope => true;
}
[ExcludeFromMediator]
public class DraftHandler : CommandHandler<MyCommand> { ... }
services.AddScoped<IMediator>(sp => new Mediator(
type => sp.GetServices(type),
type => sp.GetRequiredService(type)));
services.AddTransient<ICommandHandler<CreateOrderCommand>, CreateOrderHandler>();
Found a bug or have a feature request? Open an issue.