Unit of Work
Coordenando Transações e Persistências
Contexto e Problema Identificado
Analisando a API atual, observa-se a existência da camada de infraestrutura e dos repositórios, como o ProductRepository, que atualmente possui apenas operações de leitura por ID. Para complementar o CRUD, será implementado um método Create para criar novos produtos.
Implementação Inicial do Método Create
No ProductRepository, adiciona-se o seguinte método:
public async Task CreateAsync(Product product, CancellationToken cancellationToken = default)
{
await context.Products.AddAsync(product, cancellationToken);
await context.SaveChangesAsync(cancellationToken);
}Sua assinatura correspondente na interface IProductRepository:
Task CreateAsync(Product product, CancellationToken cancellationToken = default);Esta implementação segue o padrão estabelecido anteriormente, mas apresenta um ponto crítico de atenção.
O Problema do SaveChangesAsync em Métodos Isolados
A chamada explícita de SaveChangesAsync() dentro do método CreateAsync representa um problema arquitetural significativo.
Compreendendo o Ciclo de Vida do DbContext
Para entender completamente o problema, é fundamental compreender como o DbContext funciona no contexto de uma requisição HTTP:
Instância por requisição: Para cada requisição aberta, é criada uma instância única do DbContext
Escopo de vida: Esta instância persiste durante toda a duração da requisição
Compartilhamento: O mesmo DbContext é compartilhado entre todos os repositórios utilizados durante a requisição
Consequências da Persistência Precoce
Quando SaveChangesAsync() é executado em um repositório específico (por exemplo, no repositório de produtos), todas as entidades rastreadas por qualquer repositório que utilize o mesmo DbContext são afetadas.
Exemplo Prático:
Um repositório de produto adiciona um novo produto
Um repositório de categoria adiciona uma nova categoria
Um repositório de usuário atualiza informações do usuário
Se o
SaveChangesAsync()for chamado no repositório de produto, todas as três operações serão persistidas no banco de dados simultaneamente
Impacto no Desempenho
Executar SaveChangesAsync múltiplas vezes durante uma única requisição resulta em múltiplas viagens ao banco de dados ("descendo ao banco" múltiplas vezes), o que impacta significativamente o desempenho devido à latência de rede e sobrecarga de transações.
Solução: Padrão Unit of Work
Reestruturando o Repositório
A solução envolve remover a chamada explícita de SaveChangesAsync dos métodos do repositório, transferindo a responsabilidade de persistência para um componente coordenador.
Repositório modificado:
Agora o método apenas adiciona a entidade ao contexto em memória, sem persistir no banco de dados. Esta abordagem é aplicada a todas as operações do CRUD (Create, Read, Update, Delete).
Implementando o Caso de Uso Create
Cria-se um novo diretório Create no mesmo nível de GetById dentro de UseCases/Products, contendo os arquivos Command, Handler e Response.
Response:
Command:
Handler:
A injeção de IUnitOfWork permite que o handler controle quando as mudanças devem ser persistidas, separando a lógica de negócio da mecânica de persistência.
Definindo a Abstração Unit of Work
Interface IUnitOfWork
Cria-se a interface IUnitOfWork no projeto Domain, dentro de Abstractions:
Observação sobre Rollback: Em cenários típicos de aplicações web, não é necessário implementar explicitamente métodos de rollback. Quando o DbContext sai do escopo da requisição (seja por conclusão bem-sucedida ou por exceção), todas as mudanças não confirmadas são automaticamente descartadas, não persistindo nenhuma informação indesejada no banco de dados.
Escopos de Vida de Serviço no .NET
Implementação Concreta do UnitOfWork
A implementação concreta reside na camada de infraestrutura, no diretório de dados ao lado de AppDbContext:
Configuração de Dependências
Registro no Container de DI
A configuração de dependências é realizada na classe DependencyInjection da infraestrutura:
Justificativa dos escopos: Ambos IUnitOfWork e IProductRepository são registrados como Scoped para garantir que compartilhem a mesma instância de DbContext durante uma requisição, permitindo o funcionamento correto do padrão Unit of Work.
Expondo o Endpoint na API
Mapeamento da Rota POST
Adiciona-se o mapeamento para criação de produtos na API:
Testando a Implementação
Execução da Aplicação
Inicia-se a aplicação e o banco de dados:
Teste no Postman
Requisição POST:
Resposta (200 OK):
Verificação com GET:
Resposta (200 OK):
Boas Práticas e Considerações Adicionais
Padrões Recomendados
Injeção via Construtor: Sempre preferir injeção via construtor sobre propriedades para melhor testabilidade
Tratamento de Exceções: Implementar tratamento adequado para exceções de persistência
Transações Explícitas: Para operações complexas, considerar o uso de transações explícitas
Validações de Domínio: Executar validações antes do commit para evitar exceções do banco de dados
Exemplo Aprimorado do Handler
Conclusão
A implementação do padrão Unit of Work proporciona significativas melhorias arquiteturais:
Coordenação Centralizada: Todas as operações de persistência são coordenadas por um único ponto
Consistência Transacional: Garante que múltiplas operações sejam tratadas atomicamente
Otimização de Performance: Reduz o número de viagens ao banco de dados
Separação de Responsabilidades: Isola a lógica de persistência da lógica de negócio
Testabilidade Aprimorada: Facilita a simulação de operações de persistência em testes
A abordagem apresentada segue os princípios da Arquitetura Limpa, mantendo as camadas de aplicação e domínio independentes dos detalhes de implementação de infraestrutura, enquanto fornece um mecanismo robusto para gerenciamento transacional em aplicações empresariais.
Atualizado