ASP.NET Core - Adicionando Fluent Validation


  Este artigo vai apresentar algumas noções básicas sobre a sintaxe de validação fluente mostrando   como configurar validações fluentes em projetos ASP .NET Core Web API.

Existem várias maneiras pelas quais as validações de negócios são aplicadas. Alguns projetos podem ter usado vários padrões de design para criar camadas personalizadas, o que ajudaria a manter todas as validações em um só lugar.

A biblioteca Fluent Validations fornece uma maneira de organizar suas validações em um único local. Também ajuda a manter a sintaxe consistente em várias validações.

Vamos criar um projeto Web API usando o .NET 7.0 chamado ApiFluentValidation que será usado como exemplo.

Criando o projeto

Abra o VS 2022 e selecione o template ASP.NET Core Web API e informe o nome ApiFluentValidation definindo as seguintes configurações para o projeto:

Crie no projeto a pasta Entities e nesta pasta crie a classe Estudante :

public class Estudante
{
  
public int Id { get; set; }
  
public string Nome { get; set; } = null!;
  
public List<string> Cursos { get; set; } = null!;
}

Na pasta Controllers crie o controlador EstudantesController definindo o código abaixo:

using ApiFluentValidation.Entities;
using
Microsoft.AspNetCore.Mvc;

namespace ApiFluentValidation.Controllers;

[Route("api/[controller]")]
[ApiController]

public
class EstudantesController : ControllerBase
{
  
private readonly ILogger<EstudantesController> logger;
  
public EstudantesController(ILogger<EstudantesController> logger)
   {
    
this.logger = logger;
   }

   [HttpPost]
  
public IActionResult GetEstudante([FromBody] Estudante estudante)
   {
    
this.logger.LogInformation($"API Input : {{ Id : {estudante.Id},
                 Nome:
{estudante.Nome}}}");

    
return Ok(estudante);
   }
}

Vamos incluir no projeto o pacote nuget FluentValidation.AspNetCore :

dotnet add package FluentValidation.AspNetCore

Podemos também incluir o pacote usando o Manage Packages for Solution no VS 2022 :

Agora podemos usar os recursos da FluentValidation em nosso projeto.

Definindo e criando as validações

Com isso podemos pensar em quais validações podemos adicionar ao nosso projeto. Para simplificar vamos definir o seguinte cenário :

- Queremos que o id do aluno seja sempre maior que zero;
- O tamanho do nome do aluno não deve ser superior a 100 caracteres e não deve ser inferior a 8 caracteres;
- Para cursos , que é um array de strings, vamos definir duas validações;
   - Para cada curso, o tamanho mínimo deve ser 8 e o tamanho máximo deve ser 80;
   - A coleção de cursos deve ter mais de 5 cursos;

A seguir veremos como adicionar as validações mencionadas acima usando fluent validatiors.. Vamos criar uma pasta Validators e adicionar um novo validador chamado EstudanteValidator.

Criando o Validator

Vejamos os fundamentos uasdos para criar um Validator usando a Fluent Validation.

Para criar um validador personalizado, a classe de validação personalizada precisa ser derivada de uma classe genérica, AbstractValidator<T>, onde o parâmetro de tipo T deve ser o tipo que queremos validar. Em nosso exemplo de código, queremos validar o tipo Estudante, logo, precisamos derivar EstudanteValidator de AbstractValidator<Estudante>.

Outra coisa a observar – todas as regras de validação devem ser descritas no construtor e o método RuleFor deve ser chamado para configurar a validação. Este método recebe como entrada uma expressão lambda, que deve especificar a propriedade que queremos validar, e, esta chamada pode ser encadeada para chamar outros validadores.

Por exemplo, suponha que queremos expressar que Id deve ser sempre maior que zero. Para isso, teremos que começar chamando o método RuleFor e então podemos usar o validador GreaterThan :
RuleFor(x => x.Id).GreaterThan(0);

O pacote Fluent Validation fornece muitos validadores integrados que podem ser usados para validar tipos de dados básicos. Usamos um dos validadores integrados, GreaterThan, no exemplo acima. Semelhante a isso, existem validadores para verificação nula, strings vazias, comprimento mínimo, comprimento máximo, etc.

Podemos usar esses validadores para implementar os requisitos de validação hipotéticos mencionados acima. Se você não encontrar o validador pretendido na lista de validadores integrados, também poderá escrever seu próprio validador personalizado.

Existe também um método Must (e MustAsync) que aceita um Func como parâmetro. Então, basicamente, podemos implementar facilmente a lógica de validação personalizada usando isso. Podemos criar um método separado e chamá-lo do validador Must/MustAsync ou podemos especificar a expressão lambda.

Os validadores integrados têm mensagens de erro padrão, mas se você não quiser usar essas mensagens padrão por qualquer motivo, existe uma maneira de especificar a mensagem de erro personalizada usando o método WithMessage :

RuleFor(x => x.Id).GreaterThan(0).WithMessage(“Id é inválido.”);

Se necessário, também podemos encadear mais de um validador para uma determinada propriedade. Por exemplo, o tamanho do nome do aluno não deve ter mais de 100 caracteres e não deve ter menos de 8 caracteres.

Essa validação precisa de dois validadores diferentes, validador de tamanho mínimo e validador de tamanho máximo. Estes podem ser encadeados como mostrado na linha abaixo:

RuleFor(x => x.Name).MinimumLength(10).MaximumLength(250);

Agora que apresentamos os recursos básicos que devemos saber para criar um validator vamos definir o código na classe EstudanteValidator :

using ApiFluentValidation.Entities;
using FluentValidation;
namespace ApiFluentValidation.Validators;
public class EstudanteValidator : AbstractValidator<Estudante>
{
    public EstudanteValidator()
    {
        // Id deve ser maior que 0
        RuleFor(x => x.Id)
            .GreaterThan(0)
            .WithMessage("Id é inválido.");
        // Nome - MinLength 8, MaxLength 100
        RuleFor(x => x.Nome)
            .MinimumLength(8)
            .WithMessage("O 'Nome' deve ter no mínimo 8 caracteres.")
            .MaximumLength(100)
            .WithMessage("O 'Nome' deve er no máximo 100 caracteres.");
        // Cursos - A coleção não pode ser nula
        // Cursos - A coleção deve ter no mínimo 5 cursos
        RuleFor(x => x.Cursos)
            .NotNull()
            .WithMessage("A coleção 'Cursos' deve ter no mínimo 5 cursos.")
            .Must((estudante, cursos, builder) => cursos.Count >= 5)
            .WithMessage("A coleção 'Cursos' deve ter no mínimo 5 cursos.");
        // Cursos - coleção - regra para cada registro
        // Cada entrada não pode ser null
        // Cada entrada deve ter no mínimo 8 e no máximo 80 caracteres
        RuleForEach(x => x.Cursos)
            .NotNull()
            .MinimumLength(8)
            .WithMessage("O nome do curso deve ter no mínimo 8 caracteres.")
            .MaximumLength(80)
            .WithMessage("O nome do curso deve ter no máximo 80 caracteres.");
    }
}

Como acionar a validação ?

Agora temos um novo endpoint que aceita um tipo personalizado Estudante como entrada e, também temos o validador para o tipo personalizado. Mas agora a questão é – como essa lógica de validação seria acionada ?

Para acionar a lógica de validação, precisamos criar a instância do validador (podemos usar o operador new ou  a injeção de dependência).

Na instância do validador, o método Validate ou ValidateAsync pode ser chamado para acionar a validação, e, isso leva o objeto a ser validado como entrada e retorna o objeto ValidationResult. Este objeto de saída possui uma propriedade IsValid, indicando se o objeto é válido. Se esta propriedade for falsa, isso significa que a validação falhou e o objeto é inválido.

O ValidationResult também possui uma coleção, Errors, que como o nome sugere, contém a lista de erros que são identificados pela lógica de validação. Cada entrada na coleção Errors é uma instância do tipo ValidationFailure e cada entrada contém detalhes sobre cada erro de validação.

Aplicando isso ao nosso exemplo vamos alterar o código do controlador EstudantesController no endpoint GetEstudante :

    [HttpPost]
    public IActionResult GetEstudante([FromBody] Estudante estudante)
    {
        this.logger.LogInformation($"API Input : {{ Id : {estudante.Id}, Nome: {estudante.Nome}}}");
        // Cria uma instância do validador
        var estudanteValidator = new EstudanteValidator();
        // Chama o método Validate ou ValidateAsync e passa o objeto que queremos validar
        var result = estudanteValidator.Validate(estudante);
        if (result.IsValid)
        {
            return Ok(estudante);
        }
        var errorMessages = result.Errors.Select(x => x.ErrorMessage).ToList();
        return BadRequest(errorMessages);
    }

Neste código acionamos a validação usando o método Validate, e , se o objeto for válido ele será retornado como está, no response. Se o objeto for inválido as mensagens de erro serão selecionadas na coleção de erros e serão retornadas na resposta.

Esta é uma estratégia muito primitiva para mostrar os erros, apenas para demonstrar como funciona a Fluent Validation. Em aplicativos do mundo real, os objetos podem ser mais complexos e você pode usar uma estratégia diferente para garantir que o chamador saiba exatamente onde ocorreu o erro de validação.

Na próxima parte do artigo vamos continuar a tratar de outros recursos da Fluent Validation.

Pegue o projeto aqui :  ApiFluentValidation.zip ...

"E do modo por que Moisés levantou a serpente no deserto, assim importa que o Filho do Homem seja levantado, para que todo o que nele crê tenha a vida eterna."
João 3:14-15

Referências:


José Carlos Macoratti