ASP.NET Core - Usando ProblemDetails
  Neste artigo veremos como usar ProblemDetails na ASP.NET Core.


Para que uma API seja robusta, sustentável e utilizável, deve haver consistência na maneira como as respostas são enviadas aos clientes. Uma forma de alcançar este objetivo é usar o middleware ProblemDetails para gerar resultados detalhados para as exceções que ocorrem em uma API.

 


 

O ProblemDetails é um middleware ASP.NET Core de código aberto desenvolvido por Kristian Hellang que pode ser usado para gerar resultados detalhados para as exceções que ocorrem em seu aplicativo. Ele lida com exceções em seu pipeline de middleware e as converte em ProblemDetails.
 

Um dos objetivos das APIs REST é a consistência, ou seja, poder emitir respostas de forma consistente. Ao trabalhar com essas APIs, pode ser necessário definir formatos de resposta para os erros que ocorrem em seu aplicativo. Embora você possa usar códigos de status HTTP para essa finalidade, muitas vezes você desejará comunicar informações mais detalhadas aos clientes do que eles fornecem.

Por exemplo, considere a falha de um pagamento em um aplicativo de carrinho de compras. O pagamento pode ter falhado por vários motivos, como fundos insuficientes, informações incorretas do cartão de crédito, código da senha incorreto ou falha no sistema de processamento de transações. Portanto, é imperativo que haja uma maneira padrão pela qual essas mensagens de erro possam ser enviadas ao cliente.

A Internet Engineering Task Force (IETF) publicou um documento em março de 2016, chamado Problem Details For HTTP APIs, que define um formato que pode ser usado para enviar detalhes legíveis por máquina sobre os erros que ocorrem em um aplicativo.

 

Você pode aproveitar esse formato no middleware ProblemDetails, que é uma implementação para ASP.NET Core, para definir erros e mensagens de erro nas respostas da sua API REST. Além disso, todas as suas exceções podem ser tratadas em um só lugar: você sempre pode retornar uma instância de ProblemDetails ao cliente consumidor, independentemente do tipo de erro que ocorreu.

 

Ao usar ProblemDetails para emitir mensagens de resposta de erro, você permite que os consumidores da API reajam aos erros de forma mais eficaz. Isso torna sua API consistente e confiável.

 

Usando ProblemDetails na ASP.NET Core

 

Vamos criar um projeto no VS 2022 usando o template ASP.NET Core Web API com o nome AspNetProblemDetails.

A seguir vamos instalar o seguinte pacote no projeto:

A seguir vamos configurar a utilização do middleware no projeto incluindo na classe Program o código abaixo destacado em azul :

 

using Hellang.Middleware.ProblemDetails;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddProblemDetails();
var app = builder.Build();
// Configure the HTTP request pipeline.
app.UseSwagger();
app.UseSwaggerUI();
app.UseProblemDetails();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();

 

Agora as respostas que não são exceptions são convertidas em instâncias de ProblemDetails quando qualquer uma das seguintes condições for verdadeira:

1- Os cabeçalhos Content-Type ou Content-Length estão vazios;
2- O código de status HTTP da resposta está entre 400 e 600;


Agora vamos alterar o código do controlador WeatherForecast substituindo o método Action Get pelo código abaixo:

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private readonly ILogger<WeatherForecastController> _logger;
    public WeatherForecastController(ILogger<WeatherForecastController> logger)
    {
        _logger = logger;
    }
    [HttpGet("DivisaoPor/{x}")]
    public ActionResult Get(int x)
    {
        int i;
        try
        {
            i = 10 / x;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex.Message);
            return BadRequest();
        }
        return Ok(i);
    }
}

Neste código definimos um método Action Get que vai receber um valor x como parâmetro e vai tentar realizar a divisão de 10 por x.

Se o valor zero for informado o resultado da divisão será indefinido e será lançada uma Exception em tempo de execução que capturada no bloco catch vai retornar um BadRequest com status Code igual a 400.

Executando o projeto e informando o valor 0 para x teremos o resultado abaixo:

Observe que temos um tipo padrão de mensagem sendo exibida segundo o formato :

Este é o formato padrão do ProblemDetails.

Podemos também criar uma instância da classe Microsoft.AspNetCore.Mvc.ProblemDetails e passá-la de volta para o cliente, conforme mostrado no código do bloco catch a seguir:

[HttpGet("DivisaoPor/{x}")]
    public ActionResult Get(int x)
    {
        int i;
        try
        {
            i = 10 / x;
        }
        catch (Exception ex)
        {
            var problemDetails = new ProblemDetails
            {
                Status = StatusCodes.Status403Forbidden,
                Type = "https://www.macoratti.net",
                Title = "Divisão por zero...",
                Detail = ex.StackTrace,
                Instance = HttpContext.Request.Path
            };
            return BadRequest(problemDetails);
        }
        return Ok(i);
    }

Ao executar novamente e informar o valor de 0 para x iremos obter a seguinte mensagem retornada pela API:

Como podemos ver nos cabeçalhos de resposta, o objeto JSON de ProblemDetails é do tipo “application/problem + json”. Conforme especifica a documentação, ela contém os seguintes membros:

Type – [string] – referência de URI para identificar o tipo de problema;
Title – [string] – um breve resumo do problema legível por humanos;
Status – [número] – o código de status HTTP gerado na ocorrência do problema;
Detail – [string] – uma explicação legível para o que exatamente aconteceu;
Instance – [string] – referência URI da ocorrência;

Podemos ir além e custimizar o comportamento do ProblemDetails.

Customizando o ProblemDetails

Podemos personalizar o comportamento do middleware ProblemDetails usando ProblemDetailsOptions no na configuração conforme mostrado abaixo.

using Hellang.Middleware.ProblemDetails;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddProblemDetails(setup =>
{
    setup.IncludeExceptionDetails = (ctx, env) => builder.Environment.IsDevelopment() 
                                                   || builder.Environment.IsStaging();
});
var app = builder.Build();
// Configure the HTTP request pipeline.
app.UseSwagger();
app.UseSwaggerUI();
app.UseProblemDetails();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run(); 

Agora com esta configuração definimos para exibir os detalhes da exceção apenas no ambiente de desenvolvimento ou teste.

Estendendo a classe ProblemDetails

Se quisermos criar nossa classe de exceção personalizada e mapeá-la para o objeto ProblemDetails criado pelo middleware, também podemos fazer isso:

public class ProdutoCustomException : Exception
{
    public string AdditionalInfo { get; set; }
    public string Type { get; set; }
    public string Detail { get; set; }
    public string Title { get; set; }
    public string Instance { get; set; }

    public ProdutoCustomException(string instance)
    {
        Type = "produto-custom-exception";
        Detail = "Ocorreu umerro inesperado ao tentar obter o produto.";
        Title = "Exceção de Produto Customizada";
        AdditionalInfo = "Talvez se você tentar novamente, quem sabe...?";
        Instance = instance;
    }
}
public class ProdutoCustomDetails : ProblemDetails
{
    public string AdicionalInfo { get; set; }
}

Primeiro, criamos uma classe de exceção personalizada e, em seguida, também estendemos a classe ProblemDetails para atender às nossas necessidades.

Com nossa exceção personalizada, queremos mostrar ao usuário algumas informações adicionais sobre o erro. Para isso, em nossa classe de exceção personalizada, adicionamos uma propriedade AdicionalInfo personalizada. Também estendemos a classe ProblemDetails com a propriedade de mesmo nome, para que possamos mapear tudo corretamente.

Depois disso, vamos atualizar a configuração para mapear nossa exceção personalizada para a classe ProdutoCustomDetails:

using AspNetProblemDetails.Models;
using Hellang.Middleware.ProblemDetails;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddProblemDetails(setup =>
{
    setup.IncludeExceptionDetails = (ctx, env) => builder.Environment.IsDevelopment()
                                  || builder.Environment.IsStaging();

    setup.Map<ProdutoCustomException>(exception => new ProdutoCustomDetails
    {
        Title = exception.Title,
        Detail = exception.Detail,
        Status = StatusCodes.Status500InternalServerError,
        Type = exception.Type,
        Instance = exception.Instance,
        AdicionalInfo = exception.AdditionalInfo
    });
});

var app = builder.Build();

// Configure the HTTP request pipeline.
app.UseSwagger();
app.UseSwaggerUI();

app.UseProblemDetails();

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();

Com isso vimos que as mensagens geradas pelo middleware ProblemDetails usa um formato formato legível por máquina que é usado para padronizar mensagens de erro em controladores de API e representar respostas de API HTTP com base na especificação IETF Problem Details.

O namespace Microsoft.AspNetCore.Mvc contém uma classe ProblemDetails que você pode usar para enviar uma resposta de erro legível por máquina e o middleware ProblemDetails de Kristian Hellang facilita isso.

Pegue o projeto aqui :   AspNetProblemDetails.zip (sem as referências) ... 

"Olhai para as aves do céu, que nem semeiam, nem segam, nem ajuntam em celeiros; e vosso Pai celestial as alimenta. Não tendes vós muito mais valor do que elas?"
Mateus 6:26

Referências:


José Carlos Macoratti