Quando o CRUD comum deixa de funcionar: Introdução ao CQRS e Event Sourcing
\n\nImagine que você está escrevendo um sistema para um banco. A cada segundo, centenas de usuários verificam saldos, transferem dinheiro, abrem investimentos. Sua aplicação em C# funciona com o Entity Framework padrão e operações CRUD típicas. E de repente — o banco cai. Por quê? Porque um relatório pesado bloqueou a tabela enquanto uma transação estava sendo gravada. Familiar?
Desenvolvedores de sistemas complexos em C# mais cedo ou mais tarde encontram o limite da arquitetura clássica. Dois padrões poderosos vêm em socorro: CQRS (Command Query Responsibility Segregation) e Event Sourcing. Vamos analisá-los na prática — com exemplos reais, e não teoria seca.
\n\nO que é CQRS e por que não é "apenas separação"?
\n\nCQRS não é sobre colocar métodos em pastas diferentes. É uma filosofia: comandos (alteram o estado) e consultas (leem o estado) devem viver separadamente. No CRUD clássico, você tem um único modelo de dados tanto para leitura quanto para escrita. No CQRS — pelo menos dois.
O problema da abordagem clássica
\nSuponha que você tenha um UserRepository com o método GetUserById. Quando você lê um usuário para exibir na tela, precisa de 3 campos. Quando você atualiza — 10 campos e validações. Como resultado, um único modelo se transforma em um monstro com campos null e mapeadores confusos.
No CQRS, você separa as responsabilidades:
\n- \n
- Command — executa uma ação (ex.:
CreateOrderCommand) \n - Query — retorna dados (ex.:
GetOrderQuery) \n - Read Model — DTO simples, otimizado para uma tela específica \n
- Write Model — modelo de domínio rico com lógica de negócios \n
Escrevendo CQRS em C# com MediatR
\nA maneira mais popular de implementar CQRS em C# é usar a biblioteca MediatR. Ela implementa o padrão Mediator e se encaixa perfeitamente no conceito de comandos e consultas.
Passo 1: Definindo o comando
\npublic record CreateOrderCommand(\n Guid CustomerId,\n List<OrderItemDto> Items,\n string ShippingAddress\n) : IRequest<Guid>; // Retorna o ID do pedido criado\n\nPasso 2: Handler do comando
\npublic class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, Guid>\n{\n private readonly IOrderRepository _orderRepository;\n private readonly IUnitOfWork _unitOfWork;\n\n public CreateOrderCommandHandler(IOrderRepository orderRepository, IUnitOfWork unitOfWork)\n {\n _orderRepository = orderRepository;\n _unitOfWork = unitOfWork;\n }\n\n public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken cancellationToken)\n {\n // Lógica de negócios: verificação, validação, criação\n var order = new Order(request.CustomerId, request.ShippingAddress);\n \n foreach (var item in request.Items)\n {\n order.AddItem(item.ProductId, item.Quantity, item.Price);\n }\n\n await _orderRepository.AddAsync(order);\n await _unitOfWork.SaveChangesAsync(cancellationToken);\n \n return order.Id;\n }\n}\n\nPasso 3: Definindo a consulta
\npublic record GetOrderSummaryQuery(Guid OrderId) : IRequest<OrderSummaryDto>;\n\nPasso 4: Handler da consulta (lê de um banco separado!)
\npublic class GetOrderSummaryQueryHandler : IRequestHandler<GetOrderSummaryQuery, OrderSummaryDto>\n{\n private readonly IReadDbContext _readDbContext; // Contexto separado para leitura\n\n public GetOrderSummaryQueryHandler(IReadDbContext read