CQRS with MediatR: Command and Query Separation in .NET


As your .NET application grows in complexity, mixing business logic for reads and writes in the same service or controller can lead to confusion, duplication, and tight coupling. That’s where CQRS (Command Query Responsibility Segregation) comes in.
In this article, we’ll explore how to implement CQRS in a scalable .NET application using MediatR, a popular in-process messaging library that fits perfectly into Clean Architecture.
📌 What is CQRS?
CQRS stands for Command Query Responsibility Segregation. It is a design pattern that separates write operations (commands) from read operations (queries).
Commands: Modify system state (e.g., create an order, update a user)
Queries: Return data without side effects (e.g., get order details)
This separation makes your code more organized, easier to test, and enables you to scale reads and writes independently.
🤔 Why Not Just Use CRUD?
Traditional CRUD mixes all logic into the same service or controller:
public class OrdersController : ControllerBase
{
public IActionResult CreateOrder(OrderDto dto) { ... }
public IActionResult GetOrder(int id) { ... }
}
This leads to:
Fat controllers and services
Poor separation of concerns
Difficulty testing and reusing logic
CQRS promotes a clean split between these concerns.
⚙️ Implementing CQRS with MediatR
🧱 Project Setup
Install the MediatR NuGet package:
dotnet add package MediatR.Extensions.Microsoft.DependencyInjection
In Program.cs
or Startup.cs
:
builder.Services.AddMediatR(typeof(CreateOrderCommand).Assembly);
📝 Sample Command
public class CreateOrderCommand : IRequest<Guid>
{
public Guid CustomerId { get; set; }
public List<OrderItemDto> Items { get; set; }
}
🧰 Command Handler
public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, Guid>
{
private readonly IOrderRepository _repo;
private readonly IUnitOfWork _uow;
public CreateOrderHandler(IOrderRepository repo, IUnitOfWork uow)
{
_repo = repo;
_uow = uow;
}
public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken ct)
{
var order = new Order(request.CustomerId);
foreach (var item in request.Items)
{
order.AddItem(item.ProductId, item.Quantity);
}
_repo.Add(order);
await _uow.CommitAsync();
return order.Id;
}
}
🔍 Sample Query
public class GetOrderByIdQuery : IRequest<OrderDto>
{
public Guid OrderId { get; set; }
}
public class GetOrderByIdHandler : IRequestHandler<GetOrderByIdQuery, OrderDto>
{
private readonly IOrderReadService _readService;
public GetOrderByIdHandler(IOrderReadService readService)
{
_readService = readService;
}
public async Task<OrderDto> Handle(GetOrderByIdQuery request, CancellationToken ct)
{
return await _readService.GetByIdAsync(request.OrderId);
}
}
🧠 Why MediatR?
MediatR is a lightweight library for in-process message dispatching:
Promotes decoupling between controller and business logic
Makes unit testing easier (no service locator needed)
Enables use of pipeline behaviors (e.g., logging, validation)
🔁 Integration in Clean Architecture
Domain Layer: Remains pure, unaware of MediatR
Application Layer: Defines Commands, Queries, and Handlers
Infrastructure Layer: Implements persistence logic
Presentation Layer (API): Uses
_mediator.Send(...)
to dispatch commands/queries
✅ Benefits of Using CQRS with MediatR
Clear separation of concerns
Better testability and readability
Supports modular feature slices
Scales well in distributed and complex systems
Easily integrates with FluentValidation, AutoMapper, and OpenTelemetry
⚠️ Challenges and Considerations
Adds boilerplate (many files per feature)
Can be overkill for small CRUD systems
Requires architectural discipline to avoid service bloat
Start with CQRS where write logic is complex, or read and write models differ. Avoid using it everywhere blindly.
🧭 Best Practices
Keep command and query handlers short and focused
Use DTOs for contracts
Apply FluentValidation on commands
Use AutoMapper to map between entities and DTOs
Don’t put business logic in controllers
Use pipeline behaviors for cross-cutting concerns
🧠 Conclusion
CQRS with MediatR is a powerful approach to handling application logic in a clean and scalable way.
It fits naturally into Clean Architecture by keeping read and write concerns independent and making features easier to test, scale, and maintain.
Start small, adopt gradually, and use it where it brings the most value.
In the next article, we’ll explore Event-Driven Architecture in .NET, and how it connects with CQRS to make your system more reactive and resilient.
Happy coding!
Subscribe to my newsletter
Read articles from Vaibhav directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Vaibhav
Vaibhav
I break down complex software concepts into actionable blog posts. Writing about cloud, code, architecture, and everything in between.