Compartilhar via


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

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.

No DDD, as regras de validação podem ser consideradas invariáveis. A principal responsabilidade de uma agregação é impor invariáveis entre as alterações de estado para todas as entidades dentro dessa agregação.

Entidades de domínio sempre devem ser entidades válidas. Há um certo número de invariáveis para um objeto que sempre deve ser verdadeiro. Por exemplo, um objeto de item de pedido sempre precisa ter uma quantidade que deve ser um inteiro positivo, além de um nome de artigo e um preço. Portanto, a imposição de invariantes é responsabilidade das entidades de domínio (especialmente da raiz do agregado), e um objeto de uma entidade não deve poder existir sem ser válido. Regras invariáveis são simplesmente expressas como contratos, e exceções ou notificações são geradas 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 estar.

Vamos sugerir que agora temos um serviço chamado SendUserCreationEmailService que utiliza um UserProfile... como podemos justificar nesse serviço que o Nome não seja nulo? Vamos verificar de novo? Ou mais provavelmente, você simplesmente não se dá ao trabalho de verificar e "espera que dê tudo certo", esperando que alguém tenha se incomodado em validá-lo antes de enviá-lo para você. Claro, ao usar TDD, um dos primeiros testes que devemos escrever é verificar se, ao enviar um cliente com um nome nulo, isso deve gerar um erro. Mas uma vez que começamos a escrever esses tipos de testes uma e outra vez percebemos... "E se nunca permitirmos que o nome se torne 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 de notificação para retornar uma coleção de erros em vez de retornar uma exceção para cada validação como ela ocorre.

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

O exemplo de código a seguir mostra a abordagem mais simples de 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 foi alterado ou que todas as mutações de um método ocorreram. Por exemplo, a implementação a seguir 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, criando 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

Anotações de dados, como os atributos Required ou MaxLength, podem ser usadas para configurar propriedades de campo de banco de dados EF Core, conforme explicado em detalhes na seção Mapeamento de Tabelas, mas não funcionam mais para validação de entidade no EF Core (da mesma forma que o método IValidatableObject.Validate), como era a partir do EF 4.x no .NET Framework.

As anotações de dados e a IValidatableObject interface ainda podem ser usadas para validação de modelo durante a associaçã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, não uma preocupação com o modelo de domínio.

Tendo esclarecido a diferença conceitual, você ainda poderá usar anotações de dados e IValidatableObject na classe de entidade para validação, caso suas ações recebam um parâmetro de objeto da classe de entidade, embora isso não seja recomendado. Nesse caso, a validação ocorrerá após a associação de 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 uma implementação de exemplo para validar IValidatableObject 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 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 a entrada, para permitir a validação do modelo dentro da camada da interface do usuário. No entanto, isso não deve ser feito em detrimento da validação dentro do modelo de domínio.

Validar entidades implementando o padrão especificação e o padrão de notificação

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

Vale 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 a validação adiada no domínio

Há várias abordagens para lidar com validações adiadas no domínio. Em seu livro Implementing Domain-Driven Design, Vaughn Vernon discute estes 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 nos Objetos de Transferência de Dados (DTOs) de comando 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, porém, pode ser do lado do servidor e do cliente no caso de DTOs (comandos e ViewModels, por exemplo).

Recursos adicionais