CQRS with MediatR: Command and Query Separation in .NET

VaibhavVaibhav
3 min read

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!

0
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.