Skip to content

Commit

Permalink
feature: add more validation and update readme
Browse files Browse the repository at this point in the history
  • Loading branch information
rafaelpadovezi committed Aug 17, 2021
1 parent a616247 commit d530d62
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 19 deletions.
83 changes: 69 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
# Validação de entrada de dados e respostas de erro no ASP.NET

Nesse texto será discutido 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 este formato. Os exemplos de código foram desenvolvidos usando a versão 5 do ASP.NET.
Validação dos dados de entrada é parte essencial no desenvolvimento de software. O framework ASP.<span>NET</span> provê um conjunto de funcionalidades que auxiliam os desenvodores garantir que as APIs só processem dados que possuem valores que atendam as regras da aplicação. Nesse texto será discutido 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.<span>NET</span>. Além disso, será mostrado como customizar este formato. Os exemplos de código foram desenvolvidos usando a versão 5 do ASP.<span>NET</span>.

## Erros de Model Binding

[Model binding](https://docs.microsoft.com/pt-br/aspnet/core/mvc/models/model-binding?view=aspnetcore-5.0#what-is-model-binding) é a funcionalidade do ASP.NET que atua nas requisições HTTP convertendo os dados de entrada nas rotas em tipos .NET. Considere o `Controller` de exemplo:
[Model binding](https://docs.microsoft.com/pt-br/aspnet/core/mvc/models/model-binding?view=aspnetcore-5.0#what-is-model-binding) é a funcionalidade do ASP.<span>NET</span> 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.

![alt text](./docs/filter-pipeline-2.png "Title")

Considere o `Controller` de exemplo:

```c#
[Route("[controller]")]
Expand Down Expand Up @@ -60,7 +66,7 @@ Isso previne que a aplicação processe requisições com dados inválidos! Mas

### [ApiController]

O atributo do ASP.NET [ApiController](https://docs.microsoft.com/en-us/aspnet/core/web-api/?view=aspnetcore-5.0#apicontroller-attribute) pode ser aplicado à Controllers e trás 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 nosso Controller de exemplo:
O atributo do ASP.<span>NET</span> [ApiControllerAttribute](https://docs.microsoft.com/en-us/aspnet/core/web-api/?view=aspnetcore-5.0#apicontroller-attribute) pode ser aplicado à Controllers e trás 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 nosso Controller de exemplo:

```c#
[ApiController]
Expand All @@ -78,7 +84,8 @@ public class ExampleController : ControllerBase
}
```

E ao fazer a requisição com o valor inválido obtemos o mesmo erro quando usamos a verificação do `ModelState`. Além disso, o `ApiController` trás outras informações de erro no seu resultado:
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` são adicionados a todos os Controllers que são anotados com o `ApiControllerAttribute`.
Além disso da vaidação do `ModelState`, o `ApiControllerAttribute` trás outras informações de erro no seu resultado:

```json
{
Expand All @@ -100,6 +107,19 @@ Ou seja, por padrão o atributo apresenta o formato acima contendo:
- **traceId**: o traceId da requisição. Por padrão, o ASP.NET 5 utiliza o formato definido [pela recomendação da W3C](https://www.w3.org/TR/trace-context/). Você pode encontrar mais informações sobre o traceId e o trace context [aqui](https://dev.to/luizhlelis/using-w3c-trace-context-standard-in-distributed-tracing-3743);
- **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`.

```c#
[assembly: ApiController]
namespace WebApiSample
{
public class Startup
{
...
}
}
```

## Validação do modelo

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. Ou seja, é 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:
Expand All @@ -118,7 +138,7 @@ public class ExampleRequest
}
```

O exemplo utiliza o atributo `Required` pressente no namespace [System.ComponentModel.DataAnnotations](https://docs.microsoft.com/pt-br/aspnet/core/mvc/models/validation?view=aspnetcore-5.0#validation-attributes). Existem vários outros atributos e também possível extender essa funcionalidade criando atributos customizados.
O exemplo utiliza o atributo `Required` pressente no namespace [System.ComponentModel.DataAnnotations](https://docs.microsoft.com/pt-br/aspnet/core/mvc/models/validation?view=aspnetcore-5.0#validation-attributes).

Realizando uma requisição para a nova rota de POST com o body vazio obtemos o erro:

Expand All @@ -136,13 +156,46 @@ Realizando uma requisição para a nova rota de POST com o body vazio obtemos o
}
```

Pode se notar que a resposta segue o mesmo padrão do erro por Model Binding.
Existem vários outros atributos (a lista completa pode ser vista [aqui](https://docs.microsoft.com/pt-br/dotnet/api/system.componentmodel.dataannotations?view=net-5.0)) e também possível extender essa funcionalidade criando atributos customizados herdando a classe `ValidationAttribute` como apresentado no exemplo:

```c#
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](https://docs.fluentvalidation.net/en/latest/index.html) configurando a sua [integração com o ASP.NET](https://docs.fluentvalidation.net/en/latest/aspnet.html).

## Customização da resposta de erro

Para alguns casos a resposta de erro padrão que o `ApiController` 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](https://docs.microsoft.com/en-us/aspnet/core/web-api/handle-errors?view=aspnetcore-5.0#validation-failure-error-response) que permite alterar o formato da resposta. 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:
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](https://docs.microsoft.com/en-us/aspnet/core/web-api/handle-errors?view=aspnetcore-5.0#validation-failure-error-response) que permite alterar o formato da resposta. 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. A classe `ApiBehaviorOptions` é usada para alterar o comportameno de todos os controllers anotados com o `ApiControllerAttribute`.

Segue o exemplo abaixo:

```c#
services.AddControllers()
Expand Down Expand Up @@ -180,13 +233,15 @@ O código acima simplifica o retorno da API, trazendo apenas a lista de erros e

## Conclusã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 APIs 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`.
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 APIs como o Fluent Validation que permite mais liberdade para criação de validadores de modelos mais inteligentes. O uso do atributo `ApiController` do ASP.<span>NET</span> 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`.


## Referências
https://docs.microsoft.com/pt-br/aspnet/core/mvc/models/model-binding?view=aspnetcore-5.0
https://docs.microsoft.com/en-us/aspnet/core/web-api/?view=aspnetcore-5.0
https://docs.microsoft.com/pt-br/aspnet/core/mvc/models/validation?view=aspnetcore-5.0
https://docs.microsoft.com/en-us/aspnet/core/tutorials/first-web-api?view=aspnetcore-5.0&tabs=visual-studio
https://docs.microsoft.com/pt-br/aspnet/core/web-api/handle-errors?view=aspnetcore-5.0
https://github.com/dotnet/aspnetcore/blob/52eff90fbcfca39b7eb58baad597df6a99a542b0/src/Mvc/Mvc.Core/src/Infrastructure/ModelStateInvalidFilter.cs
- [Filtros no ASP.NET Core](https://docs.microsoft.com/pt-br/aspnet/core/mvc/controllers/filters?view=aspnetcore-5.0)
- [Model binding no ASPNET Core](https://docs.microsoft.com/pt-br/aspnet/core/mvc/models/model-binding?view=aspnetcore-5.0)
- [Criar APIs Web com o ASP.NET Core](https://docs.microsoft.com/en-us/aspnet/core/web-api/?view=aspnetcore-5.0)
- [Validação de modelo no ASP.NET Core MVC e Razor páginas](https://docs.microsoft.com/pt-br/aspnet/core/mvc/models/validation?view=aspnetcore-5.0)
- [Tutorial: criar uma API Web com o ASP.NET Core](https://docs.microsoft.com/en-us/aspnet/core/tutorials/first-web-api?view=aspnetcore-5.0&tabs=visual-studio)
- [Handle errors in ASP.NET Core web APIs](https://docs.microsoft.com/pt-br/aspnet/core/web-api/handle-errors?view=aspnetcore-5.0)
- [ModelStateInvalidFilter.cs](https://github.com/dotnet/aspnetcore/blob/52eff90fbcfca39b7eb58baad597df6a99a542b0/src/Mvc/Mvc.Core/src/Infrastructure/ModelStateInvalidFilter.cs)
- [Attribute on an assembly](https://docs.microsoft.com/en-us/aspnet/core/web-api/?view=aspnetcore-5.0#attribute-on-an-assembly)
Binary file added docs/filter-pipeline-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions src/Configuration/IsEvenAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System;
using System.ComponentModel.DataAnnotations;

namespace ExampleApi.Configuration
{
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;
}
}
}
10 changes: 9 additions & 1 deletion src/Controllers/ExampleController.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
using ExampleApi.Configuration;
using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations;

namespace ExampleApi.Controllers
{
[ApiController]
[Route("[controller]")]
public class ExampleController : ControllerBase
{
Expand All @@ -27,5 +27,13 @@ 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; }
}
}
5 changes: 1 addition & 4 deletions src/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models;

[assembly: ApiController]
namespace ExampleApi
{
public class Startup
Expand Down

0 comments on commit d530d62

Please sign in to comment.