Validação em Minimal APIs no .NET 10

A validação em Minimal APIs passou a ser suportada nativamente a partir do ASP.NET Core no .NET 10. Agora, basta uma linha no Program.cs para que o runtime valide parâmetros e corpos de requisição antes de executar o seu endpoint.

Essa é uma excelente notícia para quem prefere a abordagem minimalista, mas não quer abrir mão da robustez das validações automáticas. Neste artigo, você vai entender o que mudou, como ativar o recurso e como criar regras próprias.

Por que a validação em Minimal APIs importa

Até o .NET 9, validar dados em Minimal APIs exigia código manual ou soluções alternativas. Em outras palavras, cada endpoint precisava conferir os dados recebidos antes de seguir com a lógica de negócio.

Com o suporte embutido, você declara as regras direto em parâmetros de rota, query string, cabeçalhos e corpo da requisição. Tudo isso usando os já conhecidos Data Annotations.

Na prática, a validação em Minimal APIs protege o endpoint de entradas inválidas. Assim, o handler recebe apenas dados que já passaram pelas regras definidas, e você escreve menos código defensivo.

Se você está começando agora, o artigo sobre Minimal APIs no ASP.NET apresenta os fundamentos desse modelo de construção de APIs.

O que são Data Annotations

Os Data Annotations são atributos do namespace System.ComponentModel.DataAnnotations. Eles aplicam regras de validação e metadados direto sobre propriedades e parâmetros no C#.

Esses atributos garantem que os dados recebidos estejam no formato esperado antes de qualquer lógica rodar. Por isso, eles aparecem em praticamente todo projeto ASP.NET Core.

Por exemplo, o atributo [Required] garante que um valor não seja nulo ou vazio. Já o [Range(1, 100)] assegura que o valor fique dentro de um intervalo específico.

Para usá-los, basta decorar o parâmetro do endpoint ou a propriedade de uma classe com os atributos desejados. Em seguida, o ASP.NET Core valida os dados e retorna um erro 400 com os detalhes quando as regras não são atendidas.

using System.ComponentModel.DataAnnotations;

public class UserInput
{
    [Required]
    public string Name { get; set; } = string.Empty;

    [EmailAddress]
    [Required]
    public string Email { get; set; } = string.Empty;
}

Essa abordagem reduz código repetitivo. Além disso, ela mantém as validações centralizadas e consistentes, porque a regra fica junto do modelo que ela protege.

Atributos mais usados

A tabela abaixo resume os atributos que você vai usar com mais frequência.

Atributo O que valida
[Required] Valor obrigatório, não nulo e não vazio
[Range(min, max)] Valor numérico dentro de um intervalo
[StringLength(max)] Tamanho máximo, e opcionalmente mínimo, de uma string
[MinLength] e [MaxLength] Tamanho mínimo e máximo de strings e coleções
[EmailAddress] Formato de endereço de e-mail
[Url] Formato de URL
[RegularExpression] Valor compatível com uma expressão regular
[Compare] Igualdade entre duas propriedades, como senha e confirmação

Data Annotations e Minimal APIs antes do .NET 10

Antes do .NET 10, você até podia decorar os modelos de uma Minimal API com Data Annotations. Contudo, o pipeline das Minimal APIs não aplicava a validação automaticamente, como acontece nos controllers com o atributo [ApiController].

Por que isso acontecia

As Minimal APIs foram projetadas para serem leves, simples e de alta performance. Quando surgiram no .NET 6, o foco era escrever APIs com o mínimo de código e de sobrecarga.

O sistema de validação automática envolve model binding, leitura de atributos e tratamento de erros. Por isso, ele ficou fora da proposta inicial, pois adicionaria complexidade e processamento extra ao pipeline minimalista.

Qual era a consequência

Mesmo com [Required], [Range] ou [EmailAddress] nos modelos, o ASP.NET Core não validava esses dados nos endpoints minimalistas. Então, você precisava escolher uma destas saídas.

  • Fazer a validação manual com Validator.TryValidateObject.
  • Usar bibliotecas externas, como FluentValidation, ou um endpoint filter próprio.
  • Migrar para controllers MVC para ter a validação automática.

O exemplo abaixo mostra como ficava a validação manual. Repare que cada endpoint precisava repetir esse bloco.

app.MapPost("/users", (UserInput input) =>
{
    var context = new ValidationContext(input);
    var results = new List<ValidationResult>();

    // Validação manual, necessária antes do .NET 10
    if (!Validator.TryValidateObject(input, context, results, validateAllProperties: true))
    {
        var errors = results
            .GroupBy(r => r.MemberNames.FirstOrDefault() ?? string.Empty)
            .ToDictionary(g => g.Key, g => g.Select(r => r.ErrorMessage ?? "Invalid value.").ToArray());

        return Results.ValidationProblem(errors);
    }

    return Results.Created($"/users/{input.Email}", input);
});

Esse código funciona, mas espalha a mesma lógica por toda a API. Consequentemente, o risco de esquecer a validação em algum endpoint aumenta.

Como funciona a validação em Minimal APIs no .NET 10

O .NET 10 trouxe o método AddValidation, que registra a validação em Minimal APIs como recurso nativo. Segundo as novidades do ASP.NET Core no .NET 10, o runtime passa a validar os dados antes de chamar o handler.

Ao habilitar o recurso, o ASP.NET Core faz o seguinte.

  • Valida dados vindos da query string, dos cabeçalhos e do corpo da requisição.
  • Aplica atributos como [Required], [Range] e [EmailAddress].
  • Permite regras complexas com IValidatableObject ou atributos personalizados.
  • Retorna um 400 Bad Request com os detalhes dos erros quando a validação falha.

Por trás dos panos, o recurso usa um source generator. Ele descobre, em tempo de compilação, os tipos usados nos handlers das Minimal APIs e gera os metadados de validação.

Em seguida, o framework adiciona um endpoint filter a cada endpoint compatível. Esse filtro executa a validação e interrompe a requisição antes do handler quando encontra erros.

Habilitando a validação nas Minimal APIs

Para começar, registre os serviços com o método AddValidation no Program.cs.

var builder = WebApplication.CreateBuilder(args);

// Ativa a validação automática dos endpoints
builder.Services.AddValidation();

var app = builder.Build();

Esse método registra os componentes necessários e cria o endpoint filter que aplica a validação em todos os endpoints compatíveis. Ou seja, você não precisa decorar cada rota com nada adicional.

Validação em Minimal APIs com classes e records

O recurso funciona tanto com classes quanto com records. Quando um parâmetro do endpoint é desse tipo, o framework aplica os atributos automaticamente.

public record Product(
    [Required] string Name,
    [Range(1, 1000)] int Quantity);

app.MapPost("/products", (Product product) =>
{
    // Só chega aqui se o produto for válido
    return TypedResults.Ok(product);
});

Se o cliente enviar uma quantidade igual a zero, por exemplo, o endpoint nem chega a rodar. Nesse caso, o cliente recebe um 400 com a lista de propriedades inválidas.

Tipos aninhados e coleções

Modelos reais raramente são planos, e a validação em Minimal APIs leva isso em conta. Um pedido, por exemplo, costuma ter um cliente e uma lista de itens, cada um com as suas próprias regras.

Segundo a documentação de validação do ASP.NET Core, os tipos alcançáveis a partir do tipo raiz entram automaticamente nos metadados gerados. Com isso, as regras das classes internas também participam da validação.

public record OrderItem(
    [Required] string Sku,
    [Range(1, 50)] int Quantity);

public record Order(
    [Required] string CustomerEmail,
    [MinLength(1)] List<OrderItem> Items);

app.MapPost("/orders", (Order order) => TypedResults.Created($"/orders/{Guid.NewGuid()}", order));

Nesse cenário, um pedido sem itens falha no [MinLength(1)]. Da mesma forma, um item com quantidade 80 falha no [Range] declarado em OrderItem.

Na resposta de erro, a chave de cada mensagem identifica a propriedade inválida. Assim, o cliente consegue localizar com facilidade o dado que precisa de correção.

Por isso, a validação em Minimal APIs atende bem a contratos ricos, sem que você escreva laços manuais para percorrer cada elemento da coleção.

Validando query string e cabeçalhos

As regras não se limitam ao corpo da requisição. Você também pode decorar parâmetros vindos da query string e dos cabeçalhos, combinando os atributos de binding com os de validação.

app.MapGet("/orders", (
    [FromQuery, Range(1, 100)] int pageSize,
    [FromHeader(Name = "X-Tenant-Id"), Required] string tenantId) =>
{
    // pageSize e tenantId já chegam validados
    return TypedResults.Ok(new { pageSize, tenantId });
});

Nesse exemplo, um pageSize igual a 500 gera um erro 400. Da mesma forma, uma requisição sem o cabeçalho X-Tenant-Id é rejeitada antes do handler.

Esse padrão de validação em Minimal APIs é muito útil em APIs paginadas e multi-tenant. Afinal, limitar o tamanho da página evita consultas pesadas, e exigir o identificador do tenant evita acessos sem contexto.

Exemplo prático com atributo personalizado

Você também pode aplicar atributos diretamente nos parâmetros da Minimal API. O exemplo a seguir, inspirado na documentação oficial, usa um atributo personalizado chamado EvenNumber.

app.MapPost("/products",
    ([EvenNumber(ErrorMessage = "Product ID must be even")] int productId, [Required] string name)
        => TypedResults.Ok(productId));

Nesse exemplo, productId precisa ser um número par, regra garantida pelo atributo EvenNumber. Além disso, name é obrigatório por causa do [Required].

Se qualquer um desses requisitos falhar, a API retorna uma resposta 400 com os erros de validação. O atributo EvenNumber não existe no framework, então você precisa criá-lo, como mostra a próxima seção.

Criando o atributo EvenNumber

Para criar uma regra própria, herde de ValidationAttribute e sobrescreva o método IsValid.

using System.ComponentModel.DataAnnotations;

[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property)]
public sealed class EvenNumberAttribute : ValidationAttribute
{
    public override bool IsValid(object? value)
    {
        // Valores nulos ficam a cargo do [Required]
        if (value is null)
            return true;

        return value is int number && number % 2 == 0;
    }
}

Repare que o atributo considera válido um valor nulo. Dessa forma, cada atributo cuida de uma única responsabilidade, e a obrigatoriedade continua com o [Required].

Desativando a validação em Minimal APIs por endpoint

Quer controle total? Você pode desativar a validação em Minimal APIs para endpoints individuais com o método DisableValidation.

app.MapPost("/products",
    ([EvenNumber(ErrorMessage = "Product ID must be even")] int productId, [Required] string name)
        => TypedResults.Ok(productId))
    .DisableValidation();

Esse recurso é útil quando você quer validar manualmente ou criar um comportamento customizado. Por exemplo, um endpoint de importação em lote pode aceitar registros parcialmente inválidos e devolver um relatório.

Validações avançadas

Além dos atributos padrão do namespace System.ComponentModel.DataAnnotations, você tem duas opções para regras mais elaboradas. Primeiro, criar atributos próprios herdando de ValidationAttribute, como no exemplo anterior.

A segunda opção é implementar a interface IValidatableObject. Ela serve para regras que dependem de várias propriedades ao mesmo tempo ou de condições de negócio.

using System.ComponentModel.DataAnnotations;

public record Booking(
    [Required] string GuestName,
    DateOnly CheckIn,
    DateOnly CheckOut) : IValidatableObject
{
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        // Regra que envolve duas propriedades
        if (CheckOut <= CheckIn)
        {
            yield return new ValidationResult(
                "Check-out must be after check-in.",
                [nameof(CheckOut)]);
        }
    }
}

Com esse modelo, o endpoint que recebe um Booking rejeita reservas com datas invertidas antes de executar o handler. Assim, a regra de negócio fica no próprio tipo, perto dos dados que ela valida.

Personalizando as respostas de erro

As respostas de erro da validação em Minimal APIs seguem o formato de Problem Details. Além disso, no .NET 10 você pode personalizá-las registrando uma implementação de IProblemDetailsService.

builder.Services.AddProblemDetails(options =>
{
    options.CustomizeProblemDetails = context =>
    {
        // Acrescenta um identificador de rastreamento em toda resposta de erro
        context.ProblemDetails.Extensions["traceId"] = context.HttpContext.TraceIdentifier;
    };
});

builder.Services.AddValidation();

Com isso, os erros de validação ficam consistentes com os demais erros da API. O artigo sobre Problem Details e a RFC 9457 no .NET 10 aprofunda esse formato.

Testando a validação em Minimal APIs

Para ver o recurso em ação, envie uma requisição inválida ao endpoint de produtos. No exemplo abaixo, o nome está vazio e a quantidade está fora do intervalo permitido.

curl -X POST https://localhost:5001/products \
  -H "Content-Type: application/json" \
  -d '{ "name": "", "quantity": 0 }'

Como resposta, a API devolve o status 400 com um corpo no formato de Problem Details. A propriedade errors agrupa as mensagens por campo, o que facilita exibir os erros no front-end.

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "Name": [ "The Name field is required." ],
    "Quantity": [ "The field Quantity must be between 1 and 1000." ]
  }
}

Os textos exatos podem variar conforme a versão e a cultura configurada. Ainda assim, a estrutura com title, status e errors se mantém, então o cliente pode confiar nela.

Também vale cobrir esse comportamento com testes de integração. Assim, uma mudança acidental no registro do AddValidation não passa despercebida, já que a falha de metadados é silenciosa.

Validação nativa ou FluentValidation

A chegada da validação em Minimal APIs não torna as bibliotecas externas obsoletas. Na verdade, cada abordagem atende melhor a um tipo de cenário.

A validação nativa brilha em regras declarativas e simples, como obrigatoriedade, tamanho e formato. Além disso, ela não exige dependências extras e já se integra ao Problem Details.

Por outro lado, o FluentValidation favorece regras condicionais extensas, validações assíncronas e reaproveitamento de validadores entre camadas. Nesses casos, separar as regras do modelo pode deixar o código mais legível.

Portanto, comece pela validação nativa e só adote uma biblioteca quando as regras realmente exigirem. Essa escolha mantém o projeto mais enxuto e reduz o número de pacotes a atualizar.

Cuidados com a validação em Minimal APIs

O source generator só descobre tipos validáveis no assembly onde você chama AddValidation. Portanto, se os endpoints ou modelos ficam em outro projeto, chame AddValidation também a partir desse assembly.

A documentação recomenda criar um método de extensão em cada biblioteca que declara tipos validáveis. Depois, o projeto principal chama esses métodos junto com o seu próprio AddValidation.

namespace Catalog.Extensions;

public static class ServiceCollectionExtensions
{
    // Executa o source generator no assembly da biblioteca
    public static IServiceCollection AddCatalogValidation(this IServiceCollection services)
        => services.AddValidation();
}

Outro ponto importante: quando os metadados não são gerados, nenhuma exceção ou log aparece. Nesse caso, a entrada inválida simplesmente chega ao handler, o que torna o problema silencioso.

Também vale saber que, no .NET 10, algumas APIs de baixo nível do pacote Microsoft.Extensions.Validation são experimentais. No entanto, o AddValidation e o filtro de validação embutido continuam estáveis.

Por fim, a validação de entrada não substitui o tratamento global de exceções. Para os erros inesperados, veja o artigo sobre IExceptionHandler no .NET 10.

Conclusão

Com o suporte oficial, a validação em Minimal APIs deixa o modelo minimalista ainda mais completo e produtivo. Você ganha simplicidade na definição dos endpoints sem abrir mão da robustez no tratamento dos dados de entrada.

Se você ainda não usa Minimal APIs no seu projeto ASP.NET Core, talvez agora seja a hora certa de considerar. Afinal, com menos código, mais clareza e validação embutida, elas entregam poder com elegância.

FAQ

Em qual versão do .NET a validação em Minimal APIs ficou nativa?

No .NET 10. Antes disso, era preciso validar manualmente ou usar bibliotecas e filtros próprios.

Preciso instalar algum pacote para usar AddValidation?

Em projetos web com o SDK Microsoft.NET.Sdk.Web, não. Bibliotecas de classe comuns que declaram tipos validáveis precisam referenciar o pacote Microsoft.Extensions.Validation.

Qual status HTTP a API retorna quando a validação falha?

Ela retorna 400 Bad Request, com os detalhes dos erros no formato de Problem Details.

Posso continuar usando FluentValidation?

Sim. A validação nativa não impede o uso de bibliotecas externas, e você pode desativá-la por endpoint com DisableValidation.

A validação funciona com records?

Sim. Os atributos de Data Annotations nos parâmetros de um record são aplicados da mesma forma que nas propriedades de uma classe.

Glossário

Termo Significado
Minimal APIs Modelo do ASP.NET Core para criar endpoints HTTP com o mínimo de código e configuração
Data Annotations Atributos de System.ComponentModel.DataAnnotations que declaram regras de validação
AddValidation Método do .NET 10 que registra a validação automática dos endpoints
DisableValidation Método que desativa a validação automática em um endpoint específico
ValidationAttribute Classe base para criar atributos de validação personalizados
IValidatableObject Interface para regras de validação que envolvem várias propriedades
Endpoint filter Componente que executa lógica antes ou depois do handler de um endpoint
Source generator Recurso do compilador que gera código C# em tempo de compilação
Problem Details Formato padronizado de resposta de erro em APIs HTTP, definido pela RFC 9457

Fontes