Imagem do post CQRS em .NET: Guia Completo para Escalabilidade e Desempenho

CQRS em .NET: Guia Completo para Escalabilidade e Desempenho

Rikas98 17 de junho de 2025

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.