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

rafaelpadovezi

Rafael

Posted on August 18, 2021

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 e estão disponíveis no github.

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();
    }
}


Enter fullscreen mode Exit fullscreen mode

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();
    }
}


Enter fullscreen mode Exit fullscreen mode

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."
    ]
}


Enter fullscreen mode Exit fullscreen mode

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();
    }
}


Enter fullscreen mode Exit fullscreen mode

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."
        ]
    }
}


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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; }
}


Enter fullscreen mode Exit fullscreen mode

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."
        ]
    }
}


Enter fullscreen mode Exit fullscreen mode

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;
    }
}


Enter fullscreen mode Exit fullscreen mode

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);
        };
    });


Enter fullscreen mode Exit fullscreen mode

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"
}


Enter fullscreen mode Exit fullscreen mode

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

Obrigado por ter chegado até aqui e espero que tenha gostado. Dúvidas, sugestões ou encontrou algum erro? Por favor, deixe seu comentário.

Referências

💖 💪 🙅 🚩
rafaelpadovezi
Rafael

Posted on August 18, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related