CQRS singifica Command Query Responsibility Segragation. O objetivo principal é separar as operações de leitura (Queries) das operações de escritas (Commands) em modelos distintos. (Em APIs, essa separação costuma se refletir em endpoints distintos, mas não é obrigatório)
Em sistemas tradicionais (CRUD padrão), usamos a mesma classe, entidade e até estrutura de banco, tanto pra salvar quanto pra consultar dados. É simples no começo, mas com o tempo gera acoplamento e limitações.
Antes de entendermos a resolução de problema proposto pelo CQRS é fundamental compreeendermos a diferença entre leitura e escrita aplicados a contextos de sistemas:
Se você utilizar os mesmos modelos, como proposto pelos sistemas tradicionais (CRUD), ele tentará servir dois propósitos ao mesmo tempo, o que deixa pesado e confuso.
O CQRS surge da evolução do CQS (Command Query Separation). Que diz que: “Um método deve ou não alterar o estado do sistema (Command) ou retornar um valor (Query), mas nunca fazer as duas coisas”, portanto, o CQRS leva a ideia além, não apenas a nível de método, mas a nível de arquitetura.
Os principais benefícios do CQRS são:
Como já vimos anteriormente, em uma aplicação CQRS temos dois lados bem definidos. Lado da escrita (Command) e Lado da Leitura (Query).
Responsável por alterar o estado do sistema. Aqui moram as regras de negócios, as validações e o modelo de domínio.
CriarPedidoCommand)public class CriarPedidoCommand
{
public Guid ClienteId { get; set; }
public List<Guid> Itens { get; set; }
}
public class CriarPedidoHandler {
public void Handle(CriarPedidoCommand command)
{
var pedido = new Pedido(command.ClienteId, command.Itens);
pedido.Validar();
_repo.Salvar(pedido);
}
}
Responsável por consultar os dado sem alterar o estado. Aqui o objetivo é simplicidade, rapidez e perfomance
GetPedidosPorClienteQuery)public class GetPedidosPorClienteQuery
{
public Guid ClienteId { get; set; }
}
public class GetPedidosPorClienteHandler
{
public IEnumerable<PedidoResumoDto> Handle(GetPedidosPorClienteQuery query)
{
return _context.Pedidos
.Where(p => p.ClienteId == query.ClienteId)
.Select(p => new PedidoResumoDto(p.Id, p.Total, p.Status));
}
}
Na prática, em .NET o CQRS é quase sempre utilizado com o padrão **Mediator.** No .NET moderno, esse padrão é muito utilizado através da biblioteca MediatR.
Como já sabemos, no CQRS separamos comandos (escritas) e consultas (leituras)… Mas quem orquestra essas operações? É nesse momento que entra o Mediator. Ele age como ponto central por onde os commands e queries passam. Desse modo, os controladores (controllers) não precisam conhecer diretamente os handlers.
IMediator).Handler correspondente e o executa[Cliente / Front-end]
|
| HTTP POST /api/pedidos
v
[PedidoController]
| Recebe CriarPedidoCommand
v
[Mediator (IMediator)]
| Localiza o handler correto (CriarPedidoHandler)
v
[CriarPedidoHandler]
| Executa lógica de criação do pedido
| (gera Guid, salva no banco, etc.)
v
[Mediator]
| Retorna resultado (Guid do pedido)
v
[PedidoController]
| Retorna Ok(Guid) para o cliente
v
[Cliente / Front-end]
Application/
├── Commands/
│ ├── CriarPedido/
│ │ ├── CriarPedidoCommand.cs
│ │ └── CriarPedidoHandler.cs
├── Queries/
│ ├── GetPedidosPorCliente/
│ │ ├── GetPedidosPorClienteQuery.cs
│ │ └── GetPedidosPorClienteHandler.cs
└── Behaviors/ (opcional: logs, validação etc.)
A seguir, irei apresentar um bloco de código contendo exemplo real do mediator e logo após sua aplicação em detalhes.
// Command
public record CriarPedidoCommand(Guid ClienteId, decimal ValorTotal) : IRequest<Guid>;
// Handler
public class CriarPedidoHandler : IRequestHandler<CriarPedidoCommand, Guid>
{
public Task<Guid> Handle(CriarPedidoCommand request, CancellationToken cancellationToken)
{
var pedidoId = Guid.NewGuid();
Console.WriteLine($"Pedido criado para cliente {request.ClienteId} com valor {request.ValorTotal}");
return Task.FromResult(pedidoId);
}
}
// Controller
[ApiController]
[Route("api/pedidos")]
public class PedidoController : ControllerBase
{
private readonly IMediator _mediator;
public PedidoController(IMediator mediator) => _mediator = mediator;
[HttpPost]
public async Task<IActionResult> CriarPedido([FromBody] CriarPedidoCommand command)
{
var id = await _mediator.Send(command);
return Ok(id);
}
}
public record CriarPedidoCommand(Guid ClienteId, decimal ValorTotal) : IRequest<Guid>;
record é uma forma imutável de criar um tipo simples no C#.IRequest<Guid> vem do MediatR e indica que esse comando espera como resposta um Guid (o ID do novo pedido).public class CriarPedidoHandler : IRequestHandler<CriarPedidoCommand, Guid>
{
public Task<Guid> Handle(CriarPedidoCommand request, CancellationToken cancellationToken)
{
var pedidoId = Guid.NewGuid();
Console.WriteLine($"Pedido criado para cliente {request.ClienteId} com valor {request.ValorTotal}");
return Task.FromResult(pedidoId);
}
}
Esse é o módulo que realmente executa o comando.
IRequestHandler<TRequest, TResponse>. No caso, lida com CriarPedidoCommand e retorna um Guid.Handle é o ponto onde a ação acontece:Ou seja: o handler é o executador da intenção.
[ApiController]
[Route("api/pedidos")]
public class PedidoController : ControllerBase
{
private readonly IMediator _mediator;
public PedidoController(IMediator mediator) => _mediator = mediator;
[HttpPost]
public async Task<IActionResult> CriarPedido([FromBody] CriarPedidoCommand command)
{
var id = await _mediator.Send(command);
return Ok(id);
}
}
O controller recebe a requisição HTTP (ex: via Postman ou front-end).
CriarPedidoCommand no corpo da requisição._mediator.Send(command), o Mediator encontra o handler certo e executa.id) e devolve no Ok(id).O controller não sabe qual handler executa o comando, só fala com o Mediator. Isso mantém o baixo acoplamento e deixa a aplicação mais organizada.