Stop Conflating CQRS and MediatR

Stop Conflating CQRS and MediatR

4 min read ·

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:

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.


Whenever you're ready, there are 4 ways I can help you:

  1. (COMING SOON) Pragmatic REST APIs: You will learn how to build production-ready REST APIs using the latest ASP.NET Core features and best practices. It includes a fully functional UI application that we'll integrate with the REST API. Join the waitlist!
  2. Pragmatic Clean Architecture: Join 3,850+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.
  3. Modular Monolith Architecture: Join 1,650+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.
  4. Patreon Community: Join a community of 1,000+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.

Become a Better .NET Software Engineer

Join 63,000+ engineers who are improving their skills every Saturday morning.