Validação dos dados de entrada é parte essencial no desenvolvimento de software. O framework ASP.NET provê um conjunto de funcionalidades que auxiliam os desenvedores garantir que as APIs só processem dados que possuem valores que atendam as regras da aplicação. Nesse texto serão discutidas as formas de validação do ASP.NET e o formato das mensagens de erro retornadas. Serão tratados erros de tipos inválidos e a validação dos modelos usando a funcionalidade de DataAnnotations
do ASP.NET. Além disso, será mostrado como customizar o formato de resposta. Os exemplos de código foram desenvolvidos usando a versão 5 do ASP.NET.
Model binding é a funcionalidade do ASP.NET que atua nas requisições HTTP convertendo os dados de entrada nas rotas em tipos .NET.
A etapa de Model Binding é executada durante o pipelines de filtros. Mais especificamente, essa etapa é executada antes dos filtros de ação que por sua vez são executados antes e depois da execução dos métodos dos controllers.
Considere o Controller
de exemplo:
[Route("[controller]")]
public class ExampleController : ControllerBase
{
[HttpGet]
public ActionResult Get(int id)
{
if (id == 1)
return Ok(new ExampleRequest{Name = "Example1"});
return NotFound();
}
}
Ao fazer uma requisição para essa rota o ASP.NET vai examinar a requisição para encontrar o campo id
, que nesse caso é enviada como parâmetro. Em seguida, o valor encontrado será convertido para inteiro e o método Get
será executado. Mas o que acontece se o valor passado for um texto? O dado não é convertido e é preenchido com o valor padrão, que para inteiro é 0. Caso fosse um objeto, o valor padrão seria null
o que poderia causar um NullReferenceException
se não fosse feita nenhuma verificação.
A propriedade ModelState da classe ControllerBase
contém o estado do modelo e a validação de associação de modelo. Quando não é possível a conversão da entrada de dados o ModelState
é inválido. Logo, é possível verificar se os dados de entrada estão corretos em relação aos tipos esperados.
[Route("[controller]")]
public class ExampleController : ControllerBase
{
[HttpGet]
public ActionResult Get(int id)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
if (id == 1)
return Ok(new ExampleRequest{Name = "Example1"});
return NotFound();
}
}
Dessa forma, se essa rota for chamada enviando o valor "texto" no campo id
recebemos o seguinte erro:
{
"id": [
"The value 'texto' is not valid."
]
}
Isso previne que a aplicação processe requisições com dados inválidos. Mas se minha aplicação possui vários controllers eu preciso repetir essa verificação em todos?
O atributo do ASP.NET ApiControllerAttribute pode ser aplicado à Controllers e traz algumas funcionalidades. Entre elas, ele faz a validação automática dos dados de entrada e retorna um erro 400 de maneira similar à verificação do ModelState
. Alterando o Controller de exemplo:
[ApiController]
[Route("[controller]")]
public class ExampleController : ControllerBase
{
[HttpGet]
public ActionResult Get(int id)
{
if (id == 1)
return Ok(new ExampleRequest{Name = "Example1"});
return NotFound();
}
}
E ao fazer a requisição com o valor inválido obtemos o mesmo erro quando usamos a verificação do ModelState
. Isso ocorre porque o filtro ModelStateInvalidFilter
é adicionado a todos os Controllers que são anotados com o ApiControllerAttribute
.
Além da validação do ModelState
, o ApiControllerAttribute
traz outras informações de erro no seu resultado:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-a89db4d9a02dfc479c4d50b401f60fb5-28ba31ad1392154c-00",
"errors": {
"id": [
"The value 'texto' is not valid."
]
}
}
Por padrão o atributo tem como resposta o formato acima contendo:
- type: o link da RFC em que determina os tipos de resposta HTTP, especificamente para a sessão do erro 400;
- status: o código do status de erro;
- traceId: o traceId da requisição. Por padrão, o ASP.NET 5 utiliza o formato definido pela recomendação da W3C. Você pode encontrar mais informações sobre o traceId e o trace context aqui;
- errors: uma lista de erros contendo o erro de validação do modelo.
É possível também decorar o assembly com o ApiControllerAttribute
. Isso pode ser feito decorando a declaração do namespace que contém a classe Startup
. Dessa forma, o comportamento do ApiControllerAttribute
será aplicado a todos os controllers do assembly
[assembly: ApiController]
namespace WebApiSample
{
public class Startup
{
...
}
}
A validação dos tipos de dados é importante mas normalmente queremos aplicar outras validações ao nossos dados de entrada. Por exemplo, podemos marcar campos como obrigatórios, tamanho mínimo ou máximo e regras mais complexas. É importante garantir que a nossa aplicação só vai processar dados válidos. Isso também evita que o código da aplicação tenha uma quantidade de if
s e else
s que acabam poluindo o código. Vejam o exemplo abaixo:
[HttpPost]
public ActionResult Add(ExampleRequest example)
{
return Ok();
}
...
public class ExampleRequest
{
[Required]
public string Name { get; set; }
}
O exemplo utiliza o atributo Required
pressente no namespace System.ComponentModel.DataAnnotations.
Realizando uma requisição para a nova rota de POST com o body vazio obtemos o erro:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-421e7740cdb1394aba958b549d319bc2-ffc80b3fe2883349-00",
"errors": {
"Name": [
"The Name field is required."
]
}
}
Existem vários outros atributos (a lista completa pode ser vista aqui) e também é possível estender essa funcionalidade criando atributos customizados herdando a classe ValidationAttribute
como apresentado no exemplo:
public class ExampleRequest
{
[Required]
public string Name { get; set; }
[StringLength(1000)]
public string Description { get; set; }
[Range(1, 100)]
public int SomeValue { get; set; }
[EmailAddress]
public string Email { get; set; }
[IsEven]
public int EvenNumber { get; set; }
}
public class IsEvenAttribute : ValidationAttribute
{
public IsEvenAttribute() : base ("Value is not an even number")
{
}
public override bool IsValid(object value)
{
var intValue = Convert.ToInt32(value);
return intValue % 2 == 0;
}
}
O mesmo efeito pode ser obtido utilizando o FluentValidation configurando a sua integração com o ASP.NET.
Para alguns casos a resposta de erro padrão que o ApiControllerAttribute
envia pode ser indequada para a aplicação. Por exemplo, os campos status
e type
são redundantes considerando que o código da resposta HTTP já é retornado na requisição. Além disso, caso a aplicação retorne outros tipos de erros 400 pode ser necessário incluir novos campos na resposta.
Para isso o ASP.NET possui uma funcionalidade que permite alterar o formato da resposta. Deve-se utilizar a classe ApiBehaviorOptions
para alterar o comportamento de todos os Controllers anotados com o ApiControllerAttribute
. Essa configuração deve ser feita no método ConfigureServices
da classe Startup
da aplicação. Deve ser chamado o método ConfigureApiBehaviorOptions
preenchendo a propriedade InvalidModelStateResponseFactory
com a customização da resposta.
Segue o exemplo abaixo:
services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
options.InvalidModelStateResponseFactory = context =>
{
var response = new
{
Error = new Dictionary<string, string[]>(),
Type = "VALIDATION_ERRORS"
};
foreach (var (key, value) in context.ModelState)
response.Error.Add(key, value.Errors.Select(e => e.ErrorMessage).ToArray());
return new BadRequestObjectResult(response);
};
});
O código acima simplifica o retorno da API, trazendo apenas a lista de erros e um novo campo para indicar que o motivo do erro é de validação dos dados. Fazendo novamente a requisição problemática recebemos o erro:
{
"error": {
"Name": [
"The Name field is required."
]
},
"type": "VALIDATION_ERRORS"
}
É importante notar que apenas erros de validação, ou seja, que o ModelState
é ínválido, são afetados por essa customização.
O ASP.NET possui funcionalidades para ajudar os desenvolvedores criarem APIs mais robustas aplicando validação de dados de entrada. Além disso, existem ótimas bibliotecas como o Fluent Validation que permite mais liberdade para criação de validadores de modelos mais inteligentes. O uso do atributo ApiController
do ASP.NET incrementa as APIs adicionando funcionalidades como a resposta 400 automática para erros de validação padrão. No entanto, se desejado, é possível customizar o resultado de forma simples usando o InvalidModelStateResponseFactory
.