CQRS + MediatR in ASP.NET Core: A Practical Guide
CQRS + MediatR in ASP.NET Core: A Practical Guide
CQRS + MediatR is one of the most powerful and widely used patterns in modern .NET applications. Let’s break it down step by step in a clear, practical way.
1. What is CQRS?
CQRS stands for Command Query Responsibility Segregation.
It is a simple but powerful idea:
- Commands = Operations that change state (Create, Update, Delete)
- Queries = Operations that read state (Get data)
Instead of using the same model and service for both reading and writing, we separate them.
Why use CQRS?
| Problem in Traditional CRUD | Solution with CQRS |
|---|---|
| Same model used for read & write | Separate models optimized for each |
| Complex business logic mixed with queries | Clean separation of concerns |
| Hard to scale reads independently | Reads can be optimized separately |
| Difficult to audit changes | Commands can be logged easily |
Key Insight: Most applications read data much more often than they write it. CQRS lets you optimize both sides differently.
2. What is MediatR?
MediatR is a lightweight in-process messaging library for .NET.
It implements the Mediator Pattern - instead of classes calling each other directly, they send messages (commands/queries) through a central mediator.
Benefits of MediatR
- Loose coupling
- Clean, testable code
- Easy to add cross-cutting concerns (logging, validation, authorization, performance monitoring)
- Excellent support for CQRS
3. How CQRS + MediatR Work Together
Basic flow:
- Controller receives HTTP request
- Creates a Command or Query object (simple DTO)
- Sends it to MediatR
- MediatR finds the correct Handler and executes it
- Handler returns result (for queries) or just completes (for commands)
4. Practical Example (Simple Blog App)
Command Example (Write Operation)
// Command
public record CreatePostCommand(string Title, string Content, Guid AuthorId) : IRequest<Guid>;
// Command Handler
public class CreatePostCommandHandler : IRequestHandler<CreatePostCommand, Guid>
{
private readonly IPostRepository _repository;
public async Task<Guid> Handle(CreatePostCommand request, CancellationToken ct)
{
var post = new Post
{
Title = request.Title,
Content = request.Content,
AuthorId = request.AuthorId
};
await _repository.AddAsync(post);
return post.Id;
}
}
Query Example (Read Operation)
// Query
public record GetPostByIdQuery(Guid Id) : IRequest<PostDto>;
// Query Handler
public class GetPostByIdQueryHandler : IRequestHandler<GetPostByIdQuery, PostDto>
{
private readonly IPostReadRepository _readRepo; // Separate read repository
public async Task<PostDto> Handle(GetPostByIdQuery request, CancellationToken ct)
{
return await _readRepo.GetPostDtoAsync(request.Id);
}
}
Controller (Very Clean)
[ApiController]
[Route("api/posts")]
public class PostsController : ControllerBase
{
private readonly IMediator _mediator;
public PostsController(IMediator mediator) => _mediator = mediator;
[HttpPost]
public async Task<IActionResult> Create(CreatePostCommand command)
{
var postId = await _mediator.Send(command);
return Ok(postId);
}
[HttpGet("{id}")]
public async Task<IActionResult> Get(Guid id)
{
var post = await _mediator.Send(new GetPostByIdQuery(id));
return Ok(post);
}
}
5. Common Misconceptions & Confusion Points
- “CQRS means I need two databases” → Not true. You can start with one database and later split reads/writes.
- “MediatR is just another DI container” → No. It’s a messaging pipeline with powerful behavior pipeline support.
- “CQRS makes everything complicated” → Actually, for small apps it adds slight overhead, but for medium+ apps it makes code much cleaner.
- “Every operation needs a Command/Query” → Not necessary. Use CQRS where it gives real value (complex business logic or high read/write imbalance).
6. Advanced & Best Practices (Important for Real Projects)
- Use separate read and write models (especially DTOs for queries)
- Implement validation using FluentValidation + MediatR Pipeline Behavior
- Add logging, authorization, caching as pipeline behaviors (very powerful)
- Use Outbox Pattern when combining CQRS with RabbitMQ
- Consider Vertical Slice Architecture (group by feature, not by layer)
Pipeline Behavior Example (one of the best features of MediatR)
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct)
{
// Validate request before handler runs
return await next();
}
}
7. When Should You Use CQRS + MediatR?
Use it when:
- Application has complex business rules
- You need high performance on read side
- You want clean, testable code
- You plan to use Event Sourcing or Message Queues later
Don’t use it when:
- Building very simple CRUD apps (overkill)
- Team is new to the pattern
Quick Summary
| Concept | Purpose |
|---|---|
| Command | Changes state (write) |
| Query | Reads state (no side effects) |
| MediatR | In-process mediator that routes commands/queries to handlers |
| Pipeline Behavior | Cross-cutting concerns (validation, logging, etc.) |
Start small. Use CQRS + MediatR for the parts of your app that need it most. You don’t have to rewrite everything on day one.
Happy coding - and may your commands be side-effect-free and your queries fast!
Enjoyed this article? Share it with your network!