CQRS em .NET: Guia Completo para Escalabilidade e Desempenho
CQRS: Desmistificando a Arquitetura para .NET
O Command Query Responsibility Segregation (CQRS), ou Segregação de Responsabilidade de Comando e Consulta, é um padrão de arquitetura que separa as operações de leitura (queries) das operações de escrita (commands) em um sistema. Essa separação oferece flexibilidade, escalabilidade e melhor desempenho, principalmente em aplicações complexas.
Por que Usar CQRS?
A principal motivação para usar CQRS é otimizar o desempenho e a escalabilidade. Em sistemas tradicionais, o mesmo modelo de dados é usado tanto para leitura quanto para escrita. Isso pode levar a:
- Concorrência: Contenção em bancos de dados devido a operações de leitura e escrita simultâneas.
- Complexidade: Modelos de dados inflados para atender a diferentes necessidades de leitura e escrita.
- Escalabilidade Limitada: Dificuldade em escalar a aplicação devido à sobrecarga no banco de dados.
CQRS resolve esses problemas separando as responsabilidades de leitura e escrita, permitindo que cada lado seja otimizado independentemente.
Arquitetura CQRS: Uma Visão Geral
No CQRS, a aplicação é dividida em duas partes principais:
- Command Side (Lado do Comando): Responsável por receber comandos (ações que modificam o estado do sistema), validar esses comandos e atualizar o modelo de domínio. O Command Side geralmente usa um banco de dados otimizado para escrita.
- Query Side (Lado da Consulta): Responsável por fornecer dados para a interface do usuário ou outros sistemas. O Query Side pode usar um banco de dados ou um modelo de dados diferente, otimizado para leitura. Frequentemente utiliza-se denormalização para otimizar as consultas.
Os dados são sincronizados entre o Command Side e o Query Side usando eventos. Quando um comando é executado com sucesso, ele emite um evento que é consumido pelo Query Side, que então atualiza seu modelo de dados.
Diagrama Simplificado
sequenceDiagram
participant Cliente
participant API
participant Command Handler
participant Banco de Dados (Escrita)
participant Event Bus
participant Query Handler
participant Banco de Dados (Leitura)
Cliente->>API: Envia Comando (Ex: Criar Produto)
API->>Command Handler: Roteia o Comando
Command Handler->>Banco de Dados (Escrita): Valida e Persiste a Alteração
Command Handler->>Event Bus: Publica Evento (Ex: ProdutoCriado)
Event Bus->>Query Handler: Notifica Evento
Query Handler->>Banco de Dados (Leitura): Atualiza Modelo de Leitura
Cliente->>API: Solicita Consulta (Ex: Listar Produtos)
API->>Query Handler: Roteia a Consulta
Query Handler->>Banco de Dados (Leitura): Recupera Dados
Query Handler-->>API: Retorna Dados
API-->>Cliente: Exibe Dados
Estrutura de Ficheiros em um Projeto CQRS .NET
A organização dos ficheiros é crucial para manter a clareza e a manutenibilidade em um projeto CQRS. Uma estrutura comum é a seguinte:
NomeProjeto/
├── Commands/
│ ├── CriarProdutoCommand.cs
│ ├── AtualizarProdutoCommand.cs
│ └── ...
├── CommandHandlers/
│ ├── CriarProdutoCommandHandler.cs
│ ├── AtualizarProdutoCommandHandler.cs
│ └── ...
├── Queries/
│ ├── ObterProdutoQuery.cs
│ ├── ListarProdutosQuery.cs
│ └── ...
├── QueryHandlers/
│ ├── ObterProdutoQueryHandler.cs
│ ├── ListarProdutosQueryHandler.cs
│ └── ...
├── Events/
│ ├── ProdutoCriadoEvent.cs
│ └── ProdutoAtualizadoEvent.cs
├── EventHandlers/
│ ├── ProdutoCriadoEventHandler.cs
│ └── ProdutoAtualizadoEventHandler.cs
├── Models/
│ ├── Produto.cs (Modelo de Domínio - Command Side)
│ └── ProdutoLeitura.cs (Modelo de Leitura - Query Side)
├── Repositories/
│ ├── IProdutoRepository.cs
│ ├── ProdutoRepository.cs
│ └── ...
├── Data/
│ ├── AppDbContext.cs
│ └── ...
└── ...
- Commands: Define as intenções de mudança de estado.
- CommandHandlers: Lógica para executar os comandos e interagir com o modelo de domínio.
- Queries: Define as solicitações de leitura.
- QueryHandlers: Lógica para executar as queries e retornar dados.
- Events: Define os eventos de domínio que ocorrem após a execução de comandos.
- EventHandlers: Lógica para reagir aos eventos, tipicamente atualizando o modelo de leitura.
- Models: Define os modelos de domínio (Command Side) e os modelos de leitura (Query Side).
- Repositories: Abstrai o acesso aos dados.
- Data: Contém a configuração do contexto de dados (Entity Framework, por exemplo).
Implementando CQRS em .NET: Um Exemplo Prático
Vamos considerar um exemplo simples de criar um produto em um sistema de e-commerce.
1. Definindo o Comando
public class CriarProdutoCommand
{
public string Nome { get; set; }
public string Descricao { get; set; }
public decimal Preco { get; set; }
}
2. Definindo o Handler do Comando
public class CriarProdutoCommandHandler : IRequestHandler<CriarProdutoCommand, Guid>
{
private readonly IProdutoRepository _produtoRepository;
public CriarProdutoCommandHandler(IProdutoRepository produtoRepository)
{
_produtoRepository = produtoRepository;
}
public async Task<Guid> Handle(CriarProdutoCommand command, CancellationToken cancellationToken)
{
var produto = new Produto
{
Id = Guid.NewGuid(),
Nome = command.Nome,
Descricao = command.Descricao,
Preco = command.Preco
};
await _produtoRepository.Adicionar(produto);
// Publicar evento (implementação omitida para brevidade)
return produto.Id;
}
}
3. Definindo a Query
public class ObterProdutoQuery
{
public Guid Id { get; set; }
}
4. Definindo o Handler da Query
public class ObterProdutoQueryHandler : IRequestHandler<ObterProdutoQuery, ProdutoLeitura>
{
private readonly IProdutoLeituraRepository _produtoLeituraRepository;
public ObterProdutoQueryHandler(IProdutoLeituraRepository produtoLeituraRepository)
{
_produtoLeituraRepository = produtoLeituraRepository;
}
public async Task<ProdutoLeitura> Handle(ObterProdutoQuery query, CancellationToken cancellationToken)
{
return await _produtoLeituraRepository.Obter(query.Id);
}
}
5. Registrando os Handlers (Usando MediatR)
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(CriarProdutoCommand).Assembly));
6. Utilizando os Handlers em um Controller
[ApiController]
[Route("api/[controller]")]
public class ProdutosController : ControllerBase
{
private readonly IMediator _mediator;
public ProdutosController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost]
public async Task<IActionResult> CriarProduto([FromBody] CriarProdutoCommand command)
{
var produtoId = await _mediator.Send(command);
return CreatedAtAction(nameof(ObterProduto), new { id = produtoId }, new { id = produtoId });
}
[HttpGet("{id:guid}")]
public async Task<IActionResult> ObterProduto(Guid id)
{
var produto = await _mediator.Send(new ObterProdutoQuery { Id = id });
if (produto == null)
{
return NotFound();
}
return Ok(produto);
}
}
Neste exemplo, utilizamos a biblioteca MediatR para simplificar a implementação do padrão CQRS, facilitando o envio de comandos e queries para seus respectivos handlers.
Considerações Finais
CQRS é um padrão poderoso, mas também complexo. Avalie cuidadosamente se os benefícios (escalabilidade, desempenho, flexibilidade) justificam a complexidade adicional em seu projeto. Para sistemas menores ou menos complexos, uma arquitetura mais simples pode ser mais adequada.
Lembre-se que CQRS frequentemente caminha lado a lado com o padrão Event Sourcing, mas não são sinônimos. É possível implementar CQRS sem Event Sourcing e vice-versa.
Ao implementar CQRS, invista tempo no planejamento da estrutura de arquivos e na definição clara dos comandos, queries e eventos. Isso facilitará a manutenção e a evolução do seu sistema a longo prazo.