Skip to content

Texto sobre como implementar validação e customização da resposta de erro no ASP.NET.

Notifications You must be signed in to change notification settings

rafaelpadovezi/aspnet-validation-error-response

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Validação de entrada de dados e respostas de erro no ASP.NET

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.

Erros de Model Binding

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.

Pipelines de filtro do ASP.NET. A etapa de Model Binding é executada antes dos filtros de ação

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.

ModelState

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?

[ApiController]

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
    {
        ...
    }
}

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. É 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 ifs e elses 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.

Customização da resposta de erro

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.

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 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.

Referências

About

Texto sobre como implementar validação e customização da resposta de erro no ASP.NET.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages