Step right up to the Generative AI Use Cases Repository! See how MongoDB powers your apps with real-time, operational data. It integrates vector embeddings with live data, covering transactional, search, analytics, and more—all in one platform. Resilient, scalable, and secure—perfect for your next app. Explore the use cases here.
Have you mastered custom HttpContent
in C#?
Subtle errors can lead to unexpected behavior, memory issues, and performance bottlenecks. Don't fall into these traps.
Master it now before it breaks your code!
"We need to implement CQRS? Great, let me install MediatR."
If you've heard this in your development team - or perhaps said it yourself - you're not alone. The .NET ecosystem has gradually fused these two concepts together, creating an almost reflexive response: CQRS equals MediatR.
This mental shortcut has led countless teams down a path of unnecessary complexity. Others have avoided CQRS entirely, fearing the overhead of yet another messaging framework.
In this article, we'll dispel some common misconceptions and highlight the benefits of each pattern.
Understanding CQRS in Its Pure Form
CQRS is a pattern that separates read and write operations in your application. The pattern suggests that the models used for reading data should be different from those used for writing data.
That's it.
No specific implementation details, no prescribed libraries, just a simple architectural principle.
The pattern emerged from the understanding that in many applications, especially those with complex domains, the requirements for reading and writing data are fundamentally different. Read operations often need to combine data from multiple sources or present it in specific formats for UI consumption. Write operations need to enforce business rules, maintain consistency, and manage domain state.
This separation provides several benefits:
- Optimized read and write models for their specific purposes
- Simplified maintenance as read and write concerns evolve independently
- Enhanced scalability options for read and write operations
- Clearer boundary between domain logic and presentation needs
MediatR: A Different Tool for Different Problems
MediatR is an implementation of the mediator pattern. Its primary purpose is to reduce direct dependencies between components by providing a central point of communication. Instead of knowing about each other, the mediator connects the components.
The library provides several features:
- In-process messaging between components
- Behavior pipelines for cross-cutting concerns
- Notification handling (publish/subscribe)
The indirection MediatR introduces is its most criticized aspect. It can make code harder to follow, especially for newcomers to the codebase. However, you can easily solve this problem by defining the requests in the same file as the handler.
Why They Often Appear Together
The frequent pairing of CQRS and MediatR isn't without reason. MediatR's request/response model aligns well with CQRS's command/query separation. Commands and queries can be implemented as MediatR requests, with handlers containing the actual implementation logic.
Here's an example command using MediatR:
public record CreateHabit(string Name, string? Description, int Priority) : IRequest<HabitDto>;
public sealed class CreateHabitHandler(ApplicationDbContext dbContext, IValidator<CreateHabit> validator)
: IRequestHandler<CreateHabit, HabitDto>
{
public async Task<HabitDto> Handle(CreateHabit request, CancellationToken cancellationToken)
{
await validator.ValidateAndThrowAsync(createHabitDto);
Habit habit = createHabitDto.ToEntity();
dbContext.Habits.Add(habit);
await dbContext.SaveChangesAsync(cancellationToken);
return habit.ToDto();
}
}
CQRS with MediatR offers several advantages:
- Consistent handling of both commands and queries
- Pipeline behaviors for logging, validation, and error handling
- Clear separation of concerns through handler classes
- Simplified testing through handler isolation
However, this convenience comes at the cost of additional abstraction and complexity. We have to define the request/response classes and handlers, write code for sending the requests, and so on. This can be overkill for simple applications.
The question isn't whether this trade-off is universally good or bad but whether it's appropriate for your specific context.
CQRS Without MediatR
CQRS can be implemented just as easily without MediatR. Here's a simple example of what it might look like.
You can define commands and queries as simple interfaces:
public interface ICommandHandler<in TCommand, TResult>
{
Task<TResult> Handle(TCommand command, CancellationToken cancellationToken = default);
}
// Same thing for IQueryHandler
Then, you can implement your handlers and register them with dependency injection:
public record CreateOrderCommand(string CustomerId, List<OrderItem> Items)
: ICommand<CreateOrderResult>;
public class CreateOrderCommandHandler : ICommandHandler<CreateOrderCommand, CreateOrderResult>
{
public async Task<CreateOrderResult> Handle(
CreateOrderCommand command,
CancellationToken cancellationToken = default)
{
// implementation
}
}
// DI registration...
builder.Services
.AddScoped<ICommandHandler<CreateOrderCommand, CreateOrderResult>, CreateOrderCommandHandler>();
Finally, you can use the handler in your controller:
[ApiController]
[Route("orders")]
public class OrdersController : ControllerBase
{
[HttpPost]
public async Task<ActionResult<CreateOrderResult>> CreateOrder(
CreateOrderCommand command,
ICommandHandler<CreateOrderCommand, CreateOrderResult> handler)
{
var result = await handler.Handle(command);
return Ok(result);
}
}
What's the difference between this and the MediatR approach?
This approach provides the same separation of concerns but without the indirection. It's direct, explicit, and often sufficient for many applications.
However, it lacks some of the conveniences that MediatR offers, such as pipeline behaviors and automatically registering handlers. You also need to inject the specific handlers into your controllers, which can be cumbersome for larger applications.
Takeaway
CQRS and MediatR are distinct tools that solve different problems. While they can work well together, treating them as inseparable does a disservice to both. CQRS separates read and write concerns, while MediatR decouples components through a mediator.
The key is understanding what each pattern offers and making informed decisions based on your specific context. Sometimes, you'll want both, sometimes just one, and sometimes neither. That's the essence of thoughtful architecture: choosing the right tools for your specific needs.
If you want to learn more about implementing CQRS effectively as part of a clean, maintainable architecture, check out Pragmatic Clean Architecture. You'll learn how to apply these patterns in real-world scenarios, avoiding common pitfalls and over-engineering while building scalable applications.
That's all for today. Hope this was helpful.