A Practical Guide to Implementing CQRS Pattern with MediatR in .NET Core
Learn how to implement the CQRS Pattern with MediatR in .NET Core to separate read/write logic, making your application more scalable, maintainable, and performant.
Table of Contents
No table of contents available for this article
1. Introduction to CQRS Pattern
As software applications grow, managing data becomes increasingly complex. Think of it this way: a small shop only needs one employee handling both sales and inventory. But when that shop becomes a supermarket, you need separate teams – cashiers focusing on transactions and warehouse staff managing stock. Each team specializes in their domain.
CQRS (Command Query Responsibility Segregation) works on the same principle. Instead of using a single model to handle both reading and writing data, CQRS separates them into two distinct models:
Command: Responsible for changing data state (create, update, delete)
Query: Responsible for reading and returning data
This separation brings multiple benefits: optimized performance for each operation type, easier scalability, and improved application security.
2. Core Architecture of CQRS
2.1. Command – The State Changer
Commands are "instructions" requesting changes to the system state. Think of a Command like a purchase order: it contains all the information about what needs to be done, but it doesn't return the product itself – it only triggers the processing workflow.
Key characteristics of Commands:
Perform operations: Create, Update, Delete
Don't return data; they modify application state
Each Command is an object containing the operation name and required data
CommandHandler receives and processes the Command, then emits an event – either success or failure
Real-world example: When you click "Add to Cart" on an e-commerce website, the system creates an AddToCartCommand containing product information and quantity. This Command is sent to the AddToCartCommandHandler for processing.
2.2. Query – The Data Reader
Query has a simpler job: just read and return data without modifying anything.
Key characteristics of Queries:
Contains only data retrieval methods
Returns data to the client for UI display
QueryHandler receives the Query and returns corresponding results
Real-world example: When a customer searches for "iPhone," the system creates a SearchProductQuery and sends it to SearchProductQueryHandler. The handler queries the database and returns matching products.
3. When Should You Apply CQRS?
CQRS isn't a silver bullet for every project. This pattern shines in scenarios such as:
Large e-commerce systems: Read queries (viewing products, searching) typically outnumber write operations (ordering, cart updates) by orders of magnitude
Financial systems: Require high data integrity when writing while needing fast read speeds for reports
Real-time analytics applications: Need separate optimization for logging events and reading dashboards
Let's illustrate with an e-commerce example:
With traditional design, the Products table handles both reading (displaying products) and writing (adding/editing products). As traffic increases, both operations compete for resources, degrading performance.
With CQRS:
Command Model manages adding, editing, and deleting products – optimized for data integrity and write performance
Query Model manages displaying and searching products – optimized for read speed and user experience
4. Implementing CQRS with MediatR in .NET Core
MediatR is a lightweight library that implements the mediator pattern in .NET. It acts as a "middleman" routing requests (Commands/Queries) to their corresponding Handlers.
4.1. Install MediatR
Open Package Manager Console and run:
Install-Package MediatR
Or using .NET CLI:
dotnet add package MediatR
4.2. Define Commands and Queries
// Command: Add a new product public class AddProductCommand : IRequest<int> { public string Name { get; set; } public decimal Price { get; set; } }
// Query: Get product information by ID public class GetProductQuery : IRequest<Product> { public int ProductId { get; set; } }
Note: IRequest<T> indicates this request returns type T. For Commands that don't need to return anything, you can use IRequest<Unit> or IRequest (MediatR provides Unit as an awaitable "void").
4.3. Implement Handlers
// Handler for AddProductCommand public class AddProductCommandHandler : IRequestHandler<AddProductCommand, int> { private readonly IProductRepository _productRepository;
public AddProductCommandHandler(IProductRepository productRepository)
{
_productRepository = productRepository;
}
public async Task<int> Handle(AddProductCommand request, CancellationToken cancellationToken)
{
var product = new Product
{
Name = request.Name,
Price = request.Price
};
await _productRepository.AddAsync(product);
// Return the ID of the newly created product
return product.Id;
}}
// Handler for GetProductQuery public class GetProductQueryHandler : IRequestHandler<GetProductQuery, Product> { private readonly IProductRepository _productRepository;
public GetProductQueryHandler(IProductRepository productRepository)
{
_productRepository = productRepository;
}
public async Task<Product> Handle(GetProductQuery request, CancellationToken cancellationToken)
{
return await _productRepository.GetByIdAsync(request.ProductId);
}}
4.4. Register Services in DI Container
In Program.cs (for .NET 6+):
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
Or for .NET Core 3.1/.NET 5:
services.AddMediatR(typeof(Startup).Assembly);
4.5. Use in Controller
[ApiController] [Route("api/[controller]")] public class ProductController : ControllerBase { private readonly IMediator _mediator;
public ProductController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost]
public async Task<IActionResult> AddProduct([FromBody] AddProductCommand command)
{
var productId = await _mediator.Send(command);
return CreatedAtAction(nameof(GetProduct), new { productId }, null);
}
[HttpGet("{productId}")]
public async Task<IActionResult> GetProduct(int productId)
{
var query = new GetProductQuery { ProductId = productId };
var product = await _mediator.Send(query);
if (product == null)
return NotFound();
return Ok(product);
}}
5. Conclusion
CQRS combined with MediatR is a powerful approach for building well-structured and scalable .NET applications. However, don't apply CQRS just because it "sounds cool" – evaluate whether your project genuinely needs this separation.
For small projects or simple CRUD applications, CQRS might introduce unnecessary overhead. But for complex systems with high demands for performance and scalability, CQRS becomes a valuable tool for architecting your application professionally.
6. References
Microsoft Docs – CQRS Pattern: https://docs.microsoft.com/en-us/azure/architecture/patterns/cqrs
MediatR GitHub Repository: https://github.com/jbogard/MediatR
Martin Fowler – CQRS: https://martinfowler.com/bliki/CQRS.html