Partilhar via


Implementar um modelo de domínio de microsserviço com .NET

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.

.NET Microservices Architecture for Containerized .NET Applications eBook cover thumbnail.

Na seção anterior, os princípios e padrões de design fundamentais para projetar um modelo de domínio foram explicados. Agora é hora de explorar possíveis maneiras de implementar o modelo de domínio usando .NET (código C# simples) e EF Core. O seu modelo de domínio será composto simplesmente pelo seu código. Ele terá apenas os requisitos do modelo EF Core, mas não dependências reais do EF. Você não deve ter dependências rígidas ou referências ao EF Core ou a qualquer outro ORM em seu modelo de domínio.

Estrutura do modelo de domínio em uma biblioteca padrão do .NET personalizada

A organização de pasta usada para o aplicativo de referência eShopOnContainers demonstra o modelo DDD para o aplicativo. Você pode achar que uma organização de pasta diferente comunica mais claramente as escolhas de design feitas para seu aplicativo. Como você pode ver na Figura 7-10, no modelo de domínio de ordenação há dois agregados, o agregado de ordem e o agregado de comprador. Cada agregação é um grupo de entidades de domínio e objetos de valor, embora você possa ter uma agregação composta por uma única entidade de domínio (a raiz agregada ou entidade raiz) também.

Screenshot of the Ordering.Domain project in Solution Explorer.

A exibição Gerenciador de Soluções para o projeto Ordering.Domain, mostrando a pasta AggregatesModel contendo as pastas BuyerAggregate e OrderAggregate, cada uma contendo suas classes de entidade, arquivos de objeto de valor e assim por diante.

Figura 7-10. Estrutura do modelo de domínio para o microsserviço de pedidos no eShopOnContainers

Além disso, a camada de modelo de domínio inclui os contratos de repositório (interfaces) que são os requisitos de infraestrutura do seu modelo de domínio. Em outras palavras, essas interfaces expressam quais repositórios e os métodos que a camada de infraestrutura deve implementar. É fundamental que a implementação dos repositórios seja colocada fora da camada de modelo de domínio, na biblioteca de camada de infraestrutura, para que a camada de modelo de domínio não seja "contaminada" por API ou classes de tecnologias de infraestrutura, como o Entity Framework.

Você também pode ver uma pasta SeedWork que contém classes base personalizadas que você pode usar como base para suas entidades de domínio e objetos de valor, para que você não tenha código redundante na classe de objeto de cada domínio.

Agregações de estrutura em uma biblioteca padrão do .NET personalizada

Uma agregação refere-se a um cluster de objetos de domínio agrupados para corresponder à consistência transacional. Esses objetos podem ser instâncias de entidades (uma das quais é a raiz agregada ou entidade raiz) mais quaisquer objetos de valor adicional.

Consistência transacional significa que um agregado tem a garantia de ser consistente e atualizado no final de uma ação comercial. Por exemplo, a agregação de pedidos do modelo de domínio de microsserviço de pedidos eShopOnContainers é composta como mostra a Figura 7-11.

Screenshot of the OrderAggregate folder and its classes.

Uma exibição detalhada da pasta OrderAggregate: Address.cs é um objeto de valor, IOrderRepository é uma interface de repo, Order.cs é uma raiz agregada, OrderItem.cs é uma entidade filho e OrderStatus.cs é uma classe de enumeração.

Figura 7-11. A ordem agregada na solução Visual Studio

Se você abrir qualquer um dos arquivos em uma pasta agregada, poderá ver como ele é marcado como uma classe base personalizada ou interface, como entidade ou objeto de valor, conforme implementado na pasta SeedWork .

Implementar entidades de domínio como classes POCO

Você implementa um modelo de domínio no .NET criando classes POCO que implementam suas entidades de domínio. No exemplo a seguir, a classe Order é definida como uma entidade e também como uma raiz agregada. Como a classe Order deriva da classe base Entity, ela pode reutilizar o código comum relacionado a entidades. Tenha em mente que essas classes base e interfaces são definidas por você no projeto de modelo de domínio, portanto, é o seu código, não o código de infraestrutura de um ORM como o EF.

// COMPATIBLE WITH ENTITY FRAMEWORK CORE 5.0
// Entity is a custom base class with the ID
public class Order : Entity, IAggregateRoot
{
    private DateTime _orderDate;
    public Address Address { get; private set; }
    private int? _buyerId;

    public OrderStatus OrderStatus { get; private set; }
    private int _orderStatusId;

    private string _description;
    private int? _paymentMethodId;

    private readonly List<OrderItem> _orderItems;
    public IReadOnlyCollection<OrderItem> OrderItems => _orderItems;

    public Order(string userId, Address address, int cardTypeId, string cardNumber, string cardSecurityNumber,
            string cardHolderName, DateTime cardExpiration, int? buyerId = null, int? paymentMethodId = null)
    {
        _orderItems = new List<OrderItem>();
        _buyerId = buyerId;
        _paymentMethodId = paymentMethodId;
        _orderStatusId = OrderStatus.Submitted.Id;
        _orderDate = DateTime.UtcNow;
        Address = address;

        // ...Additional code ...
    }

    public void AddOrderItem(int productId, string productName,
                            decimal unitPrice, decimal discount,
                            string pictureUrl, int units = 1)
    {
        //...
        // Domain rules/logic for adding the OrderItem to the order
        // ...

        var orderItem = new OrderItem(productId, productName, unitPrice, discount, pictureUrl, units);

        _orderItems.Add(orderItem);

    }
    // ...
    // Additional methods with domain rules/logic related to the Order aggregate
    // ...
}

É importante notar que esta é uma entidade de domínio implementada como uma classe POCO. Ele não tem nenhuma dependência direta do Entity Framework Core ou de qualquer outra estrutura de infraestrutura. Esta implementação é como deve ser no DDD, apenas código C# implementando um modelo de domínio.

Além disso, a classe é decorada com uma interface chamada IAggregateRoot. Essa interface é uma interface vazia, às vezes chamada de interface de marcador, que é usada apenas para indicar que essa classe de entidade também é uma raiz agregada.

Uma interface de marcador é por vezes considerada como um anti-padrão; no entanto, também é uma maneira limpa de marcar uma classe, especialmente quando essa interface pode estar evoluindo. Um atributo pode ser a outra opção para o marcador, mas é mais rápido ver a classe base (Entity) ao lado da interface IAggregate em vez de colocar um marcador de atributo Aggregate acima da classe. É uma questão de preferências, em qualquer caso.

Ter uma raiz agregada significa que a maioria do código relacionado à consistência e às regras de negócios das entidades agregadas deve ser implementada como métodos na classe raiz agregada Order (por exemplo, AddOrderItem ao adicionar um objeto OrderItem à agregação). Você não deve criar ou atualizar objetos OrderItems de forma independente ou direta; a classe AggregateRoot deve manter o controle e a consistência de qualquer operação de atualização em relação às suas entidades filhas.

Encapsular dados nas Entidades de Domínio

Um problema comum em modelos de entidade é que eles expõem propriedades de navegação de coleção como tipos de lista acessíveis publicamente. Isso permite que qualquer desenvolvedor colaborador manipule o conteúdo desses tipos de coleção, o que pode ignorar regras de negócios importantes relacionadas à coleção, possivelmente deixando o objeto em um estado inválido. A solução para isso é expor o acesso somente leitura a coleções relacionadas e fornecer explicitamente métodos que definam maneiras pelas quais os clientes podem manipulá-las.

No código anterior, observe que muitos atributos são somente leitura ou privados e só são atualizáveis pelos métodos de classe, portanto, qualquer atualização considera invariantes de domínio de negócios e lógica especificada nos métodos de classe.

Por exemplo, seguindo padrões DDD, você não deve fazer o seguinte de qualquer método de manipulador de comando ou classe de camada de aplicativo (na verdade, deve ser impossível para você fazer isso):

// WRONG ACCORDING TO DDD PATTERNS – CODE AT THE APPLICATION LAYER OR
// COMMAND HANDLERS
// Code in command handler methods or Web API controllers
//... (WRONG) Some code with business logic out of the domain classes ...
OrderItem myNewOrderItem = new OrderItem(orderId, productId, productName,
    pictureUrl, unitPrice, discount, units);

//... (WRONG) Accessing the OrderItems collection directly from the application layer // or command handlers
myOrder.OrderItems.Add(myNewOrderItem);
//...

Nesse caso, o método Add é puramente uma operação para adicionar dados, com acesso direto à coleção OrderItems. Portanto, a maioria da lógica de domínio, regras ou validações relacionadas a essa operação com as entidades filhas serão espalhadas pela camada de aplicativo (manipuladores de comando e controladores de API da Web).

Se você contornar a raiz agregada, a raiz agregada não poderá garantir suas invariantes, sua validade ou sua consistência. Eventualmente, você terá código espaguete ou código de script transacional.

Para seguir padrões DDD, as entidades não devem ter setters públicos em nenhuma propriedade de entidade. As alterações em uma entidade devem ser conduzidas por métodos explícitos com linguagem ubíqua explícita sobre a mudança que estão realizando na entidade.

Além disso, as coleções dentro da entidade (como os itens de ordem) devem ser propriedades somente leitura (o método AsReadOnly explicado mais tarde). Você deve ser capaz de atualizá-lo somente de dentro dos métodos de classe raiz agregados ou dos métodos de entidade filha.

Como você pode ver no código para a raiz de agregação de ordem, todos os setters devem ser privados ou, pelo menos, somente leitura externamente, de modo que qualquer operação contra os dados da entidade ou suas entidades filhas deve ser executada por meio de métodos na classe de entidade. Isso mantém a consistência de forma controlada e orientada a objetos, em vez de implementar código de script transacional.

O trecho de código a seguir mostra a maneira correta de codificar a tarefa de adicionar um objeto OrderItem à agregação Order.

// RIGHT ACCORDING TO DDD--CODE AT THE APPLICATION LAYER OR COMMAND HANDLERS
// The code in command handlers or WebAPI controllers, related only to application stuff
// There is NO code here related to OrderItem object's business logic
myOrder.AddOrderItem(productId, productName, pictureUrl, unitPrice, discount, units);

// The code related to OrderItem params validations or domain rules should
// be WITHIN the AddOrderItem method.

//...

Neste trecho, a maioria das validações ou lógicas relacionadas à criação de um objeto OrderItem estará sob o controle da raiz de agregação Order — no método AddOrderItem — especialmente validações e lógica relacionadas a outros elementos na agregação. Por exemplo, você pode obter o mesmo item de produto como resultado de várias chamadas para AddOrderItem. Nesse método, você pode examinar os itens de produto e consolidar os mesmos itens de produto em um único objeto OrderItem com várias unidades. Além disso, se houver valores de desconto diferentes, mas o ID do produto for o mesmo, você provavelmente aplicará o desconto mais alto. Esse princípio se aplica a qualquer outra lógica de domínio para o objeto OrderItem.

Além disso, a nova operação OrderItem(params) também será controlada e executada pelo método AddOrderItem a partir da raiz agregada Order. Portanto, a maioria da lógica ou validações relacionadas a essa operação (especialmente qualquer coisa que afete a consistência entre outras entidades filhas) estará em um único lugar dentro da raiz agregada. Esse é o objetivo final do padrão de raiz agregada.

Quando você usa o Entity Framework Core 1.1 ou posterior, uma entidade DDD pode ser melhor expressa porque permite o mapeamento para campos , além das propriedades. Isso é útil ao proteger coleções de entidades filhas ou objetos de valor. Com esse aprimoramento, você pode usar campos privados simples em vez de propriedades e pode implementar qualquer atualização para a coleção de campos em métodos públicos e fornecer acesso somente leitura por meio do método AsReadOnly.

No DDD, você deseja atualizar a entidade somente por meio de métodos na entidade (ou no construtor) para controlar qualquer invariante e a consistência dos dados, para que as propriedades sejam definidas apenas com um acessor get. As propriedades são apoiadas por campos privados. Os membros privados só podem ser acedidos a partir da classe. No entanto, há uma exceção: o EF Core também precisa definir esses campos (para que possa retornar o objeto com os valores adequados).

Mapear propriedades com apenas obter acessadores para os campos na tabela do banco de dados

O mapeamento de propriedades para colunas de tabela de banco de dados não é uma responsabilidade de domínio, mas parte da camada de infraestrutura e persistência. Mencionamos isso aqui apenas para que você esteja ciente dos novos recursos do EF Core 1.1 ou posterior relacionados a como você pode modelar entidades. Detalhes adicionais sobre este tópico são explicados na seção infraestrutura e persistência.

Quando você usa o EF Core 1.0 ou posterior, dentro do DbContext você precisa mapear as propriedades que são definidas apenas com getters para os campos reais na tabela do banco de dados. Isso é feito com o método HasField da classe PropertyBuilder.

Mapear campos sem propriedades

Com o recurso no EF Core 1.1 ou posterior para mapear colunas para campos, também é possível não usar propriedades. Em vez disso, você pode apenas mapear colunas de uma tabela para campos. Um caso de uso comum para isso são campos privados para um estado interno que não precisa ser acessado de fora da entidade.

Por exemplo, no exemplo de código OrderAggregate anterior, há vários campos privados, como o _paymentMethodId campo, que não têm nenhuma propriedade relacionada para um setter ou getter. Esse campo também pode ser calculado dentro da lógica de negócios do pedido e usado a partir dos métodos do pedido, mas também precisa ser persistido no banco de dados. Portanto, no EF Core (desde a v1.1), há uma maneira de mapear um campo sem uma propriedade relacionada para uma coluna no banco de dados. Isso também é explicado na seção Camada de infraestrutura deste guia.

Recursos adicionais