Introduction
In my previous article, Understanding CQRS (Command Query Responsibility Segregation) Pattern in .NET, we implemented the CQRS pattern from scratch without relying on any external libraries. The goal was to understand how commands, queries, handlers, and dependency injection work together behind the scenes rather than hiding that complexity behind a framework.
If you haven't read that article yet, I highly recommend starting there first. This article assumes you're already familiar with the CQRS pattern and the project we built together.
Now that we understand the fundamentals, it's time to improve our implementation by introducing MediatR.
One of the common observations in our previous implementation was that every controller had to inject the handlers it needed. As the application grows, the number of constructor dependencies also grows, making controllers more tightly coupled to individual handlers. While the application works perfectly fine, the code can become harder to maintain over time.
MediatR solves this by acting as a mediator between your controllers and handlers. Instead of controllers knowing about every command or query handler, they simply send a request through IMediator, and MediatR automatically finds and executes the appropriate handler. This keeps controllers cleaner and reduces direct dependencies without changing the overall CQRS design.
In this article, we'll continue with the same Product Management Web API from the previous article. We won't redesign the project or introduce new architectural patterns. We'll simply replace our manual handler wiring with MediatR while keeping the existing folder structure and overall flow almost unchanged.
By the end of this article, you'll see how easy it is to migrate an existing CQRS implementation to MediatR and why it has become the standard approach in many .NET applications.
What is MediatR?
MediatR is a lightweight library that implements the Mediator design pattern in .NET. Its primary purpose is to reduce direct dependencies between different parts of your application by acting as an intermediary for handling requests.
In our previous implementation, the controller directly depended on individual command and query handlers. Whenever a request came in, the controller was responsible for calling the appropriate handler.
Controller
|
|-- CreateProductCommandHandler
|-- UpdateProductCommandHandler
|-- DeleteProductCommandHandler
`-- GetProductByIdQueryHandlerAs the number of operations grows, controllers gradually become dependent on more and more handlers. While this approach works, it increases coupling between the controller and the application layer.
With MediatR, the controller no longer communicates with handlers directly. Instead, it sends a command or query to IMediator, and MediatR automatically locates and executes the correct handler.
Controller
|
IMediator
|
|-- CreateProductCommandHandler
|-- UpdateProductCommandHandler
|-- DeleteProductCommandHandler
`-- GetProductByIdQueryHandlerThis small change makes a big difference. The controller only needs a single dependency (IMediator), while MediatR takes care of routing each request to its corresponding handler. As a result, controllers become cleaner, easier to maintain, and no longer need to know which handler is responsible for processing a particular request.
It's important to understand that MediatR does not implement CQRS for you. CQRS is still your architectural pattern, and you'll continue to create commands, queries, responses, and handlers just as before. MediatR simply removes the manual wiring between controllers and handlers, allowing you to focus on your application's business logic rather than the plumbing code.
Why Use MediatR with CQRS?
If you've already implemented CQRS manually, you might be wondering whether MediatR is actually necessary. After all, our previous implementation worked perfectly without it.
The answer is that MediatR doesn't change how CQRS works; it simplifies how the different parts communicate with each other.
In our previous project, each controller had to inject the handlers it needed. As more commands and queries were added, the constructor gradually became larger because every new operation required another dependency.
public class ProductsController(
CreateProductCommandHandler createHandler,
UpdateProductCommandHandler updateHandler,
DeleteProductCommandHandler deleteHandler,
GetAllProductsQueryHandler getAllHandler,
GetProductByIdQueryHandler getByIdHandler)
{
...
}This isn't wrong, but it does make the controller responsible for knowing about every handler it uses.
With MediatR, the controller only depends on IMediator.
public class ProductsController(IMediator mediator)
{
...
}Whenever the controller needs to execute a command or query, it simply sends the request through the mediator.
await _mediator.Send(command);or
var products = await _mediator.Send(query);MediatR automatically resolves the appropriate handler from the dependency injection container and executes it. The controller doesn't need to know which handler processes the request or how that handler is resolved.
This approach provides several benefits:
- Controllers have only a single dependency.
- Adding a new command or query doesn't require modifying the controller constructor.
- Controllers remain focused on handling HTTP requests rather than coordinating handlers.
- The communication between controllers and handlers becomes loosely coupled, making the application easier to maintain as it grows.
It's worth mentioning that using MediatR is completely optional. CQRS is a design pattern, not a library. You can implement it manually, just as we did in the previous article, or you can use MediatR to reduce boilerplate code and improve maintainability. Both approaches follow the same CQRS principles; the difference is simply how requests are dispatched to their handlers.
Installing MediatR
To start using MediatR in our project, we first need to install the NuGet package. At the time of writing, the latest stable version is 14.2.0.
You can install it using the .NET CLI:
dotnet add package MediatR --version 14.2.0Or, if you prefer using the Package Manager Console in Visual Studio:
Install-Package MediatR -Version 14.2.0Once the package is installed, we're ready to integrate it into our existing CQRS project.
One thing worth mentioning is that older versions of MediatR required installing an additional package called MediatR.Extensions.Microsoft.DependencyInjection for dependency injection support. That's no longer the case. Starting with recent versions, everything needed to register MediatR is included in the main MediatR package itself, so we only need a single package reference.
In the next section, we'll register MediatR with the dependency injection container so it can automatically discover and resolve our command and query handlers.
Registering MediatR
After installing the package, the next step is to register MediatR with the dependency injection container.
Open the Program.cs file and register MediatR using the AddMediatR extension method.
using MediatR;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMediatR(config =>
{
config.RegisterServicesFromAssembly(typeof(Program).Assembly);
});The RegisterServicesFromAssembly method tells MediatR which assembly to scan for handlers. During application startup, it automatically discovers all classes that implement interfaces such as IRequestHandler<TRequest, TResponse> and registers them with the dependency injection container.
Since all of our commands, queries, and handlers are part of the same Web API project, using typeof(Program).Assembly is sufficient.
If your application is split into multiple projects, such as a separate Application project that contains all commands, queries, and handlers, you should register that assembly instead.
builder.Services.AddMediatR(config =>
{
config.RegisterServicesFromAssembly(typeof(CreateProductCommand).Assembly);
});This ensures that MediatR can locate and resolve all of your handlers automatically without requiring you to register each one manually.
Updating Commands to IRequest
In our previous implementation, commands were simple classes that contained the data required to perform an operation. The corresponding handler accepted the command and processed it.
For example, our CreateProductCommand looked like this:
public class CreateProductCommand
{
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public int Stock { get; set; }
}To make this command compatible with MediatR, we simply need to implement the IRequest<TResponse> interface.
Since creating a product returns a ProductResponse, we'll specify that as the generic type.
using MediatR;
public class CreateProductCommand : IRequest<ProductResponse>
{
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public int Stock { get; set; }
}That's the only change required. The properties remain exactly the same. We're simply informing MediatR that this class represents a request which will produce a ProductResponse.
The same approach applies to other commands as well. For example, our UpdateProductCommand can be updated like this:
using MediatR;
public class UpdateProductCommand : IRequest<ProductResponse>
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public int Stock { get; set; }
}For operations like delete, returning a small status can be useful so the controller can decide between 404 Not Found and 204 No Content. In this project, we'll return bool from delete and implement IRequest<bool>.
using MediatR;
public class DeleteProductCommand : IRequest<bool>
{
public int Id { get; set; }
}Apart from implementing these interfaces, the structure of your commands doesn't change. They still represent the data required to perform an action. The only difference is that MediatR can now recognize them as requests and route them to the appropriate handler automatically.
Updating Queries to IRequest<T>
Just like commands, our queries also need to implement a MediatR request interface. The only difference is that queries always return data, so we'll use the generic IRequest<TResponse> interface.
Let's start with the GetProductByIdQuery.
Our previous implementation looked like this:
public class GetProductByIdQuery
{
public int Id { get; set; }
}Since this query returns a single product, we'll implement IRequest<ProductResponse>.
using MediatR;
public class GetProductByIdQuery : IRequest<ProductResponse>
{
public int Id { get; set; }
}Similarly, our GetAllProductsQuery returns a collection of products, so we'll specify List<ProductResponse> as the response type.
using MediatR;
public class GetAllProductsQuery : IRequest<List<ProductResponse>>
{
}Notice that GetAllProductsQuery doesn't contain any properties because it doesn't require any input to retrieve all products. Even an empty class can act as a request in MediatR.
With these small changes, all of our queries become MediatR requests. Similar to the commands, the overall structure remains unchanged. We're simply implementing the appropriate interface so MediatR knows what type of response each query should return and can dispatch it to the correct handler automatically.
Implementing Command Handlers
Now that our commands implement IRequest or IRequest<T>, it's time to update their handlers.
Previously, our command handlers were regular classes with a Handle method that we invoked manually from the controller.
For example, our CreateProductCommandHandler looked something like this:
public class CreateProductCommandHandler
{
private readonly ApplicationDbContext _context;
public CreateProductCommandHandler(ApplicationDbContext context)
{
_context = context;
}
public async Task<ProductResponse> Handle(CreateProductCommand command)
{
// Implementation
}
}To integrate with MediatR, the handler needs to implement the IRequestHandler<TRequest, TResponse> interface.
using MediatR;
public class CreateProductCommandHandler
: IRequestHandler<CreateProductCommand, ProductResponse>
{
private readonly ApplicationDbContext _context;
public CreateProductCommandHandler(ApplicationDbContext context)
{
_context = context;
}
public async Task<ProductResponse> Handle(
CreateProductCommand request,
CancellationToken cancellationToken)
{
// Existing implementation
}
}The implementation inside the Handle method remains almost identical to what we had before. The only noticeable changes are:
- The handler now implements
IRequestHandler<CreateProductCommand, ProductResponse>. - The
Handlemethod includes aCancellationTokenparameter, as required by the interface. - The parameter can be named
requestorcommandbased on your preference.
The same approach applies to the UpdateProductCommandHandler.
public class UpdateProductCommandHandler
: IRequestHandler<UpdateProductCommand, ProductResponse>
{
public async Task<ProductResponse> Handle(
UpdateProductCommand request,
CancellationToken cancellationToken)
{
// Existing implementation
}
}For delete, we'll keep the same success/failure status and use IRequestHandler<TRequest, TResponse>.
For example, our DeleteProductCommandHandler becomes:
public class DeleteProductCommandHandler
: IRequestHandler<DeleteProductCommand, bool>
{
public async Task<bool> Handle(
DeleteProductCommand request,
CancellationToken cancellationToken)
{
// Existing implementation
}
}Apart from implementing the appropriate interface and adding the CancellationToken parameter, the business logic inside your handlers doesn't need to change. All of the validation, database operations, and response creation can remain exactly as they were in the previous implementation.
Implementing Query Handlers
Updating query handlers follows the same approach as command handlers. Instead of being regular classes, they now implement the IRequestHandler<TRequest, TResponse> interface provided by MediatR.
Let's start with the GetProductByIdQueryHandler.
Our previous implementation looked like this:
public class GetProductByIdQueryHandler
{
private readonly ApplicationDbContext _context;
public GetProductByIdQueryHandler(ApplicationDbContext context)
{
_context = context;
}
public async Task<ProductResponse> Handle(GetProductByIdQuery query)
{
// Existing implementation
}
}After introducing MediatR, the handler becomes:
using MediatR;
public class GetProductByIdQueryHandler
: IRequestHandler<GetProductByIdQuery, ProductResponse>
{
private readonly ApplicationDbContext _context;
public GetProductByIdQueryHandler(ApplicationDbContext context)
{
_context = context;
}
public async Task<ProductResponse> Handle(
GetProductByIdQuery request,
CancellationToken cancellationToken)
{
// Existing implementation
}
}The changes are very similar to what we made for the command handlers. The handler now implements IRequestHandler<GetProductByIdQuery, ProductResponse>, and the Handle method includes a CancellationToken parameter required by the interface.
The same applies to GetAllProductsQueryHandler.
public class GetAllProductsQueryHandler
: IRequestHandler<GetAllProductsQuery, List<ProductResponse>>
{
private readonly ApplicationDbContext _context;
public GetAllProductsQueryHandler(ApplicationDbContext context)
{
_context = context;
}
public async Task<List<ProductResponse>> Handle(
GetAllProductsQuery request,
CancellationToken cancellationToken)
{
// Existing implementation
}
}Other than implementing the IRequestHandler interface and updating the method signature, the rest of the code remains unchanged. Your query logic, database access, and mapping can stay exactly as they were. MediatR simply provides a standard way to receive the request and return the response, while automatically invoking the correct handler when the query is sent.
Updating the Controller to Use IMediator
Now comes the part where you'll notice the biggest difference. Instead of injecting individual command and query handlers into the controller, we'll inject a single IMediator instance and use it to send requests.
Before making this change, there's one important thing to mention.
In the previous article, we didn't bind commands and queries directly to the API endpoints. Instead, we created separate request models to receive data from the client and then mapped those request models to their corresponding command or query classes. This approach keeps our API contract separate from the application's internal implementation, making it easier to evolve both independently.
We'll continue following the same approach in this article.
Previously, our controller looked something like this:
[HttpPost]
public async Task<IActionResult> Create(CreateProductRequest request)
{
var command = new CreateProductCommand
{
Name = request.Name,
Price = request.Price,
Stock = request.Stock
};
var result = await _createProductCommandHandler.Handle(command);
return Ok(result);
}The controller was responsible for creating the command and then calling the appropriate handler directly.
After introducing MediatR, the mapping remains exactly the same. The only difference is how the command is executed.
First, inject IMediator into the controller.
public class ProductsController(IMediator mediator) : ControllerBase
{
private readonly IMediator _mediator = mediator;
}Now update the action method.
[HttpPost]
public async Task<IActionResult> Create(CreateProductRequest request)
{
var command = new CreateProductCommand
{
Name = request.Name,
Price = request.Price,
Stock = request.Stock
};
var result = await _mediator.Send(command);
return Ok(result);
}The same applies to queries.
[HttpGet("{id}")]
public async Task<IActionResult> GetById(int id)
{
var query = new GetProductByIdQuery
{
Id = id
};
var result = await _mediator.Send(query);
return Ok(result);
}Notice that the request mapping hasn't changed at all. We still convert our API request models into commands and queries, just as we did in the previous article. The only difference is that instead of calling a specific handler's Handle method, we simply call Send on IMediator.
MediatR takes care of locating the correct handler and executing it, allowing the controller to depend on a single service regardless of how many commands and queries the application contains.
Advantages of MediatR and When Should You Use It?
After migrating our project to MediatR, you might notice that the overall application hasn't changed much. We still have commands, queries, handlers, DTOs, and the same business logic. The biggest improvement is in how these components communicate with each other.
One of the most noticeable benefits is cleaner controllers. Instead of injecting multiple handlers, the controller depends on a single IMediator instance. This keeps constructors small and allows controllers to focus solely on handling HTTP requests.
Another advantage is reduced coupling. Controllers no longer need to know which handler is responsible for processing a particular command or query. They simply send a request, and MediatR takes care of locating and executing the appropriate handler. This makes the application easier to extend because adding a new command or query usually doesn't require any changes to the controller.
MediatR also removes a lot of repetitive wiring code. Once your handlers are registered, request routing happens automatically, allowing you to spend more time writing business logic instead of connecting different parts of the application.
That said, MediatR isn't a requirement for implementing CQRS. As we saw in the previous article, CQRS can be implemented successfully without using any external library. If you're building a small application with only a few endpoints, introducing MediatR may not provide significant benefits and can add an extra abstraction layer.
However, as the application grows and the number of commands and queries increases, MediatR helps keep the codebase organized and easier to maintain. This is why it's widely adopted in medium and large .NET applications that follow the CQRS pattern.
Conclusion
In the previous article, we implemented CQRS from scratch to understand how the pattern works behind the scenes. We manually created commands, queries, handlers, and wired everything together using dependency injection. That approach helped us understand the core concepts without relying on any external libraries.
In this article, we built on the same project and integrated MediatR without changing the overall architecture. Our commands and queries still represent requests, handlers still contain the business logic, and the separation between read and write operations remains exactly the same. The only difference is that MediatR now handles the communication between controllers and handlers, resulting in cleaner controllers and less boilerplate code.
Understanding both approaches is valuable. Implementing CQRS manually gives you a solid understanding of how the pattern works, while MediatR provides a cleaner and more maintainable way to apply those same principles in real-world applications.
I hope this article helped you understand how easy it is to migrate an existing CQRS implementation to MediatR. If you have any questions or suggestions, feel free to share them in the comments.
If you'd like to explore the complete source code for this implementation, you can find it in the GitHub repository below.
GitHub Repository: https://github.com/YogeshHadiya33/CQRSArchitecturalPatternWithMediatR
