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:

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:

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:

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:

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:

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

Common MediatR Patterns

PatternBest Use Case
Request/ResponseRetrieve or update data
CommandCreate, update, or delete operations
QueryRead-only operations
NotificationMultiple independent actions
Pipeline BehaviorLogging, 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.