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:
Complex business workflows
Multiple use cases
Clean Architecture
CQRS implementation
Large development teams
Extensive testing requirements
For these applications, separating requests into dedicated handlers improves maintainability and reduces coupling.
When MediatR May Be Unnecessary
Not every application benefits from MediatR.
For a simple CRUD API with only a few endpoints, adding requests, handlers, and pipeline behaviors may increase complexity without delivering significant value.
A straightforward service layer is often sufficient for:
Small internal tools
Prototype applications
Basic CRUD services
Lightweight APIs
Choose MediatR when it solves an architectural problem—not simply because it's popular.
Common Mistakes
Creating a Handler for Every Tiny Operation
Some developers create handlers for trivial methods that simply forward calls to a repository.
For example:
Controller
│
Handler
│
Service
│
Repository
If the handler contains no business logic, MediatR adds an extra layer without improving maintainability.
Putting Business Logic in Controllers
Even when using MediatR, controllers should remain lightweight.
Avoid:
Validation logic
Business rules
Database access
Controllers should receive requests, send them through MediatR, and return responses.
Overusing Notifications
Notifications are excellent for independent actions, but they should not be used when execution order or transactional consistency is critical.
If one operation depends on another, a command handler is usually a better choice.
Ignoring Pipeline Behaviors
Many teams adopt MediatR but continue duplicating validation and logging inside handlers.
Pipeline behaviors provide a cleaner and more reusable solution for cross-cutting concerns.
Best Practices
Keep handlers focused on a single responsibility.
Use commands for writes and queries for reads.
Keep controllers thin.
Use pipeline behaviors for validation, logging, and exception handling.
Inject only the dependencies required by each handler.
Avoid creating handlers that simply wrap repository methods.
Group requests by feature to improve project organization.
Unit test handlers independently from controllers.
Common MediatR Patterns
| Pattern | Best Use Case |
|---|---|
| Request/Response | Retrieve or update data |
| Command | Create, update, or delete operations |
| Query | Read-only operations |
| Notification | Multiple independent actions |
| Pipeline Behavior | Logging, validation, authorization, caching |
Choosing the appropriate pattern keeps applications easier to maintain as they grow.
Conclusion
MediatR is a powerful library for building maintainable ASP.NET Core applications by promoting loose coupling and clear separation of responsibilities. Its request handlers, notifications, and pipeline behaviors help organize business logic, simplify testing, and support architectural patterns such as Clean Architecture and CQRS.
However, MediatR is not a requirement for every project. Small CRUD applications often benefit more from a straightforward service layer than from additional abstraction. The key is to apply MediatR where it genuinely improves maintainability rather than introducing unnecessary complexity.
By keeping handlers focused, leveraging pipeline behaviors for cross-cutting concerns, and avoiding common anti-patterns, development teams can use MediatR to build scalable, testable, and well-structured .NET applications that remain easy to evolve over time.
Jasen FiciPosted Jul 30, 2026, 12:05 PM
We highlighted this in DotNetNews here: https://dotnetnews.co/archive/the-net-news-daily-issue-508/