Pular para o conteúdo
.NETDDD na prática: Onion e CQRS
Módulo 07Domain-Driven Design (DDD)

DDD na prática: Onion e CQRS

avancado 40 min de leitura·Atualizado · .NET 9
Resumo

DDD tático encaixa naturalmente na Onion Architecture (Módulo 03) — e conversa muito bem com o CQRS, que você verá no Módulo 08: o domínio no centro, sem dependências; a aplicação orquestra casos de uso; a infraestrutura implementa persistência. Um repositório por raiz de agregado.

1. Objetivos de Aprendizagem#

  • Posicionar entidades, VOs, agregados e repositórios nas camadas da Onion.
  • Definir um repositório por raiz de agregado.
  • Antever como DDD e CQRS se combinam: commands mutam via domínio; queries leem direto.

2. Pré-requisitos#

  • Onion Architecture (Aula 17.1) e Repository/Service (Aula 17.3).
  • Domínio rico (Aula 44.4).

A seção sobre CQRS é um preview do Módulo 08 (Tópicos Avançados) — você não precisa conhecê-lo ainda; ele volta lá em detalhe.

3. Conceito#

A Onion Architecture e o DDD são feitos um para o outro. As dependências apontam para dentro — o domínio não conhece ninguém:

  • Domain (centro): entidades, value objects, agregados e as interfaces de repositório (IProdutoRepository). Zero dependência de framework. É onde vivem as regras (Aula 44.4).
  • Application: casos de uso que orquestram o domínio — na prática, os handlers de command/query do CQRS (que você verá no Módulo 08). Dependem do domínio, não da infraestrutura.
  • Infrastructure: implementações concretas — ProdutoRepository com EF Core, mapeamento dos value objects (OwnsOne), DbContext. Depende do domínio (implementa as interfaces dele).

A regra um repositório por raiz de agregado vem daqui: repositórios carregam e salvam agregados inteiros pela raiz, nunca objetos internos soltos. Não existe IItemPedidoRepository — o item se salva através do Pedido.

Com CQRS, a divisão fica limpa:

  • Commands (escrita) carregam o agregado, chamam um método de domínio (que garante a invariante) e salvam. Passam pelo modelo rico.
  • Queries (leitura) podem pular o domínio e projetar direto para um DTO/SELECT otimizado — não há invariante a proteger numa leitura.

4. Mão na Massa#

Domain — a interface do repositório mora aqui (perto de quem ela serve):

C#
// Domain/Repositories/IProdutoRepository.cs
public interface IProdutoRepository
{
    Task<Produto?> ObterAsync(Guid id, CancellationToken ct = default);
    Task SalvarAsync(Produto produto, CancellationToken ct = default);
}

Application — um command handler (estilo MediatR, detalhado no Módulo 08) orquestra:

C#
// Application/Produtos/ReajustarPreco.cs
public record ReajustarPrecoCommand(Guid ProdutoId, decimal Percentual) : IRequest;

public class ReajustarPrecoHandler : IRequestHandler<ReajustarPrecoCommand>
{
    private readonly IProdutoRepository _repo;
    public ReajustarPrecoHandler(IProdutoRepository repo) => _repo = repo;

    public async Task Handle(ReajustarPrecoCommand cmd, CancellationToken ct)
    {
        var produto = await _repo.ObterAsync(cmd.ProdutoId, ct)
            ?? throw new KeyNotFoundException();
        produto.Reajustar(cmd.Percentual);     // regra no domínio (Aula 44.4)
        await _repo.SalvarAsync(produto, ct);
    }
}

Infrastructure — EF Core implementa a interface e mapeia o value object:

C#
// Infrastructure/Persistence/ProdutoRepository.cs
public class ProdutoRepository : IProdutoRepository
{
    private readonly CatalogDbContext _db;
    public ProdutoRepository(CatalogDbContext db) => _db = db;

    public Task<Produto?> ObterAsync(Guid id, CancellationToken ct = default) =>
        _db.Produtos.FirstOrDefaultAsync(p => p.Id == id, ct);

    public async Task SalvarAsync(Produto produto, CancellationToken ct = default)
    {
        _db.Produtos.Update(produto);
        await _db.SaveChangesAsync(ct);
    }
}

// Value object Dinheiro mapeado como owned type
public class CatalogDbContext : DbContext
{
    public DbSet<Produto> Produtos => Set<Produto>();

    protected override void OnModelCreating(ModelBuilder b) =>
        b.Entity<Produto>().OwnsOne(p => p.Preco);   // Preco.Valor / Preco.Moeda
}

A leitura, por CQRS, não precisa do agregado — projeta direto:

C#
// Application/Produtos/ObterCatalogo.cs (query)
var itens = await _db.Produtos
    .Select(p => new ProdutoDto(p.Id, p.Nome, p.Preco.Valor, p.Preco.Moeda))
    .ToListAsync(ct);

5. Exemplo Completo#

O fluxo de uma escrita: Controller → envia ReajustarPrecoCommand (MediatR) → Handler carrega o Produto pelo IProdutoRepository → chama produto.Reajustar() (invariante garantida no domínio) → repositório salva o agregado. A regra está no centro; as bordas (HTTP, EF) só entram e saem. Comparado à Aula 17.3, o Service deixa de conter regra e vira orquestração fina.

6. Boas Práticas e Armadilhas#

FaçaEvite
Interface de repositório no Domain; implementação na InfraDbContext/EF vazando para o domínio
Um repositório por raiz de agregadoRepositório para entidade interna (ItemPedido)
Query lendo direto para DTOCarregar o agregado só para ler e devolver
Atenção

não transforme o repositório num CRUD genérico IRepository<T> que expõe IQueryable. Isso fura a fronteira do agregado e deixa a query montar estados que o domínio nunca permitiria.

7. Segurança e Produção#

  • Com o domínio isolado, as regras não dependem de HTTP nem de banco: dá para testá-las com testes de unidade rápidos (Módulo 15), sem infraestrutura. Combine com concorrência otimista (Aula 44.3) na raiz do agregado.

8. Exercícios#

  • Fácil: desenhe as três camadas e coloque Produto, IProdutoRepository, ProdutoRepository e o handler em cada uma.
  • Médio: escreva um AdicionarItemCommand + handler para o agregado Pedido (Aula 44.3).
  • Desafio: implemente uma query de catálogo paginada (Aula 30.1) que não passe pelo agregado e explique por quê isso é seguro.

9. Resumo#

DDD encaixa na Onion (domínio no centro, aplicação orquestra, infra implementa) e no CQRS (commands via domínio, queries diretas). Um repositório por raiz de agregado mantém as fronteiras — e as regras — no lugar certo.

10. Próximos Passos#

Você tem o mapa do DDD tático aplicado ao projeto. Reforce com os testes do Módulo 15 e, se o domínio crescer, estude domain events para coordenar mudanças entre agregados.

11. Referências#

44-05 — DDD na prática: Onion e CQRS | Curso ASP.NET Core