Projetar a camada de persistência da infraestrutura
Gorjeta
Este conteúdo é um trecho do eBook, .NET Microservices Architecture for Containerized .NET Applications, disponível no .NET Docs ou como um PDF para download gratuito que pode ser lido offline.
Os componentes de persistência de dados fornecem acesso aos dados hospedados dentro dos limites de um microsserviço (ou seja, o banco de dados de um microsserviço). Eles contêm a implementação real de componentes como repositórios e classes de Unidade de Trabalho, como objetos personalizados do Entity Framework (EF). DbContext O EF DbContext implementa os padrões Repository e Unit of Work.
O padrão do repositório
O padrão Repositório é um padrão de Design Controlado por Domínio destinado a manter as preocupações de persistência fora do modelo de domínio do sistema. Uma ou mais abstrações de persistência - interfaces - são definidas no modelo de domínio, e essas abstrações têm implementações na forma de adaptadores específicos de persistência definidos em outro lugar no aplicativo.
As implementações de repositório são classes que encapsulam a lógica necessária para acessar fontes de dados. Eles centralizam a funcionalidade comum de acesso a dados, proporcionando melhor manutenção e dissociando a infraestrutura ou tecnologia usada para acessar bancos de dados do modelo de domínio. Se você usar um Object-Relational Mapper (ORM) como o Entity Framework, o código que deve ser implementado será simplificado, graças ao LINQ e à digitação forte. Isso permite que você se concentre na lógica de persistência de dados em vez de no encanamento de acesso a dados.
O padrão Repository é uma maneira bem documentada de trabalhar com uma fonte de dados. No livro Patterns of Enterprise Application Architecture, Martin Fowler descreve um repositório da seguinte forma:
Um repositório executa as tarefas de um intermediário entre as camadas do modelo de domínio e o mapeamento de dados, agindo de forma semelhante a um conjunto de objetos de domínio na memória. Os objetos cliente criam consultas declarativamente e enviam-nas para os repositórios para obter respostas. Conceitualmente, um repositório encapsula um conjunto de objetos armazenados no banco de dados e operações que podem ser executadas neles, fornecendo uma maneira mais próxima da camada de persistência. Os repositórios, também, suportam o propósito de separar, claramente e em uma direção, a dependência entre o domínio de trabalho e a alocação ou mapeamento de dados.
Definir um repositório por agregação
Para cada raiz agregada ou agregada, você deve criar uma classe de repositório. Você pode aproveitar o C# Generics para reduzir o número total de classes concretas que você precisa manter (como demonstrado mais adiante neste capítulo). Em um microsserviço baseado em padrões DDD (Domain-Driven Design), o único canal que você deve usar para atualizar o banco de dados deve ser os repositórios. Isso ocorre porque eles têm uma relação um-para-um com a raiz agregada, que controla as invariantes do agregado e a consistência transacional. Não há problema em consultar o banco de dados através de outros canais (como você pode fazer seguindo uma abordagem CQRS), porque as consultas não alteram o estado do banco de dados. No entanto, a área transacional (ou seja, as atualizações) deve ser sempre controlada pelos repositórios e pelas raízes agregadas.
Basicamente, um repositório permite preencher dados na memória que vêm do banco de dados na forma de entidades de domínio. Uma vez que as entidades estão na memória, elas podem ser alteradas e, em seguida, persistidas de volta para o banco de dados por meio de transações.
Como observado anteriormente, se você estiver usando o padrão de arquitetura CQS/CQRS, as consultas iniciais serão realizadas por consultas laterais fora do modelo de domínio, executadas por instruções SQL simples usando o Dapper. Essa abordagem é muito mais flexível do que os repositórios porque você pode consultar e ingressar em quaisquer tabelas necessárias, e essas consultas não são restritas por regras das agregações. Esses dados vão para a camada de apresentação ou para o aplicativo cliente.
Se o usuário fizer alterações, os dados a serem atualizados virão do aplicativo cliente ou da camada de apresentação para a camada de aplicativo (como um serviço de API da Web). Ao receber um comando em um manipulador de comandos, você usa repositórios para obter os dados que deseja atualizar do banco de dados. Você atualiza-o na memória com os dados passados com os comandos e, em seguida, adiciona ou atualiza os dados (entidades de domínio) no banco de dados por meio de uma transação.
É importante enfatizar novamente que você deve definir apenas um repositório para cada raiz agregada, como mostra a Figura 7-17. Para atingir o objetivo da raiz agregada de manter a consistência transacional entre todos os objetos dentro da agregação, você nunca deve criar um repositório para cada tabela no banco de dados.
Figura 7-17. A relação entre repositórios, agregações e tabelas de banco de dados
O diagrama acima mostra as relações entre as camadas de Domínio e Infraestrutura: Buyer Aggregate depende do IBuyerRepository e Order Aggregate depende das interfaces IOrderRepository, essas interfaces são implementadas na camada Infrastructure pelos repositórios correspondentes que dependem do UnitOfWork, também implementado lá, que acessa as tabelas na camada de dados.
Impor uma raiz agregada por repositório
Pode ser valioso implementar o design do repositório de tal forma que ele imponha a regra de que apenas raízes agregadas devem ter repositórios. Você pode criar um tipo de repositório genérico ou base que restrinja o tipo de entidades com as quais ele trabalha para garantir que elas tenham a interface do IAggregateRoot
marcador.
Assim, cada classe de repositório implementada na camada de infraestrutura implementa seu próprio contrato ou interface, conforme mostrado no código a seguir:
namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Repositories
{
public class OrderRepository : IOrderRepository
{
// ...
}
}
Cada interface de repositório específica implementa a interface IRepository genérica:
public interface IOrderRepository : IRepository<Order>
{
Order Add(Order order);
// ...
}
No entanto, uma maneira melhor de fazer com que o código imponha a convenção de que cada repositório está relacionado a uma única agregação é implementar um tipo de repositório genérico. Dessa forma, fica explícito que você está usando um repositório para direcionar uma agregação específica. Isso pode ser feito facilmente implementando uma interface base genérica IRepository
, como no código a seguir:
public interface IRepository<T> where T : IAggregateRoot
{
//....
}
O padrão Repository facilita o teste da lógica do aplicativo
O padrão Repository permite que você teste facilmente seu aplicativo com testes de unidade. Lembre-se de que os testes de unidade apenas testam seu código, não a infraestrutura, portanto, as abstrações do repositório facilitam a consecução desse objetivo.
Como observado em uma seção anterior, é recomendável definir e colocar as interfaces do repositório na camada do modelo de domínio para que a camada de aplicativo, como o microsserviço da API da Web, não dependa diretamente da camada de infraestrutura na qual você implementou as classes de repositório reais. Ao fazer isso e usar a injeção de dependência nos controladores de sua API da Web, você pode implementar repositórios fictícios que retornam dados falsos em vez de dados do banco de dados. Essa abordagem dissociada permite criar e executar testes de unidade que focam a lógica do seu aplicativo sem exigir conectividade com o banco de dados.
As conexões com bancos de dados podem falhar e, mais importante, executar centenas de testes em um banco de dados é ruim por dois motivos. Primeiro, pode demorar muito tempo devido ao grande número de testes. Em segundo lugar, os registros de banco de dados podem mudar e afetar os resultados dos testes, especialmente se os testes estiverem sendo executados em paralelo, de modo que eles podem não ser consistentes. Os testes de unidade normalmente podem ser executados em paralelo; Os testes de integração podem não suportar a execução paralela, dependendo da sua implementação. O teste no banco de dados não é um teste de unidade, mas um teste de integração. Você deve ter muitos testes de unidade sendo executados rapidamente, mas menos testes de integração nos bancos de dados.
Em termos de separação de preocupações para testes de unidade, sua lógica opera em entidades de domínio na memória. Ele assume que a classe de repositório entregou isso. Uma vez que sua lógica modifica as entidades de domínio, ela assume que a classe de repositório irá armazená-las corretamente. O ponto importante aqui é criar testes de unidade em relação ao seu modelo de domínio e sua lógica de domínio. As raízes agregadas são os principais limites de consistência no DDD.
Os repositórios implementados no eShopOnContainers dependem da implementação DbContext do EF Core dos padrões Repository e Unit of Work usando seu rastreador de alterações, para que não dupliquem essa funcionalidade.
A diferença entre o padrão Repository e o padrão de classe de acesso a dados herdado (classe DAL)
Um objeto DAL típico executa diretamente operações de acesso e persistência de dados em relação ao armazenamento, geralmente no nível de uma única tabela e linha. Operações CRUD simples implementadas com um conjunto de classes DAL frequentemente não suportam transações (embora este nem sempre seja o caso). A maioria das abordagens de classe DAL faz uso mínimo de abstrações, resultando em acoplamento estreito entre o aplicativo ou classes BLL (Business Logic Layer) que chamam os objetos DAL.
Ao usar o repositório, os detalhes de implementação da persistência são encapsulados longe do modelo de domínio. O uso de uma abstração proporciona facilidade de estender o comportamento através de padrões como Decoradores ou Proxies. Por exemplo, preocupações transversais, como cache, registro em log e tratamento de erros, podem ser aplicadas usando esses padrões em vez de codificadas no próprio código de acesso a dados. Também é trivial oferecer suporte a vários adaptadores de repositório que podem ser usados em diferentes ambientes, desde o desenvolvimento local até ambientes de preparação compartilhados e produção.
Unidade de Execução de Trabalho
Uma unidade de trabalho refere-se a uma única transação que envolve várias operações de inserção, atualização ou exclusão. Em termos simples, isso significa que, para uma ação específica do usuário, como um registro em um site, todas as operações de inserção, atualização e exclusão são tratadas em uma única transação. Isso é mais eficiente do que lidar com várias operações de banco de dados de forma chattier.
Essas várias operações de persistência são executadas posteriormente em uma única ação quando o código da camada de aplicativo o comanda. A decisão sobre a aplicação das alterações na memória ao armazenamento real do banco de dados normalmente é baseada no padrão Unidade de Trabalho. No EF, o padrão de Unidade de Trabalho é implementado por a DbContext e é executado quando uma chamada é feita para SaveChanges
.
Em muitos casos, esse padrão ou forma de aplicar operações no armazenamento pode aumentar o desempenho do aplicativo e reduzir a possibilidade de inconsistências. Ele também reduz o bloqueio de transações nas tabelas do banco de dados, porque todas as operações pretendidas são confirmadas como parte de uma transação. Isso é mais eficiente em comparação com a execução de muitas operações isoladas no banco de dados. Portanto, o ORM selecionado pode otimizar a execução em relação ao banco de dados agrupando várias ações de atualização dentro da mesma transação, em oposição a muitas execuções de transação pequenas e separadas.
O padrão de Unidade de Trabalho pode ser implementado com ou sem o uso do padrão Repositório.
Os repositórios não devem ser obrigatórios
Os repositórios personalizados são úteis pelas razões citadas anteriormente, e essa é a abordagem para o microsserviço de pedidos no eShopOnContainers. No entanto, não é um padrão essencial para implementar em um design DDD ou mesmo no desenvolvimento .NET em geral.
Por exemplo, Jimmy Bogard, ao fornecer feedback direto para este guia, disse o seguinte:
Este será provavelmente o meu maior feedback. Eu realmente não sou um fã de repositórios, principalmente porque eles escondem os detalhes importantes do mecanismo de persistência subjacente. É por isso que eu vou para MediatR para comandos, também. Posso usar todo o poder da camada de persistência e empurrar todo esse comportamento de domínio para minhas raízes agregadas. Eu geralmente não quero zombar dos meus repositórios – eu ainda preciso ter esse teste de integração com a coisa real. Ir CQRS significava que não tínhamos mais necessidade de repositórios.
Os repositórios podem ser úteis, mas não são críticos para o seu design DDD da mesma forma que o padrão Aggregate e um modelo de domínio avançado. Portanto, use o padrão Repositório ou não, como achar melhor.
Recursos adicionais
Padrão do repositório
Edward Hieatt e Rob me. Padrão do repositório.
https://martinfowler.com/eaaCatalog/repository.htmlO padrão do repositório
https://learn.microsoft.com/previous-versions/msp-n-p/ff649690(v=pandp.10)Eric Evans. Design orientado por domínio: Lidando com a complexidade no coração do software. (Livro; inclui uma discussão sobre o padrão do repositório)
https://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215/
Unidade de padrão de trabalho
Martin Fowler. Unidade de padrão de trabalho.
https://martinfowler.com/eaaCatalog/unitOfWork.htmlImplementando o repositório e a unidade de padrões de trabalho em um aplicativo MVC ASP.NET
https://learn.microsoft.com/aspnet/mvc/overview/older-versions/getting-started-with-ef-5-using-mvc-4/implementing-the-repository-and-unit-of-work-patterns-in-an-asp-net-mvc-application