Partilhar via


Validações de design na camada de modelo de domínio

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.

No DDD, as regras de validação podem ser consideradas invariantes. A principal responsabilidade de um agregado é impor invariantes em todas as mudanças de estado para todas as entidades dentro desse agregado.

As entidades de domínio devem ser sempre entidades válidas. Há um certo número de invariantes para um objeto que deve ser sempre verdadeiro. Por exemplo, um objeto de item de ordem sempre precisa ter uma quantidade que deve ser um inteiro positivo, além de um nome de artigo e preço. Portanto, a imposição de invariantes é de responsabilidade das entidades de domínio (especialmente da raiz agregada) e um objeto de entidade não deve ser capaz de existir sem ser válido. As regras invariantes são simplesmente expressas como contratos, e exceções ou notificações são levantadas quando são violadas.

O raciocínio por trás disso é que muitos bugs ocorrem porque os objetos estão em um estado em que nunca deveriam ter estado.

Vamos propor que agora temos um SendUserCreationEmailService que leva um UserProfile ... como podemos racionalizar nesse serviço que Name não é nulo? Verificamos novamente? Ou mais provável... Você simplesmente não se preocupa em verificar e "esperar pelo melhor" – você espera que alguém se incomodou em validá-lo antes de enviá-lo para você. Claro, usando TDD um dos primeiros testes que devemos escrever é que se eu enviar um cliente com um nome nulo que ele deve gerar um erro. Mas uma vez que começamos a escrever este tipo de testes repetidamente, percebemos ... "E se nunca permitíssemos que o nome se tornasse nulo? não teríamos todos esses testes!".

Implementar validações na camada de modelo de domínio

As validações geralmente são implementadas em construtores de entidade de domínio ou em métodos que podem atualizar a entidade. Há várias maneiras de implementar validações, como verificar dados e gerar exceções se a validação falhar. Há também padrões mais avançados, como o uso do padrão Especificação para validações e o padrão Notificação para retornar uma coleção de erros em vez de retornar uma exceção para cada validação à medida que ela ocorre.

Validar condições e lançar exceções

O exemplo de código a seguir mostra a abordagem mais simples para validação em uma entidade de domínio gerando uma exceção. Na tabela de referências no final desta seção, você pode ver links para implementações mais avançadas com base nos padrões que discutimos anteriormente.

public void SetAddress(Address address)
{
    _shippingAddress = address?? throw new ArgumentNullException(nameof(address));
}

Um exemplo melhor demonstraria a necessidade de garantir que o estado interno não mudasse ou que todas as mutações de um método ocorressem. Por exemplo, a seguinte implementação deixaria o objeto em um estado inválido:

public void SetAddress(string line1, string line2,
    string city, string state, int zip)
{
    _shippingAddress.line1 = line1 ?? throw new ...
    _shippingAddress.line2 = line2;
    _shippingAddress.city = city ?? throw new ...
    _shippingAddress.state = (IsValid(state) ? state : throw new …);
}

Se o valor do estado for inválido, a primeira linha de endereço e a cidade já foram alteradas. Isso pode tornar o endereço inválido.

Uma abordagem semelhante pode ser usada no construtor da entidade, gerando uma exceção para garantir que a entidade seja válida depois de criada.

Usar atributos de validação no modelo com base em anotações de dados

As anotações de dados, como os atributos Required ou MaxLength, podem ser usadas para configurar as propriedades de campo do banco de dados EF Core, conforme explicado em detalhes na seção Mapeamento de tabela, mas não funcionam mais para validação de entidade no EF Core (nem o método), como têm feito desde o IValidatableObject.Validate EF 4.x no .NET Framework.

As anotações de dados e a interface ainda podem ser usadas para validação de IValidatableObject modelo durante a vinculação de modelo, antes da invocação de ações do controlador, como de costume, mas esse modelo deve ser um ViewModel ou DTO e isso é uma preocupação de MVC ou API e não uma preocupação de modelo de domínio.

Tendo deixado clara a diferença conceitual, você ainda pode usar anotações de dados e IValidatableObject na classe de entidade para validação, se suas ações receberem um parâmetro de objeto de classe de entidade, o que não é recomendado. Nesse caso, a validação ocorrerá após a vinculação do modelo, pouco antes de invocar a ação e você pode verificar a propriedade ModelState.IsValid do controlador para verificar o resultado, mas, novamente, isso acontece no controlador, não antes de persistir o objeto de entidade no DbContext, como havia feito desde o EF 4.x.

Você ainda pode implementar a validação personalizada na classe de entidade usando anotações de dados e o IValidatableObject.Validate método, substituindo o método SaveChanges do DbContext.

Você pode ver um exemplo de IValidatableObject implementação para validar entidades neste comentário no GitHub. Esse exemplo não faz validações baseadas em atributos, mas elas devem ser fáceis de implementar usando reflexão na mesma substituição.

No entanto, do ponto de vista do DDD, o modelo de domínio é melhor mantido enxuto com o uso de exceções nos métodos de comportamento da sua entidade ou implementando os padrões de Especificação e Notificação para impor regras de validação.

Pode fazer sentido usar anotações de dados na camada de aplicativo em classes ViewModel (em vez de entidades de domínio) que aceitarão entrada, para permitir a validação do modelo dentro da camada da interface do usuário. No entanto, isso não deve ser feito com a exclusão da validação dentro do modelo de domínio.

Validar entidades implementando o padrão Especificação e o padrão Notificação

Finalmente, uma abordagem mais elaborada para implementar validações no modelo de domínio é implementando o padrão Especificação em conjunto com o padrão de Notificação, conforme explicado em alguns dos recursos adicionais listados posteriormente.

Vale a pena mencionar que você também pode usar apenas um desses padrões — por exemplo, validando manualmente com instruções de controle, mas usando o padrão Notification para empilhar e retornar uma lista de erros de validação.

Usar validação adiada no domínio

Existem várias abordagens para lidar com validações diferidas no domínio. Em seu livro Implementing Domain-Driven Design, Vaughn Vernon discute isso na seção sobre validação.

Validação em duas etapas

Considere também a validação em duas etapas. Use a validação em nível de campo em seu comando Data Transfer Objects (DTOs) e a validação em nível de domínio dentro de suas entidades. Você pode fazer isso retornando um objeto de resultado em vez de exceções para facilitar o tratamento dos erros de validação.

Usando a validação de campo com anotações de dados, por exemplo, você não duplica a definição de validação. A execução, no entanto, pode ser tanto do lado do servidor quanto do lado do cliente no caso de DTOs (comandos e ViewModels, por exemplo).

Recursos adicionais