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

Dica

Esse conteúdo é um trecho do eBook da Arquitetura de Microsserviços do .NET para os Aplicativos .NET em Contêineres, 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.

Em 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 daquela agregação.

Entidades de domínio devem ser sempre entidades válidas. Existe um determinado número de invariáveis para um objeto que devem ser sempre verdadeiras. Por exemplo, um objeto de item do pedido sempre deve ter uma quantidade que deve ser um inteiro positivo, além de um nome de artigo e preço. Portanto, a imposição de invariáveis é de responsabilidade das entidades de domínio (especialmente da raiz de agregação) e um objeto de entidade não deve ser capaz de existir sem ser válido. Regras invariáveis simplesmente são expressas como contratos e exceções ou notificações são geradas quando elas são violadas.

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

Vamos propor que agora temos um SendUserCreationEmailService que usa um UserProfile..., como podemos racionalizar nesse serviço que o Nome não é nulo? Podemos verificar novamente? Ou, mais provável… você apenas não se preocupa em verificar e "espera pelo melhor": você espera que alguém tenha se preocupado em validá-lo antes de enviá-lo a você. É claro que, usando TDD, um dos primeiros testes que devemos escrever é que, se eu enviar um cliente com um nome nulo, isso deverá gerar um erro. Mas depois de começar a escrever esses tipos de testes repetidamente, percebemos: "e se nunca fosse permitido que o nome ficasse nulo? Não teríamos todos esses testes".

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

As validações normalmente são implementadas em construtores de entidade de domínio ou em métodos que podem atualizar a entidade. Existem várias maneiras de implementar validações, como verificar dados e aumentar exceções se a validação falhar. Também há padrões mais avançados, como usar o padrão de 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 conforme ela ocorre.

Validar condições e gerar 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ê encontrará 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 seria demonstrar a necessidade de garantir que o estado interno não tenha mudado ou que todas as mutações para um método ocorreram. Por exemplo, a implementação a seguir deixará 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á terão sido 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 quando for 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 necessários, pode ser usado para configurar propriedades de campo de banco de dados do EF Core, conforme explicado em detalhes na seção Mapeamento de tabela, mas elas não funcionam mais para validação de entidade no EF Core (o método IValidatableObject.Validate também não funciona mais para isso) como ocorria desde o EF 4.x no .NET Framework.

Anotações de dados e a interface IValidatableObject ainda podem ser usados para validação do modelo durante o model binding, antes da invocação de ações do controlador como de costume, mas esse modelo deve ser um ViewModel ou um DTO e essa é uma questão do MVC ou API e não do 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 a validação se as 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 o model binding, antes da invocação da ação, e você poderá verificar a propriedade ModelState.IsValid do controlador para saber o resultado. Mas agora isso acontece no controlador e não antes da persistência do objeto de entidade no DbContext, como ocorria desde o EF 4.x.

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

Você pode ver um exemplo de implementação para validar entidades IValidatableObjectneste comentário no GitHub. Esse exemplo não faz validações baseadas em atributo, mas deve ser fácil de ser implementado usando a reflexão na mesma substituição.

No entanto, do ponto de vista de DDD, o modelo de domínio fica mais 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 aceitem a entrada para permitir a validação do modelo na camada da interface do usuário. No entanto, isso não deve ser feito na exclusão de validação dentro do modelo de domínio.

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

Por fim, uma abordagem mais elaborada para implementar a validação no modelo de domínio é implementando o padrão de 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, validação manual com instruções de controle, mas usando o padrão de Notificação para empilhar e retornar uma lista de erros de validação.

Usar a validação adiada no domínio

Existem várias abordagens para lidar com validações adiadas no domínio. Em seu livro Implementing Domain-Driven Design (Implementando design controlado por domínio), Vaughn Vernon discute isso na seção sobre a validação.

Validação de duas etapas

Considere também a validação de duas etapas. Use a validação em nível de campo em seu comando de DTOs (Objetos de Transferência de Dados) 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 tornar mais fácil lidar com os 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 estar do lado do servidor e do lado do cliente no caso de DTOs (comandos e ViewModels, por exemplo).

Recursos adicionais