Implementing CQRS with MediatR
With MediatR, a popular open-source mediator implementation for .NET, developers can achieve greater separation of concerns through the Command Query Responsibility Segregation (CQRS) pattern.
In modern application development, organizing code for maintainability, scalability, and separation of concerns is crucial. With MediatR, a popular open-source mediator implementation for .NET, developers can achieve greater separation of concerns through the Command Query Responsibility Segregation (CQRS) pattern.
In this article, I will introduce the CQRS pattern, explore the MediatR library, and guide you through integrating MediatR within an ABP-based layered application. Through practical examples, I'll demonstrate how to implement CQRS using a single data source, with step-by-step explanations. In future articles, we can explore more advanced scenarios like separating read and write data sources and implementing database synchronization (so, it's totally up to your feedback 😀).
What is CQRS?
Command Query Responsibility Segregation (CQRS) is an architectural pattern that separates read and write operations for a data store. In this pattern:
Commands: Change the state of an object or system but don't return data (for just WRITE operations)
Queries: Return data without changing the system's state (for just READ operations)
This pattern offers better scalability by enforcing separate read and write operations, allowing them to scale independently and use different data sources. Additionally, it improves testability since commands and queries can be tested in isolation, making it easier to verify business logic and data access separately.
What is MediatR?
MediatR is a lightweight mediator library for .NET that helps implement the mediator pattern. It facilitates:
In-process messaging
Request/response communication
Event-based communication
Pipeline behaviors for cross-cutting concerns (which are pretty helpful if you need a middle layer between request and response and set additional logic or validation requirements)
MediatR simplifies implementing CQRS by providing a clean, consistent way to handle commands and queries. Now, let's implement CQRS with MediatR in an ABP-based layered application.
In this guide, I use an ABP-based application to provide a clear, layered DDD structure for demonstrating our CQRS implementation. If you already have a layered DDD application, feel free to apply these concepts directly to your own project—the underlying principles are the same. (So, feel free to skip the project creation section)
Creating an ABP Based Application & Setting Up MediatR
Before starting the development, please create a new solution named AbpMediatrDemo
and run it by following the getting started tutorial.
You can use the following configurations:
Solution Template: Application (Layered)
Solution Name: AbpMediatrDemo
UI Framework: MVC / Razor Pages
UI Theme: LeptonX
Database Provider: MongoDB
After creating the application via ABP Studio, then you can open the solution in your favorite IDE:
Setting Up MediatR
Step 1: Installing Required Packages
First, add the MediatR packages to your application's Application.Contracts
layer by using the following commands:
dotnet add package MediatR
dotnet add package MediatR.Extensions.Microsoft.DependencyInjection
Add these packages to the
Application.Contracts
project since they will be used both in the application services and command/query models.
Step 2: Registering MediatR Services
Register the MediatR services in your application layer's module class (we need to make the registration in this project because we will create our models and handlers in the application layer, in the next steps):
public class AbpMediatrDemoApplicationModule : AbpModule | |
{ | |
public override void ConfigureServices(ServiceConfigurationContext context) | |
{ | |
Configure<AbpAutoMapperOptions>(options => | |
{ | |
options.AddMaps<AbpMediatrDemoApplicationModule>(); | |
}); | |
context.Services.AddMediatR(config => | |
{ | |
config.RegisterServicesFromAssembly(typeof(AbpMediatrDemoApplicationContractsModule).Assembly); | |
}); | |
} | |
} |
With this configuration, we can use MediatR services in our application including in-process messaging, request handlers, and pipeline behaviors as example. This registration enables MediatR to scan the assembly and automatically discover all handlers, allowing us to send commands and queries through the mediator pattern without manual registration of each handler.
Example: CQRS in Action
Now, we can demonstrate this pattern with a simple example. Let's go through with a step-by-step approach:
Creating Command and Query Models
Define your command and query models in the Application.Contracts
project:
CreateProductCommand.cs:
using System;
using MediatR;
namespace AbpMediatrDemo;
public class CreateProductCommand : IRequest<ProductDto>
{
public string Name { get; set; }
public decimal Price { get; set; }
public string Description { get; set; }
}
GetProductQuery.cs:
using System;
using MediatR;
namespace AbpMediatrDemo;
public class GetProductQuery : IRequest<ProductDto>
{
public Guid Id { get; set; }
}
In our command and query models, our classes implement the IRequest<ProductDto>
interface, where ProductDto
represents the response type that will be returned from the command or query operation. Let's define this DTO class next.
ProductDto.cs:
using System;
using MediatR;
namespace AbpMediatrDemo;
public class ProductDto
{
public Guid Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string Description { get; set; }
}
After defining our command and query models that encapsulate the input parameters and specify the return types (ProductDto
) for our operations, we can now create their corresponding handlers. These handlers contain the actual business logic that will be executed when the respective commands or queries are triggered. This separation of models and handlers follows the CQRS (Command Query Responsibility Segregation) pattern, which helps maintain a clean separation between the data modification operations (commands) and data retrieval operations (queries).
Implementing Command and Query Handlers
In your Application
layer, implement handlers for your commands and queries as follows:
CreateProductCommandHandler.cs:

For demonstration purposes, this command handler simply returns the input data without persisting it to a database. In a real application, you would inject a repository and save the product record. The command handler executes when a product creation request is made through the application layer, which we'll implement next.
GetProductQueryHandler.cs:

The GetProductQueryHandler
class represents the query side of our CQRS implementation, demonstrating how MediatR facilitates separation of read operations from write operations.
Using MediatR in Application Services
First, let's create a new application service interface as below in the *.Application.Contracts
project:
using System;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
namespace AbpMediatrDemo;
public interface IProductAppService : IApplicationService
{
Task<ProductDto> CreateAsync(CreateProductCommand command);
Task<ProductDto> GetAsync(Guid id);
}
Then, we can create a new application service class (ProductAppService.cs
) and implement the interface:
public class ProductAppService : ApplicationService, IProductAppService
{
private readonly IMediator _mediator;
public ProductAppService(IMediator mediator)
{
_mediator = mediator;
}
public async Task<ProductDto> CreateAsync(CreateProductCommand command)
{
return await _mediator.Send(command);
}
public async Task<ProductDto> GetAsync(Guid id)
{
return await _mediator.Send(new GetProductQuery { Id = id });
}
}
In the ProductAppService
class, we can see how MediatR elegantly simplifies our application service implementation and bridges the gap between our API contracts and the underlying CQRS handlers:
This service class acts as a thin wrapper around MediatR's core functionality. First, we inject the
IMediator
interface through constructor injection. This interface is the primary entry point for all MediatR operations.In this
CreateAsync
method, we receive aCreateProductCommand
directly from the client and simply forward it to MediatR using the_mediator.Send()
method. MediatR then takes responsibility for locating the appropriate handler (in this case,CreateProductCommandHandler
), executing it, and returning the result. This approach removes the need for the application service to contain any business logic or data access code.Similarly, in the
GetAsync
method, we construct a newGetProductQuery
object with the provided ID and send it through the mediator. Again, MediatR handles the routing to the correct query handler (GetProductQueryHandler
).
With this approach, you only need to inject the IMediator
service into your application service. Then, in the related handlers, you can inject any required services based on your needs. This enables powerful separation of concerns and makes it possible to use different data stores for command and query operations.
Let's set a breakpoint in the created services and examine the complete flow:
Conclusion
Integrating MediatR with ABP Framework provides a powerful combination for implementing CQRS in your applications. This approach helps separate read and write operations, leading to a more maintainable and scalable architecture.
By following the steps outlined in this article, you can successfully implement MediatR within your ABP-based layered application, taking advantage of both frameworks' strengths to build robust, maintainable software solutions.
If you found this article helpful, please show your support by giving it a reaction. 😀
💪 Drop a comment if you'd like to see a deep-dive follow-up article on implementing advanced CQRS patterns - I'm planning to cover how to leverage different data stores (PostgreSQL for writes, MongoDB for reads - depends on your comments -) to build high-performance, scalable systems.
After reading this by Steve Smith (https://ardalis.com/moving-from-controllers-and-actions-to-endpoints-with-mediatr/), I was curious to see if the Mediator Pattern and Mediatr could be utilised in an ABP project and this post has totally nailed the answer to that question! Well written and concise!
Excited to your upcoming articles on this topic!