GB.
2025-07-238 min read

CQRS + MediatR in ASP.NET Core: A Practical Guide

#CQRS#MediatR#ASP.NET Core#Clean Architecture#Design Patterns#.NET

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 CRUDSolution with CQRS
Same model used for read & writeSeparate models optimized for each
Complex business logic mixed with queriesClean separation of concerns
Hard to scale reads independentlyReads can be optimized separately
Difficult to audit changesCommands 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:

  1. Controller receives HTTP request
  2. Creates a Command or Query object (simple DTO)
  3. Sends it to MediatR
  4. MediatR finds the correct Handler and executes it
  5. 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

ConceptPurpose
CommandChanges state (write)
QueryReads state (no side effects)
MediatRIn-process mediator that routes commands/queries to handlers
Pipeline BehaviorCross-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!