ASP.NET Core - Tratamento global de exceções (.NET 8)


 Neste artigo veremos como realizar o tratamento global de exceções em aplicações ASP.NET Core no .NET 8.

Capturar e tratar erros(exceções) é uma das tarefas obrigatórias para todo o programador, e um dos recursos oferecidos para fazer isso na linguagem C# é o bloco try-catch.

O bloco try-catch-finally é usado para envolver o código onde existe a possibilidade de uma exceção/erro ocorrer e é constituído das seguintes seções :

  1. O código que pode gerar uma exceção é colocando dentro do bloco try;
  2. Se o erro/exceção ocorre,r o bloco catch entra em ação e o você faz o tratamento do erro;
  3. Dentro do bloco finally você coloca o código que deverá ser executado sempre, quer ocorra ou não a exceção.

Agora ter que definir blocos try-catch em todos os métodos Action de um controlador em sua API pode ser uma tarefa trabalhosa e sujeita a erros, além de deixar o código mais difícil de manter.

Par evitar isso podemos capturar todas as exceções não tratadas usando um manipulador de exceção em um único lugar e não vamos precisar usar o bloco try-catch nos métodos dos controladores. Para isso precisamos criar um middleware customizado que vai fazer o tratamento de exceções.

Usar um middleware permite introduzir lógica antes ou depois da execução de solicitações HTTP e podemos estender isso facilmente para implementar o tratamento de exceções adicionando uma instrução try-catch no middleware e retornar uma resposta HTTP de erro.

Usando middleware no .NET 8

A ASP.NET Core 8 apresenta uma nova abstração através da interface IExceptionHandler para gerenciar exceções, e, o middleware integrado do manipulador de exceções usa implementações de IExceptionHandler para lidar com exceções.

Esta interface possui apenas o método TryHandleAsync que tenta manipular a exceção especificada no pipeline do ASP.NET Core. Se a exceção puder ser tratada, ela deverá retornar verdadeiro, e se a exceção não puder ser tratada, ela deverá retornar falso. Isso permite implementar uma lógica personalizada de tratamento de exceções para diferentes cenários.

A seguir temos uma implementação para uma classe GlobalExceptionHandler :

using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;

namespace APICatalogo.Exceptions;

public class GlobalExceptionHandler : IExceptionHandler
{
    private readonly ILogger<GlobalExceptionHandler> _logger;

    public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
    {
        _logger = logger;
    }
    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        _logger.LogError(
            exception, "Exception occurred: {Message} ",
            exception.Message);

        var problemDetails = new ProblemDetails
        {
            Status = StatusCodes.Status500InternalServerError,
            Title = "Server Error"
        };

        httpContext.Response.StatusCode = problemDetails.Status.Value;

        await httpContext.Response
            .WriteAsJsonAsync(problemDetails, cancellationToken);

        return true;
    }
}

Configurando a implementação de IExceptionHandler

Precisamos de duas coisas para adicionar uma implementação IExceptionHandler ao pipeline de solicitação do ASP.NET Core:

1- Registrar o serviço IExceptionHandler com injeção de dependência;
2- Registrar o ExceptionHandlerMiddleware com o pipeline de solicitação;

...
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();

builder.Services.AddProblemDetails();
...
var
app = builder.Build();
...

Podemos chamar o método AddExceptionHandler para registrar GlobalExceptionHandler como um serviço e isso é feito com o tempo de vida Singleton, por isso tenha cuidado ao injetar serviços com um tempo de vida diferente.

Também estamos invocando AddProblemDetails para gerar uma resposta de detalhes do problema para exceções comuns.

Além disso precismos também chamar UseExceptionHandler para adicionar ExceptionHandlerMiddleware ao pipeline de solicitação:


app.UseExceptionHandler();    

...

app.Run();

Encadeando manipuladores de exceção

Podemos adicionar várias implementações de IExceptionHandler e elas são chamadas na ordem em que são registradas. Um possível caso de uso para isso é usar exceções para controle de fluxo.

Você pode definir exceções personalizadas como BadRequestException e NotFoundException. Elas correspondem ao código de status HTTP que você retornaria da API.

Aqui está uma implementação de BadRequestExceptionHandler:

public class BadRequestExceptionHandler : IExceptionHandler
{
    private readonly ILogger<BadRequestExceptionHandler> _logger;

    public BadRequestExceptionHandler(ILogger<BadRequestExceptionHandler> logger)
    {
        _logger = logger;
    }
    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {

        if(exception is not BadHttpRequestException badRequestException)
        {
            return false;
        }

        _logger.LogError(
            exception, "Exception occurred: {Message} ",
            badRequestException.Message);

        var problemDetails = new ProblemDetails
        {
            Status = StatusCodes.Status400BadRequest,
            Title = "BadRequest",
            Detail = badRequestException.Message
        };

        httpContext.Response.StatusCode = problemDetails.Status.Value;

        await httpContext.Response
            .WriteAsJsonAsync(problemDetails, cancellationToken);

        return true;
    }
}

Não esqueça que temos que registrar a exceção usando  AddExceptionHandler na classe Program:


builder.Services.AddExceptionHandler<BadRequestExceptionHandler>();
 

Desta forma , usar um middleware para tratamento de exceções é uma excelente solução no ASP.NET Core. No entanto, é ótimo termos novas opções usando a interface IExceptionHandler.

A seguir temos as vantagens e desvantagens desta abordagem:

  • Vantagens:
  • Desvantagens:
  • Podemos considerar também a utilização do padrão Result como uma alternativa mais robusta para realizar o tratamento de exceções. (Iremos abordar este assunto em outro artigo)

    Este padrão oferece as seguintes vantagens e desvantagens:
     

  • Vantagens:
  • Desvantagens:
  • Em muitos casos, uma abordagem combinada pode ser eficaz.

    Você pode usar um middleware para capturar exceções globais e realizar ações de nível de aplicação, enquanto utiliza o padrão Result nos controladores para um tratamento mais granular e específico.

    E estamos conversados

    "(Disse Jesus) Em verdade, em verdade vos digo que vem a hora e já chegou, em que os mortos ouvirão a voz do Filho de Deus; e os que a ouvirem viverão."
    João 5:25

    Referências:


    José Carlos Macoratti