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

Dica

Esse conteúdo é um trecho do eBook, arquitetura de microsserviços do .NET para aplicativos .NET em contêineres, disponível em do .NET Docs ou como um PDF para download gratuito que pode ser lido offline.

miniatura da capa do eBook sobre arquitetura de microsserviços do .NET para aplicativos .NET em contêineres.

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

Estrutura de modelo de domínio em uma biblioteca personalizada do .NET Standard

A organização de pastas usada para o aplicativo de referência eShopOnContainers demonstra o modelo DDD para o aplicativo. Você pode achar que uma organização de pastas diferente comunica mais claramente as opções de design feitas para seu aplicativo. Como você pode ver na Figura 7-10, no modelo de domínio de ordenação há duas agregações, a agregação do pedido e a agregação do comprador. Cada agregado é um grupo de entidades de domínio e objetos de valor, embora você possa ter um agregado composto por uma única entidade de domínio, a entidade raiz.

Captura de tela do projeto Ordering.Domain no Gerenciador de Soluções.

A exibição do Gerenciador de Soluções para o projeto Ordering.Domain, mostrando a pasta AggregatesModel que contém 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 de modelo de domínio para o microsserviço de ordenação 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 camadas de infraestrutura, para que a camada de modelo de domínio não seja "contaminada" pela 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.

Estruturar agregações em uma biblioteca .NET Standard 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 entidade raiz ou raiz agregada), junto com quaisquer objetos de valor adicionais.

A consistência transacional significa que uma agregação tem a garantia de ser consistente e atualizada no final de uma ação comercial. Por exemplo, o agregado de pedidos do modelo de domínio do microsserviço de pedidos eShopOnContainers é composto conforme mostrado na Figura 7-11.

Captura de tela da pasta OrderAggregate e suas classes.

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

Figura 7-11. A agregação de pedidos na solução do Visual Studio

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

Implementar entidades de domínio como classes POCO

Implemente 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 de agregação. 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 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 observar que essa é uma entidade de domínio implementada como uma classe POCO. Ele não tem nenhuma dependência direta no Entity Framework Core ou em qualquer outra estrutura de infraestrutura. Essa implementação é como deveria 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 de agregação.

Às vezes, uma interface de marcador é considerada como um antipadrã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 de agregação significa que a maior parte do código relacionado à consistência e às regras de negócio das entidades do agregado deve ser implementada como métodos na classe raiz de agregação Order (por exemplo, AddOrderItem ao adicionar um objeto OrderItem ao agregado). 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 entidades filho.

Encapsular dados nas Entidades de Domínio

Um problema comum em modelos de entidade é que eles expõem as propriedades de navegação da 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 definem maneiras como os clientes podem manipulá-los.

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 invariáveis de domínio de negócios e lógica especificadas dentro dos 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 filho será distribuída pela camada de aplicativo (manipuladores de comando e controladores de API Web).

Se você contornar a raiz de agregação, a raiz de agregação não poderá garantir suas invariáveis, sua validade ou sua consistência. Por fim, 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 orientadas por métodos explícitos, utilizando uma linguagem consistente que descreva claramente a alteração que estão realizando na entidade.

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

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 filho deve ser executada por meio dos métodos na classe de entidade. Isso mantém a consistência de forma controlada e orientada a objetos em vez de implementar o código de script transacional.

O snippet de código a seguir mostra a maneira adequada 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ógica relacionadas à criação de um objeto OrderItem estará sob o controle da raiz do agregado 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 chamadas para AddOrderItem várias vezes. Nesse método, você pode examinar os itens do 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 a ID do produto for a mesma, 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 da raiz de agregação de Ordem. Portanto, a maioria da lógica ou validações relacionadas a essa operação (especialmente qualquer coisa que impacte a consistência entre outras entidades filhas) estará em um único lugar dentro da raiz do agregado. Esta é a principal finalidade do padrão de raiz de agregação.

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 de propriedades. Isso é útil ao proteger as coleções de entidades filho ou objetos de valor. Com esse aprimoramento, você pode usar campos privados simples em vez de propriedades e 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 invariável e a consistência dos dados, de modo que as propriedades sejam definidas apenas com um acessador get. As propriedades são apoiadas por campos privados. Membros privados só pode ser acessados de dentro da classe. No entanto, há uma exceção: o EF Core também precisa definir esses campos (para que ele possa retornar o objeto com os valores adequados).

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

Mapear 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 no 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 de infraestrutura e persistência.

Quando você utiliza o EF Core 1.0 ou versões posteriores, dentro do DbContext, é necessário 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 precisam ser acessados 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 utilizado nos métodos do pedido, mas também precisa ser armazenado no banco de dados. Portanto, no EF Core (desde v1.1), há uma maneira de mapear um campo sem uma propriedade relacionada a uma coluna no banco de dados. Isso também é explicado na seção Camada de infraestrutura deste guia.

Recursos adicionais