MediatR
MediatR is a popular open-source .NET library by Jimmy Bogard that provides an in-process implementation of the Mediator design pattern. In plain terms, it acts as a middleman — instead of one object talking directly to another, both talk to MediatR, and MediatR routes the message to whoever should handle it.
It is the most common way to implement CQRS in a .NET application.
How MediatR connects to CQRS
CQRS defines the concept — separate your reads from your writes. MediatR provides the mechanism to do it cleanly in .NET:
| CQRS concept | MediatR equivalent |
|---|---|
| Command | IRequest (returns Unit or a result) |
| Query | IRequest<TResponse> |
| Command Handler | IRequestHandler<TCommand, TResponse> |
| Query Handler | IRequestHandler<TQuery, TResponse> |
| Command/Query Bus | IMediator.Send() |
| Notification (event) | INotification + INotificationHandler |
Core flow
Controller
│
└──► mediator.Send(new GetUserQuery(id))
│
└──► MediatR resolves ──► GetUserQueryHandler.Handle()
│
└──► returns UserDto
The controller never knows GetUserQueryHandler exists. MediatR resolves the correct handler from the DI container at runtime.
Setting it up
Install via NuGet:
dotnet add package MediatR
Register in Program.cs:
builder.Services.AddMediatR(cfg =>
cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
A minimal CQRS example
Query + Handler:
// Query (the request)
public record GetProductQuery(int Id) : IRequest<ProductDto>;
// Handler (the responder)
public class GetProductHandler : IRequestHandler<GetProductQuery, ProductDto>
{
public async Task<ProductDto> Handle(GetProductQuery request, CancellationToken ct)
{
// fetch from DB and return DTO
}
}
Command + Handler:
public record CreateProductCommand(string Name, decimal Price) : IRequest<int>;
public class CreateProductHandler : IRequestHandler<CreateProductCommand, int>
{
public async Task<int> Handle(CreateProductCommand request, CancellationToken ct)
{
// persist to DB, return new ID
}
}
Dispatching from a controller:
[ApiController]
public class ProductsController : ControllerBase
{
private readonly IMediator _mediator;
public ProductsController(IMediator mediator) => _mediator = mediator;
[HttpGet("{id}")]
public Task<ProductDto> Get(int id) => _mediator.Send(new GetProductQuery(id));
[HttpPost]
public Task<int> Create(CreateProductCommand cmd) => _mediator.Send(cmd);
}
Pipeline Behaviours (middleware for handlers)
One of MediatR's most powerful features is IPipelineBehavior<TRequest, TResponse> — it lets you wrap every handler with cross-cutting logic without touching the handler itself.
Common uses:
| Behaviour | What it does |
|---|---|
| Validation | Run FluentValidation before the handler executes |
| Logging | Log request/response automatically |
| Caching | Return a cached result for queries |
| Transaction | Wrap commands in a DB transaction |
public class LoggingBehaviour<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(TResponse).Name}");
return response;
}
}
Register it:
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehaviour<,>));
When to use MediatR
MediatR shines when:
- You are building on CQRS and want clean handler separation
- You have many cross-cutting concerns (validation, logging, caching) to apply uniformly
- You want controllers to be thin — no business logic, just dispatch
Avoid it for simple CRUD apps where the extra layer adds noise without benefit.
Related notes
- CQRS
- Design pattern
- Dependency Injection
- DotNet
- Transactions