CQRS com MediatR no .NET 8: Guia Prático para Arquiteturas Escaláveis
CQRS com MediatR no .NET 8: Guia Prático para Arquiteturas Escaláveis

## Por Que CQRS Ainda É Relevante em 2024

Se você já trabalhou em um sistema legado que cresceu sem estrutura, provavelmente já viu o caos de ter um único repositório gordo resolvendo leitura, escrita, regras de negócio e validação ao mesmo tempo. O CQRS — *Command Query Responsibility Segregation* — resolve exatamente esse problema ao separar de forma explícita as operações que **modificam estado** das que apenas **consultam dados**.

Combinado com o MediatR no .NET 8, esse padrão deixa de ser teoria e vira uma estrutura de código que qualquer desenvolvedor do time consegue seguir. Neste post, vou mostrar como aplicar isso na prática, com exemplos reais do tipo de código que usamos nos projetos da [MIT Desenvolvimento de Sistemas](https://mit.des.br).

## O Conceito em 30 Segundos

CQRS divide as operações em dois "lados":

- **Command**: qualquer operação que muda o estado do sistema (criar, atualizar, deletar). Retorna apenas confirmação ou erro.

- **Query**: qualquer operação que lê dados. Não muda nada, só retorna.

O MediatR atua como o intermediário — você dispara um *Command* ou uma *Query* sem precisar conhecer quem vai processar. É o padrão Mediator do GoF aplicado de forma elegante.

## Configurando o Projeto

Vamos partir de um projeto ASP.NET Core com Clean Architecture. Instale o MediatR:

```bash

dotnet add package MediatR

dotnet add package MediatR.Extensions.Microsoft.DependencyInjection

No `Program.cs`, registre o MediatR apontando para o assembly onde estão seus handlers:

```csharp

builder.Services.AddMediatR(cfg =>

    cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));

## Estrutura de Pastas Recomendada

Uma estrutura que funciona bem em projetos reais:

src/

├── Application/

│   ├── Commands/

│   │   ├── CreateOrder/

│   │   │   ├── CreateOrderCommand.cs

│   │   │   ├── CreateOrderCommandHandler.cs

│   │   │   └── CreateOrderCommandValidator.cs

│   ├── Queries/

│   │   ├── GetOrderById/

│   │   │   ├── GetOrderByIdQuery.cs

│   │   │   ├── GetOrderByIdQueryHandler.cs

│   │   │   └── GetOrderByIdResponse.cs

Cada operação vive em sua própria pasta. Isso elimina o problema clássico de repositórios com dezenas de métodos que ninguém sabe ao certo o que fazem.

## Criando um Command

Vamos criar um command para registrar um pedido:

```csharp

// CreateOrderCommand.cs

public record CreateOrderCommand(

    Guid CustomerId,

    List<OrderItemDto> Items

) : IRequest<Guid>;

Usando `record` com `IRequest<TResponse>`, indicamos que esse command retorna o ID do pedido criado. Simples, imutável, sem ambiguidade.

Agora o handler:

```csharp

// CreateOrderCommandHandler.cs

public class CreateOrderCommandHandler

    : IRequestHandler<CreateOrderCommand, Guid>

{

    private readonly IOrderRepository _repository;

    private readonly IUnitOfWork _unitOfWork;

    public CreateOrderCommandHandler(

        IOrderRepository repository,

        IUnitOfWork unitOfWork)

    {

        _repository = repository;

        _unitOfWork = unitOfWork;

    }

    public async Task<Guid> Handle(

        CreateOrderCommand request,

        CancellationToken cancellationToken)

    {

        var order = Order.Create(request.CustomerId, request.Items);

        await _repository.AddAsync(order, cancellationToken);

        await _unitOfWork.CommitAsync(cancellationToken);

        return order.Id;

    }

}

O handler tem **uma única responsabilidade**: processar o command. Sem lógica de apresentação, sem validação embutida, sem mistura de concerns.

## Criando uma Query

```csharp

// GetOrderByIdQuery.cs

public record GetOrderByIdQuery(Guid OrderId)

    : IRequest<OrderResponse?>;

// GetOrderByIdQueryHandler.cs

public class GetOrderByIdQueryHandler

    : IRequestHandler<GetOrderByIdQuery, OrderResponse?>

{

    private readonly IOrderReadRepository _readRepo;

    public GetOrderByIdQueryHandler(IOrderReadRepository readRepo)

    {

        _readRepo = readRepo;

    }

    public async Task<OrderResponse?> Handle(

        GetOrderByIdQuery request,

        CancellationToken cancellationToken)

    {

        return await _readRepo.GetByIdAsync(

            request.OrderId, cancellationToken);

    }

}

Repare que usei `IOrderReadRepository` — uma interface específica para leitura. Em projetos mais avançados, o lado de *query* pode até acessar uma base de dados separada ou uma view materializada. O CQRS abre essa porta sem que você precise refatorar tudo.

## Usando o Controller

O controller fica limpo, sem nenhuma lógica de negócio:

```csharp

[ApiController]

[Route("api/[controller]")]

public class OrdersController : ControllerBase

{

    private readonly IMediator _mediator;

    public OrdersController(IMediator mediator)

    {

        _mediator = mediator;

    }

    [HttpPost]

    public async Task<IActionResult> Create(

        [FromBody] CreateOrderCommand command)

    {

        var orderId = await _mediator.Send(command);

        return CreatedAtAction(nameof(GetById),

            new { id = orderId }, null);

    }

    [HttpGet("{id:guid}")]

    public async Task<IActionResult> GetById(Guid id)

    {

        var order = await _mediator.Send(

            new GetOrderByIdQuery(id));

        return order is null

            ? NotFound()

            : Ok(order);

    }

}

O controller só delega. Ele não sabe como o pedido é criado — e não precisa saber.

## Pipeline Behaviors: O Poder Real do MediatR

Aqui está onde o MediatR vai além do simples despacho de mensagens. Os *Pipeline Behaviors* permitem adicionar comportamentos transversais — logging, validação, cache — sem modificar os handlers.

Exemplo de behavior de validação com FluentValidation:

```csharp

public class ValidationBehavior<TRequest, TResponse>

    : IPipelineBehavior<TRequest, TResponse>

    where TRequest : IRequest<TResponse>

{

    private readonly IEnumerable<IValidator<TRequest>> _validators;

    public ValidationBehavior(

        IEnumerable<IValidator<TRequest>> validators)

    {

        _validators = validators;

    }

    public async Task<TResponse> Handle(

        TRequest request,

        RequestHandlerDelegate<TResponse> next,

        CancellationToken cancellationToken)

    {

        if (!_validators.Any()) return await next();

        var context = new ValidationContext<TRequest>(request);

        var failures = _validators

            .SelectMany(v => v.Validate(context).Errors)

            .Where(f => f != null)

            .ToList();

        if (failures.Any())

            throw new ValidationException(failures);

        return await next();

    }

}

Registre no container:

```csharp

builder.Services.AddTransient(

    typeof(IPipelineBehavior<,>),

    typeof(ValidationBehavior<,>));

Pronto. Toda request que passar pelo MediatR vai ser validada automaticamente, sem uma linha a mais nos handlers.

## Quando Usar (e Quando Não Usar)

**Use CQRS quando:**

- O sistema tem leituras e escritas com complexidades muito diferentes

- Você precisa escalar leitura e escrita de forma independente

- O domínio é rico e as regras de negócio são complexas

- Está construindo microsserviços ou uma aplicação com múltiplos times

**Evite quando:**

- O sistema é um CRUD simples sem regras de negócio

- O time é pequeno e a sobrecarga de estrutura não compensa

- O prazo não permite o investimento inicial em organização

## Conclusão

CQRS com MediatR não é bala de prata, mas quando aplicado no contexto certo, transforma a manutenibilidade do código. A separação entre commands e queries forçou nosso time a pensar melhor nas responsabilidades de cada operação, reduziu acoplamento e facilitou a escrita de testes unitários.

Nos projetos **BigSchool** e **MITransport** da [MIT Desenvolvimento de Sistemas](https://mit.des.br), esse padrão nos permitiu evoluir features de forma independente sem quebrar o que já estava funcionando.

Se você quer aprofundar em Clean Architecture, microsserviços com .NET 8 ou precisa de consultoria em arquitetura de software para o seu projeto, entre em contato com a **MIT Desenvolvimento de Sistemas**: [mit.des.br](https://mit.des.br). Transformamos complexidade em código que escala.

*Publicado por Gil Robson de Sá — Tech Lead e Arquiteto de Software com mais de 35 anos de experiência. Conecte-se no [LinkedIn](https://linkedin.com/in/gilrobson).*

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *