A lightweight, high-performance mediator library for .NET β implementing the Mediator pattern with CQRS support, pipeline behaviors, and notification publishing.
- CQRS ready β Separate
ICommand<T>,IQuery<T>, andIRequest<T>contracts - Clean void commands β
Task Handle(), noUnitboilerplate needed - Pipeline behaviors β Middleware-style
IPipelineBehavior<TRequest, TResponse>for cross-cutting concerns - Notification publishing β Fan-out to multiple
INotificationHandler<T>with error resilience - Zero dependencies on contracts β
Light.Mediator.Contractshas no external dependencies - High performance β Cached wrapper resolution via
ConcurrentDictionary, zero-alloc fast-path - Safe DI registration β Assembly scanning with duplicate protection and safe type loading
- Targets
netstandard2.1
dotnet add package Lightsoft.MediatorFor projects that only define requests/commands/queries (e.g., shared contracts):
dotnet add package Lightsoft.Mediator.Contractsusing Light.Mediator;
// Query with response
public record GetOrderById(int Id) : IQuery<OrderDto>;
public class GetOrderByIdHandler : IQueryHandler<GetOrderById, OrderDto>
{
public Task<OrderDto> Handle(GetOrderById request, CancellationToken ct)
=> Task.FromResult(new OrderDto(request.Id, "Sample Order"));
}
// Void command β clean, no Unit!
public record DeleteOrder(int Id) : ICommand;
public class DeleteOrderHandler : ICommandHandler<DeleteOrder>
{
public Task Handle(DeleteOrder request, CancellationToken ct)
=> Task.CompletedTask;
}using Light.Mediator;
using System.Reflection;
builder.Services.AddMediatorFromAssemblies(Assembly.GetExecutingAssembly());public class OrdersController : ControllerBase
{
private readonly ISender _sender;
public OrdersController(ISender sender) => _sender = sender;
[HttpGet("{id}")]
public async Task<OrderDto> Get(int id)
=> await _sender.Send(new GetOrderById(id));
[HttpDelete("{id}")]
public async Task Delete(int id)
=> await _sender.Send(new DeleteOrder(id));
}| Interface | Purpose | Returns |
|---|---|---|
IRequest<TResponse> |
Base request with typed response | TResponse |
IRequest |
Void request (returns Unit) |
Unit |
ICommand<TResponse> |
Command with response | TResponse |
ICommand |
Void command | Unit |
IQuery<TResponse> |
Query with response | TResponse |
INotification |
Notification (fan-out) | β |
Unit is a readonly struct used internally for void operations. With the non-generic handler interfaces, you rarely need to interact with Unit directly:
// β
Void handler β clean, no Unit!
public class DeleteOrderHandler : ICommandHandler<DeleteOrder>
{
public Task Handle(DeleteOrder request, CancellationToken ct)
=> Task.CompletedTask;
}// Generic β with response
public class MyHandler : IRequestHandler<MyRequest, MyResponse> { ... }
public class MyHandler : ICommandHandler<MyCommand, int> { ... }
public class MyHandler : IQueryHandler<MyQuery, MyDto> { ... }
// Non-generic β void (Task Handle(), no Unit!)
public class MyHandler : IRequestHandler<MyRequest> { ... }
public class MyHandler : ICommandHandler<MyCommand> { ... }Non-generic handlers use Task Handle(...) independently (MediatR-style) β the library automatically bridges to Task<Unit> internally via VoidRequestHandlerAdapter.
Multiple handlers per notification type β all execute sequentially:
public class SendEmailHandler : INotificationHandler<OrderCreated>
{
public Task Handle(OrderCreated notification, CancellationToken ct)
=> Task.CompletedTask;
}await mediator.Publish(new OrderCreated(orderId));Add cross-cutting concerns (logging, validation, transactions) as middleware:
// Open generic β applies to 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)
{
Console.WriteLine($"Handling {typeof(TRequest).Name}");
var result = await next(ct);
Console.WriteLine($"Handled {typeof(TRequest).Name}");
return result;
}
}
// Closed β for specific void command (use shorthand IPipelineBehavior<TRequest>)
public class DeleteAuditBehavior : IPipelineBehavior<DeleteOrder>
{
public Task<Unit> Handle(
DeleteOrder request, RequestHandlerDelegate<Unit> next, CancellationToken ct)
{
Console.WriteLine($"Auditing delete: {request.Id}");
return next(ct);
}
}Register behaviors β they execute in registration order (first registered = outermost):
builder.Services.AddBehaviors(
typeof(LoggingBehavior<,>), // outermost
typeof(ValidationBehavior<,>), // innermost
typeof(DeleteAuditBehavior) // closed β auto-resolves to IPipelineBehavior<DeleteOrder, Unit>
);Pipeline execution order:
LoggingBehavior:Before β ValidationBehavior:Before β Handler β ValidationBehavior:After β LoggingBehavior:After
builder.Services.AddMediatorFromAssemblies(
Assembly.GetExecutingAssembly(),
typeof(SomeHandler).Assembly
);What gets registered:
MediatorasIMediator,ISender,IPublisherβ viaTryAddTransientIRequestHandler<,>implementations βTryAddTransient(single handler per request)IRequestHandler<>(void) β registers handler +VoidRequestHandlerAdapterbridgeINotificationHandler<>implementations βAddTransientwith duplicate protection
builder.Services.AddBehaviors(
typeof(LoggingBehavior<,>), // open generic
typeof(DeleteAuditBehavior) // closed β auto-detects every IPipelineBehavior<,> interface it implements
);Re-registering the exact same behavior type (same service interface + implementation pair) is a no-op, so calling AddBehaviors more than once for the same type β e.g. from two separate composition modules β won't double up the pipeline.
- Non-cancellation exceptions are collected and thrown as
AggregateException OperationCanceledException/TaskCanceledExceptionpropagate immediately
- Handler exceptions propagate through the pipeline normally
- Behaviors can catch and handle exceptions via standard try/catch around
next()
Mediator.Contracts/ β Pure contracts, zero dependencies
βββ ICommand.cs ICommand<T>, ICommand
βββ INotification.cs INotification
βββ IQuery.cs IQuery<T>
βββ IRequest.cs IRequest<T>, IRequest
βββ Unit.cs Unit struct
Mediator/ β Core + DI, depends on Contracts
βββ Adapters/ Internal DI-constructed bridges
β βββ VoidRequestHandlerAdapter.cs TaskβTask<Unit> bridge
βββ Wrappers/ Internal handler/behavior wrappers
β βββ BehaviorWrapper.cs
β βββ HandlerWrapper.cs
β βββ NotificationHandlerWrapper.cs
βββ ICommandHandler.cs ICommandHandler<T,R>, ICommandHandler<T>
βββ IMediator.cs IMediator : ISender, IPublisher
βββ INotificationHandler.cs INotificationHandler<T>
βββ IPipelineBehavior.cs IPipelineBehavior<T,R>, IPipelineBehavior<T>
βββ IPublisher.cs IPublisher
βββ IQueryHandler.cs IQueryHandler<T,R>
βββ IRequestHandler.cs IRequestHandler<T,R>, IRequestHandler<T>
βββ ISender.cs ISender
βββ Mediator.cs Core mediator implementation
βββ RequestHandlerDelegate.cs RequestHandlerDelegate<T> delegate
βββ ServiceCollectionExtensions.cs DI registration extensions
- Contracts β Reference only this in shared/domain projects. Zero dependencies, minimal surface.
- Mediator β Reference this in your composition root / startup project. Brings in
Microsoft.Extensions.DependencyInjection.
MIT