O FluentValidation valida objetos .NET com regras escritas em código, e não com atributos no modelo. Declaramos uma classe derivada de AbstractValidator<T> e adicionamos uma cadeia de RuleFor() por propriedade.

A biblioteca já traz validadores para os casos que encontramos o tempo todo: NotEmpty(), Length(), InclusiveBetween(), EmailAddress(), IsInEnum() e Must() para tudo o que ela não cobre. Este artigo passa por cada um deles e depois cria o nosso próprio validador personalizado.

Este artigo continua de onde parou a nossa introdução ao FluentValidation no ASP.NET Core, mas os conceitos daqui valem para qualquer aplicação .NET, não só para aplicações ASP.NET Core. Para demonstrar isso, vamos escrever todo o código em uma biblioteca de classes .NET e depois referenciá-la em uma API ASP.NET Core para ver o resultado final.

Para baixar o código-fonte deste artigo, você pode acessar o repositório FluentValidation Validators.

O que é o FluentValidation?

O FluentValidation é uma biblioteca .NET de código aberto para validar objetos com regras definidas em uma classe separada, em vez de atributos no modelo.

Criamos um validador derivando de AbstractValidator<T> e escrevendo no construtor uma cadeia de RuleFor() por propriedade. A chamada a Validate() retorna um ValidationResult com IsValid e uma lista de falhas; ValidateAsync() faz o mesmo para regras que acessam um banco de dados ou uma API.

O motivo para preferi-lo às data annotations é que as regras são C# comum em uma classe comum. Elas podem depender de serviços injetados, seguir caminhos diferentes conforme o estado do objeto, tratar uma propriedade de forma diferente em dois contextos e ser cobertas por testes unitários sem precisar montar uma requisição.

O custo é que nada é validado automaticamente. Algo precisa chamar o validador: um filtro, um pipeline behavior do MediatR ou o próprio handler.

Configuração do projeto

Vamos começar criando uma nova biblioteca de classes. No Visual Studio, é o modelo Class Library, tendo como destino o .NET 8 ou posterior, porque o FluentValidation 12 não oferece mais suporte ao .NET Standard.

O modelo de projeto também grava <Nullable>enable</Nullable> em ClassLibrary1.csproj. Vamos remover essa linha, como faz o repositório de exemplo. Com ela, o ASP.NET Core trata como obrigatória toda propriedade de tipo de referência não anulável dos nossos modelos e responde às requisições com um 400 próprio antes de o FluentValidation rodar.

Em seguida, vamos instalar o FluentValidation:

dotnet add package FluentValidation

Agora, vamos adicionar ao projeto uma nova enumeração OrderStatus:

public enum OrderStatus
{
    Accepted,
    Processing,
    Complete
}

Além disso, vamos adicionar uma classe chamada Product:

public class Product
{
    public string Name { get; set; }
}

Em seguida, vamos adicionar uma classe chamada Order:

public class Order
{
    public string CustomerName { get; set; }
    public int Price { get; set; }
    public string CustomerEmail { get; set; }
    public OrderStatus OrderStatus { get; set; }
    public Product Product { get; set; }
}

Por fim, vamos preparar o validador:

using FluentValidation;

public class OrderValidator : AbstractValidator<Order>
{
    public OrderValidator()
    {

    }
}

API ASP.NET Core

Para testar, vamos adicionar um novo projeto de API ASP.NET Core e referenciar a biblioteca de classes.

Vamos selecionar “Add > New Project” no Solution Explorer e escolher o modelo ASP.NET Core Web API.

Vamos aceitar todos os valores padrão e depois adicionar, em WebApplication1, uma referência ao projeto ClassLibrary1:

dotnet add WebApplication1 reference ClassLibrary1

Vamos clicar com o botão direito em WebApplication1 e selecionar “Set as Startup Project” para tornar a API o projeto de inicialização padrão:

Validadores com FluentValidation - Definição de uma API como projeto de inicialização

Logo depois, vamos usar o atalho “CTRL-F5” para iniciar a API sem depuração e, quando abrirmos a URL /weatherforecast no navegador, devemos ver o já conhecido resultado da previsão do tempo:

Exemplo de resposta da API

Configuração do controlador

Agora que temos uma API ASP.NET Core básica configurada, com o nosso projeto referenciado, precisamos adicionar um controlador que aceite um objeto Order.

Vamos adicionar um controlador de API vazio à pasta Controllers e chamá-lo de OrdersController, para que ele represente o recurso com que vamos interagir.

No OrdersController criado, vamos adicionar uma diretiva using para referenciar o ClassLibrary:

using ClassLibrary1;

Em seguida, vamos adicionar o seguinte método:

[HttpPost]
public ActionResult Post([FromBody] Order order)
{
    return Ok("Success!");
}

Esse método é quase idêntico ao do nosso artigo anterior: simplesmente aceitamos um objeto no corpo de uma requisição POST e retornamos uma resposta 200 (OK) com o texto “Success!”.

Integração do FluentValidation

Agora, precisamos disponibilizar os validadores para a API. Para isso, vamos instalar o pacote de injeção de dependência (DI) no projeto WebApplication1:

dotnet add package FluentValidation.DependencyInjectionExtensions

Depois, vamos abrir o Program.cs e adicionar as diretivas using necessárias:

using ClassLibrary1;
using FluentValidation;

Além disso, precisamos registrar os validadores no contêiner de DI:

builder.Services.AddControllers();
builder.Services.AddValidatorsFromAssemblyContaining<OrderValidator>();

Isso registra a classe OrderValidator que criamos antes como IValidator<Order>, junto com todos os outros validadores do mesmo assembly, para que possamos injetar qualquer um deles onde precisarmos validar.

Existe um caminho mais antigo que merece menção, porque muito código existente o usa. O pacote FluentValidation.AspNetCore se integra ao pipeline de validação do ASP.NET Core e roda os validadores automaticamente antes de a ação ser executada, preenchendo ModelState da mesma forma que a validação do ModelState faz com as data annotations. A documentação do próprio FluentValidation sobre o ASP.NET Core hoje é direta sobre isso: “Não recomendamos mais usar essa abordagem em projetos novos, mas ela continua disponível para implementações legadas.” Esse pacote também para na versão 11.3.1, e a mesma página diz que ele não tem mais suporte.

Por isso, chamamos o validador nós mesmos. Vamos adicionar using FluentValidation; ao OrdersController, injetar o validador e validar o pedido recebido:

private readonly IValidator<Order> _validator;

public OrdersController(IValidator<Order> validator)
{
    _validator = validator;
}

[HttpPost]
public async Task<ActionResult> Post([FromBody] Order order)
{
    var validationResult = await _validator.ValidateAsync(order);

    if (!validationResult.IsValid)
    {
        foreach (var error in validationResult.Errors)
        {
            ModelState.AddModelError(error.PropertyName, error.ErrorMessage);
        }

        return ValidationProblem(ModelState);
    }

    return Ok("Success!");
}

Três linhas de código de integração nos dão um 400 (Bad Request) com um payload padrão de problem details, e a validação acontece onde conseguimos vê-la. O mesmo formato funciona para validar um endpoint de API mínima, em que não existe nenhum controlador ao qual se conectar.

Vamos abrir o Postman e adicionar a requisição POST para confirmar que tudo está funcionando:

Requisição no Postman - Teste de validadores com FluentValidation

Se clicarmos no botão Send, devemos ver o status 200 (OK) retornado e a mensagem “Success!”:

Resposta no Postman - Teste de validadores com FluentValidation

Agora que está tudo configurado, vamos adicionar validação aos modelos e testá-la na API à medida que avançamos.

Validadores nativos

Os validadores nativos cobrem presença, tamanho, formato e pertencimento a um conjunto, e todos se ligam a uma propriedade por meio de RuleFor().

A presença tem dois níveis de rigor. NotNull() rejeita apenas null, enquanto NotEmpty() também rejeita uma string vazia, espaços em branco e o valor padrão do tipo. É por isso que, com strings, recorremos a NotEmpty().

O tamanho trata strings e números separadamente: MinimumLength(), MaximumLength() e Length() para texto, InclusiveBetween() e os validadores de comparação para números.

É no formato que a biblioteca mais poupa trabalho. EmailAddress() e Matches() cuidam dos padrões que, de outra forma, escreveríamos e reescreveríamos. A própria documentação do FluentValidation chama a regra de e-mail padrão de “uma verificação intencionalmente ingênua, para corresponder ao comportamento do EmailAddressAttribute do ASP.NET Core”.

O pertencimento a um conjunto fica por conta de IsInEnum(). Isso importa mais do que parece: uma propriedade de enumeração vinculada a partir de JSON aceita qualquer inteiro, inclusive valores que a enumeração nunca declarou, e é IsInEnum() que os rejeita.

Tudo o que a biblioteca não cobre fica com Must(), que recebe um predicado.

Validação de null e de valores vazios

A validação mais comum e simples é garantir que um valor foi definido e, no caso de strings ou sequências, que o valor não está vazio.

Vamos adicionar as duas regras a seguir ao OrderValidator:

RuleFor(model => model.CustomerName).NotNull();
RuleFor(model => model.CustomerEmail).NotEmpty();

Essas duas regras garantem que existe um valor em CustomerName e que CustomerEmail tem um valor que não é só espaço em branco. Na maioria dos casos, usaríamos apenas o validador NotEmpty(), a menos que quiséssemos aceitar espaços em branco como valor válido; ainda assim, vale a pena mostrar as duas opções.

As duas regras são incondicionais. Quando uma propriedade é obrigatória só em alguns casos, os métodos When() e Unless() do FluentValidation fazem o mesmo trabalho que atributos condicionais de campo obrigatório, sem nenhum atributo personalizado à vista.

Validação de comprimento

Depois de garantir que as strings têm um valor, a próxima validação que normalmente queremos é a de comprimento.

Vamos alterar a regra que acabamos de criar para CustomerName:

RuleFor(model => model.CustomerName).MinimumLength(10);

Agora, garantimos que o nome tenha pelo menos 10 caracteres (um nome ausente passa a ser aceito, porque os validadores de comprimento consideram null válido). Se preferirmos, podemos usar os seguintes validadores de comprimento semelhantes:

  • MaximumLength (útil quando queremos garantir que uma string tenha um número de caracteres menor ou igual a um valor especificado)
  • Length (quando queremos que a string tenha exatamente um certo número de caracteres ou um número dentro de um intervalo)

Validação de intervalo

Em geral, sempre que temos inteiros no modelo, precisamos aplicar algum tipo de limite razoável ao valor. É aqui que podemos usar os validadores de intervalo.

Vamos voltar à classe OrderValidator e adicionar um validador para a propriedade Price:

RuleFor(model => model.Price).InclusiveBetween(1, 1000);

Aqui, simplesmente garantimos que o valor do preço esteja entre 1 e 1.000, inclusive. Também podemos usar os seguintes validadores semelhantes, cujo uso, felizmente, fica óbvio pelos nomes:

  • ExclusiveBetween
  • LessThan / LessThanOrEqualTo
  • GreaterThan / GreaterThanOrEqualTo

Validação de e-mail

Outra validação frequentemente necessária é a de endereços de e-mail. Para ativá-la, basta adicionar a seguinte regra à propriedade CustomerEmail, no lugar da regra NotEmpty() que tínhamos antes:

RuleFor(model => model.CustomerEmail).EmailAddress();

Vale a pena sermos precisos sobre o que esse validador faz, porque o nome promete mais do que a regra entrega. A verificação padrão de EmailAddress() é deliberadamente ingênua: ela aceita qualquer valor que contenha um único @ que não esteja no início nem no final. Assim, a@b passa, e has [email protected] também, enquanto not-an-email e a@b@c são rejeitados. Isso é intencional, e é a mesma verificação de @ que o próprio EmailAddressAttribute do ASP.NET Core faz.

Se precisarmos de algo mais rigoroso, o validador Matches() recebe uma expressão regular e é o lugar certo para colocar a nossa própria regra.

The Web API Production Checklist, e-book gratuito em inglês

E-book gratuito

Sua Web API está pronta para produção?

33 itens para verificar antes de implantá-la, com a correção de cada um. Um PDF gratuito de 76 páginas para .NET 10.

O e-book está em inglês.

Baixe o checklist gratuito

PDF gratuito. Um único e-mail para enviá-lo. Cancele a inscrição quando quiser.

Validação de enumerações

A maioria das aplicações .NET usa enumerações para indicar o estado de um objeto. Para validá-las, muitas vezes precisamos, a duras penas, fazer conversões/boxing/unboxing e verificar todos os membros da enumeração para saber se a entrada é válida.

Com o FluentValidation, isso é extremamente fácil. Para demonstrar, vamos adicionar uma validação de enumeração à propriedade OrderStatus:

RuleFor(model => model.OrderStatus).IsInEnum();

O método IsInEnum() garante que o valor seja um dos membros incluídos na enumeração.

Embora pareça simples aqui, pense em uma API como a do artigo anterior. Nesse caso, se tivéssemos uma enumeração no modelo, receberíamos o valor da aplicação que faz a chamada na forma de um int, que o model binding vincularia depois à propriedade de enumeração. Como não temos controle sobre esses valores de entrada, precisamos aplicar à validação da enumeração uma abordagem no estilo “lista de permissões” (whitelist), para garantir que o valor vinculado seja um dos que permitimos. É exatamente isso que o método IsInEnum() faz por nós.

Vamos enviar no Postman a mesma requisição de antes e ver o resultado:

Resposta no Postman - Validadores de enumeração com FluentValidation

Agora, vemos que estamos recebendo erros de validação para as regras que criamos, o que significa que tudo está funcionando bem.

Vamos alterar o corpo da requisição para que tudo volte a passar:

Resposta no Postman - Validação de enumeração bem-sucedida com FluentValidation

Com isso, vimos a maioria dos validadores nativos que normalmente usaríamos. Na próxima seção, vamos ver como encadear validadores.

Encadeamento de validadores

Muitas vezes, precisamos aplicar mais de uma regra de validação a uma propriedade específica. É aí que entra o encadeamento.

Vamos demonstrar isso encadeando um validador ao validador EmailAddress() que já temos:

RuleFor(model => model.CustomerEmail)
    .EmailAddress()
    .MinimumLength(20);

Aqui, garantimos duas coisas:

  1. O valor de CustomerEmail é um endereço de e-mail válido
  2. O valor de CustomerEmail tem pelo menos 20 caracteres

É um exemplo bem artificial, mas demonstra uma técnica que podemos aplicar quando queremos usar um validador nativo que não atende totalmente às nossas necessidades e queremos ser mais específicos.

Vamos alterar o valor de customerEmail na requisição do Postman para “AAAAA” e clicar em Send:

Resposta no Postman - Encadeamento de validadores com FluentValidation

Repare que agora recebemos 2 erros de validação, um para cada regra encadeada.

Aqui, também vale a pena mencionar como funciona o modo de “cascata” (cascade) do FluentValidation. No exemplo acima, o validador EmailAddress() é aplicado e, independentemente de a validação passar ou falhar, a validação MinimumLength() também é aplicada em seguida.

Isso tem o efeito colateral de aplicar validação extra (e, portanto, processamento extra) quando a chamada vai falhar de qualquer forma. Às vezes, isso não é desejável.

Para evitar isso, vamos alterar a regra de validação de e-mail:

RuleFor(model => model.CustomerEmail)
    .Cascade(CascadeMode.Stop)
    .EmailAddress()
    .MinimumLength(20);

Agora, se a validação EmailAddress() falhar, o validador MinimumLength() não é executado. O modo de cascata também pode ser definido para o validador inteiro, no construtor, ou globalmente, para todos os validadores.

Hoje, a enumeração CascadeMode declara exatamente dois membros, Continue e Stop. Código e artigos mais antigos usam CascadeMode.StopOnFirstFailure, que o FluentValidation tornou obsoleto na versão 11.0 e removeu na 12.0 (guia de atualização do FluentValidation 12.0, consultado em 13 de setembro de 2026). No FluentValidation 12, essa linha gera um erro de compilação, e não um aviso.

Vamos rodar a aplicação e enviar a mesma requisição no Postman de novo:

Resposta no Postman - Opções de cascata para validadores com FluentValidation

Agora, só a primeira regra de validação aparece. Vamos alterar o valor de customerEmail para “[email protected]”, para que a entrada volte a ser válida.

Na próxima seção, vamos ver como usar validadores aninhados quando temos várias classes.

Validadores aninhados

No projeto atual, temos duas classes, Order e Product. Até agora, só aplicamos validação à classe Order. E se quiséssemos aplicar validação também à classe Product? Como garantiríamos, então, que essa validação fosse chamada quando validássemos o Order? Esse é um caso comum, então vamos ver como implementá-lo.

Primeiro, vamos adicionar um validador simples para a classe Product:

using FluentValidation;

public class ProductValidator : AbstractValidator<Product>
{
    public ProductValidator()
    {
        RuleFor(model => model.Name).NotEmpty();
    }
}

Vamos manter tudo simples aqui, porque queremos nos concentrar em como chamar esse validador a partir da classe Order.

Dito isso, vamos adicionar uma nova regra ao validador de Order:

RuleFor(model => model.Product)
    .NotNull()
    .SetValidator(new ProductValidator());

De forma bem simples, garantimos que um valor para Product seja enviado e passamos a validação para o ProductValidator. Esse é um ótimo exemplo de separação de responsabilidades, porque, se e quando a classe Product evoluir (e, com ela, as regras de validação), isso não será uma preocupação do OrderValidator.

Vamos rodar a API e executar no Postman a requisição que já temos:

Resposta no Postman - Erro em validadores aninhados com FluentValidation

Agora, recebemos um erro para a propriedade do produto, informando que ela precisa existir. Quem dispara o erro é a nossa regra de validação NotNull().

Vamos tentar trocar o valor do produto por um objeto vazio:

Resposta no Postman - Erro do validador aninhado com FluentValidation

Ainda recebemos um erro, mas repare que agora é a regra de validação aninhada que dispara para a propriedade Product.Name.

Para corrigir a entrada, vamos alterá-la mais uma vez:

Resposta no Postman - Validação aninhada bem-sucedida com FluentValidation

Podemos ver que agora a requisição é bem-sucedida.

A seguir, vamos falar sobre como validar coleções.

Coleções

No projeto atual, um único Order tem um único Product. Mas e se um mesmo pedido tiver vários produtos (um caso mais realista)?

Vamos atualizar a classe Order para refletir isso:

public Product[] Products { get; set; }

Se formos ao OrderValidator, veremos que agora há um erro de compilação na seguinte linha:

RuleFor(model => model.Product)
    .NotNull()
    .SetValidator(new ProductValidator());

O que queremos fazer agora é continuar usando o ProductValidator, mas invocá-lo para cada produto.

Para isso, vamos modificar a linha com erro:

RuleForEach(model => model.Products).SetValidator(new ProductValidator());

Agora, garantimos que cada produto seja validado com as regras do ProductValidator.

Vamos rodar a aplicação e modificar a requisição no Postman:

Resposta no Postman - Validadores de coleção com FluentValidation

The Web API Production Checklist, e-book gratuito em inglês

E-book gratuito

Sua Web API está pronta para produção?

33 itens para verificar antes de implantá-la, com a correção de cada um. Um PDF gratuito de 76 páginas para .NET 10.

O e-book está em inglês.

Baixe o checklist gratuito

PDF gratuito. Um único e-mail para enviá-lo. Cancele a inscrição quando quiser.

Repare que temos dois produtos na entrada, mas a validação falha para o segundo produto (índice 1), porque a propriedade “name” está vazia. Isso prova que a validação está sendo executada para cada item do array, e a falha é informada em Products[1].Name, de modo que sabemos exatamente qual item estava errado.

Vamos corrigir a requisição no Postman:

Resposta no Postman - Validação de coleção bem-sucedida com FluentValidation

Agora, tudo voltou a funcionar.

Na próxima seção, vamos falar sobre como lançar exceções de validação.

Lançamento de exceções

A forma mais comum de invocar os validadores é o método Validate(), seguido da inspeção do ValidationResult.

No entanto, e se quisermos lançar uma exceção automaticamente e interromper a execução do código?

Podemos fazer isso com o método ValidateAndThrow():

orderValidator.ValidateAndThrow(order);

Repare que, desta vez, não guardamos o resultado da validação. Isso porque ValidateAndThrow retorna void. Ou a validação é bem-sucedida e o código continua, ou uma ValidationException é lançada.

O motivo mais comum para fazer isso é o tratamento global de erros. Em vez de verificar constantemente os resultados da validação em todo o código e agir de acordo (o que violaria o princípio “DRY“), podemos lançar uma ValidationException e capturar essa exceção mais acima no código.

Comportamento padrão

Na nossa API, chamamos o validador explicitamente e inspecionamos o resultado: esse é o comportamento que configuramos antes. Vamos ver o que isso nos custa em um exemplo menor antes de passar a lançar a exceção.

Para demonstrar esse comportamento, vamos alterar o método da API que já temos:

[HttpPost]
public ActionResult Post([FromBody] Order order)
{
    var product = new Product
    {
        Name = null // will fail validation
    };

    var validationResult = new ProductValidator().Validate(product);

    return validationResult.IsValid
        ? (ActionResult) Ok("Success!")
        : BadRequest("Validation failed");
}

Vamos explicar o código:

  1. Primeiro, criamos um novo Product com valores que sabemos que vão falhar na validação
  2. Em seguida, criamos um ProductValidator() e validamos o produto
  3. Por fim, retornamos Ok ou BadRequest, conforme o resultado da validação

Se rodarmos a API e executarmos no Postman a requisição que já temos:

Resposta no Postman - Comportamento padrão de validação com FluentValidation

Podemos ver a resposta “Validation failed”.

Uso de ValidateAndThrow()

Agora, vamos alterar o código para lançar uma exceção em vez disso. Primeiro, precisamos da referência ao FluentValidation, que adicionamos antes:

using FluentValidation;

Em seguida, vamos modificar o método:

[HttpPost]
public ActionResult Post([FromBody] Order order)
{
    var product = new Product
    {
        Name = null // will fail validation
    };

    new ProductValidator().ValidateAndThrow(product);

    return Ok("Success!");
}

Agora, se rodarmos a API e executarmos a requisição no Postman, o código de resposta é 500 (Internal Server Error) e o corpo informa que uma ValidationException foi lançada.

Esse não é um bom payload para uma requisição inválida, e é por isso que ValidateAndThrow() só compensa quando há algo mais acima esperando para capturar a exceção e transformá-la em uma resposta. Um middleware de tratamento global de erros é um desses lugares; executar validadores dentro de um pipeline do MediatR é outro, em que um behavior valida a requisição e o pipeline nunca chega ao handler.

Vamos reverter o método para a versão que configuramos antes:

[HttpPost]
public async Task<ActionResult> Post([FromBody] Order order)
{
    var validationResult = await _validator.ValidateAsync(order);

    if (!validationResult.IsValid)
    {
        foreach (var error in validationResult.Errors)
        {
            ModelState.AddModelError(error.PropertyName, error.ErrorMessage);
        }

        return ValidationProblem(ModelState);
    }

    return Ok("Success!");
}

Na próxima seção, vamos falar sobre como levar o FluentValidation a outro nível com validadores personalizados.

Como escrever um validador personalizado no FluentValidation?

Há três formas, e a escolha depende de a regra precisar ou não de um nome.

Para uma regra pontual, Must() recebe um predicado, e a regra fica pronta em uma linha. O predicado recebe o valor da propriedade e retorna bool, e a chamada é combinada com WithMessage() para dizer o que deu errado.

Para uma regra usada em mais de um validador, escrevemos um método de extensão para IRuleBuilder<T, TProperty> que retorna IRuleBuilderOptions<T, TProperty>. Dentro dele, combinamos validadores existentes com a nossa própria chamada a Must() e depois o chamamos como qualquer regra nativa.

A forma de método de extensão é o que torna uma regra personalizada reutilizável, porque a regra e a mensagem dela andam juntas, e todo validador recebe o mesmo comportamento.

Para regras que precisam de serviços injetados ou de trabalho assíncrono, a terceira opção é uma classe que implementa IPropertyValidator ou um AbstractValidator passado para SetValidator().

Se clicarmos com o botão direito em qualquer validador nativo que usamos até aqui e selecionarmos “Go to definition”, veremos que ele fica em uma classe chamada DefaultValidatorExtensions. Esses validadores estendem a interface IRuleBuilder, e o nosso também vai estendê-la.

Vejamos a validação que já temos para a propriedade CustomerName na classe Order:

RuleFor(model => model.CustomerName).MinimumLength(10);

E se quisermos fazer mais do que garantir que o nome tenha pelo menos 10 caracteres? E se quisermos garantir que seja de fato um nome real, com nome e sobrenome e um espaço no meio? Vamos criar um validador personalizado para encapsular essa lógica.

Criação de um validador FullName personalizado

Primeiro, vamos adicionar uma classe FluentValidationExtensions com um novo método de extensão:

using FluentValidation;

public static class FluentValidationExtensions
{
    public static IRuleBuilderOptions<T, string> FullName<T>(this IRuleBuilder<T, string> ruleBuilder)
    {
        return ruleBuilder
                   .MinimumLength(10)
                   .Must(val => val.Split(" ").Length >= 2);
    }
}

Vamos explicar o código:

  1. Primeiro, vemos que o IRuleBuilder na assinatura do método recebe 2 argumentos de tipo, T e string. Basicamente, isso significa que o validador personalizado pode ser aplicado a qualquer validador de uma classe, desde que a propriedade validada seja uma string. Se quiséssemos restringi-lo a certas classes, poderíamos usar restrições genéricas do C#, mas mantemos tudo simples para fins de demonstração.
  2. Na implementação, usamos o validador MinimumLength que já existe e adicionamos o código de validação:
.Must(val => val.Split(" ").Length >= 2);

Estamos usando o “encadeamento de validadores” (que vimos antes) e também o validador Must, que aceita um predicado cujo tipo de retorno precisa ser bool. Basicamente, é aqui que podemos colocar qualquer lógica personalizada. Neste caso, dividimos o valor pelos espaços em um array e garantimos que haja pelo menos dois elementos (o primeiro nome e, depois, possivelmente nomes do meio e sobrenomes). Claro que essa é uma implementação bem ingênua, mas, mais uma vez, mantemos tudo simples para focar nos diferentes validadores do FluentValidation, e não em uma lógica complicada.

Uso do validador personalizado

Vamos modificar a regra de validação para usar o novo validador:

RuleFor(model => model.CustomerName).FullName();

Agora, vamos rodar a API de novo e executar a requisição no Postman:

Resposta no Postman - Validadores personalizados com FluentValidation

A requisição continua sendo bem-sucedida.

Vamos ver o que acontece se alterarmos o valor de customerName para “JoeBloggs” (sem espaço) e executarmos a requisição de novo:

Resposta no Postman - Falha em validadores personalizados com FluentValidation

Repare que as duas regras de validação falharam. Também vemos que a segunda regra de validação não é muito amigável para o usuário, então vamos alterar o validador personalizado para exigir exatamente um espaço e usar o método WithMessage():

return ruleBuilder
           .MinimumLength(10)
           .Must(val => val.Split(" ").Length == 2)
           .WithMessage("Name must contain a single space and be at least 10 characters long");

Isso deve deixar o erro um pouco mais fácil de entender. Vamos rodar a API e executar a requisição de novo:

Resposta no Postman - Mensagens de erro personalizadas em validadores com FluentValidation

Agora, o cliente deve conseguir entender facilmente por que a requisição falhou e fazer os ajustes necessários. O método WithMessage() é útil para isso quando queremos fornecer informações de erro melhores do que as que o FluentValidation oferece por padrão, o que é especialmente importante na criação de uma API pública.

Qual validador do FluentValidation usar?

Até aqui, usamos pelo menos uma vez a maioria dos validadores que este artigo aborda. Em vez de percorrer o passo a passo de novo para encontrar o que precisamos, aqui está o conjunto completo em um só lugar, junto com o que cada um realmente verifica:

O que queremos verificarValidadorObservação
Se há um valorNotNull()Rejeita null; aceita strings vazias e só com espaços em branco
Se há um valor e ele tem conteúdoNotEmpty()Rejeita null, string vazia, espaços em branco e valores padrão
O comprimento de uma stringMinimumLength(), MaximumLength(), Length()Length(min, max) aceita um intervalo
Os limites de um númeroInclusiveBetween(), ExclusiveBetween()Também GreaterThan(), LessThanOrEqualTo() e as respectivas variantes
Um endereço de e-mailEmailAddress()Verifica apenas se o valor contém um único @ que não esteja no início nem no final, e não se ele está bem formado: a@b passa, e has [email protected] também
Se um valor segue um padrãoMatches()Aceita uma regex ou uma instância de Regex
Se um valor de enumeração é um membro declaradoIsInEnum()Detecta valores int fora do intervalo vinculados a partir de uma requisição
Se um valor pertence a um conjuntoIsInEnum() ou Must() com uma buscaNão existe um validador específico para "um de"
Qualquer outra coisaMust()Recebe um predicado que retorna bool
Um objeto aninhadoSetValidator()Passa a propriedade para outro AbstractValidator<T>
Cada item de uma coleçãoRuleForEach().SetValidator()Os erros são informados com o índice do item
Parar após a primeira falha.Cascade(CascadeMode.Stop)O nome antigo StopOnFirstFailure foi removido, e não marcado como obsoleto: na 12.1.1, ele gera o erro de compilação CS0117
Substituir a mensagem.WithMessage()Também .WithErrorCode() e .WithSeverity()

Três linhas dessa tabela merecem uma segunda leitura. NotNull() e NotEmpty() não são intercambiáveis, e EmailAddress() é muito mais permissivo do que o nome sugere.

Conclusão

Neste artigo, mergulhamos fundo no assunto e vimos muitos casos com que normalmente precisamos lidar ao validar objetos em uma aplicação .NET: os validadores nativos de presença, comprimento, intervalo, formato e enumerações, o encadeamento deles com um modo de cascata, validadores aninhados, a validação de coleções, o lançamento de exceção em caso de falha e, por fim, a criação do nosso próprio validador personalizado.

Para a maioria das empresas, os dados são o ativo mais valioso, então é importante protegermos o que entra no sistema. Por isso, é essencial contar com uma ótima biblioteca como o FluentValidation e saber quais regras aplicar em cada caso.

Boas validações e lembre-se: “nunca confie na entrada do usuário!”

Testado com .NET 10.0.10 e FluentValidation 12.1.1.