As ASP.NET Core applications grow, controllers often become responsible for too many tasks—handling HTTP requests, validating input, executing business logic, and interacting with the data layer. This tight coupling makes applications harder to test, maintain, and extend.
MediatR is a popular library that implements the Mediator pattern, helping developers decouple application components by routing requests through dedicated handlers. It is widely used in applications following Clean Architecture and CQRS principles.
However, MediatR isn't a solution for every problem. While it improves code organization, misusing it can introduce unnecessary complexity. In this article, we'll explore practical MediatR patterns, common mistakes, and when it makes sense to use it.
What Is MediatR?
MediatR is an in-process messaging library that enables communication between different parts of an application without creating direct dependencies.
Instead of a controller calling a service directly:
Controller
│
ProductService
│
Repository
The request flows through MediatR:
Controller
│
MediatR
│
Request Handler
│
Repository
The controller only knows about MediatR, while the business logic resides inside dedicated request handlers.
Request and Response Pattern
The most common MediatR pattern is the Request/Response model.
First, define a request:
using MediatR;
public record GetProductQuery(int Id) : IRequest<Product>;
Next, implement the handler:
public class GetProductHandler
: IRequestHandler<GetProductQuery, Product>
{
private readonly IProductRepository _repository;
public GetProductHandler(IProductRepository repository)
{
_repository = repository;
}
public async Task<Product> Handle(
GetProductQuery request,
CancellationToken cancellationToken)
{
return await _repository.GetByIdAsync(request.Id);
}
}
Finally, send the request from a controller:
[HttpGet("{id}")]
public async Task<IActionResult> Get(
int id,
IMediator mediator)
{
var product = await mediator.Send(new GetProductQuery(id));
return product is null
? NotFound()
: Ok(product);
}
This approach keeps controllers thin and delegates business logic to handlers.
Using Commands for Data Modification
Queries retrieve data, while commands modify it.
Example command:
public record CreateProductCommand(
string Name,
decimal Price) : IRequest<int>;
The corresponding handler performs validation, business logic, and persistence before returning the new product ID.
Separating commands and queries improves readability and aligns well with the Command Query Responsibility Segregation (CQRS) pattern.
Notifications for Multiple Actions
Sometimes a single event should trigger multiple independent actions.
For example:
Send an email
Update inventory
Write an audit log
Publish an integration event
Instead of placing all logic inside one handler, use notifications.
public record ProductCreatedNotification(int ProductId)
: INotification;
Each notification handler executes independently, making the application easier to extend without modifying existing code.
Pipeline Behaviors
One of MediatR's most powerful features is Pipeline Behaviors.
They allow cross-cutting concerns to execute before or after request handlers.
Common uses include:
Validation
Logging
Performance monitoring
Authorization
Exception handling
Instead of duplicating logic across handlers, pipeline behaviors centralize these concerns, resulting in cleaner and more maintainable code.
When MediatR Works Best
MediatR provides the greatest value in applications that have:
Join the conversation! Your thoughts help the community grow.