.NET  -  Tratamento global de exceções


  O tratamento de exceções é uma das tarefas mais importantes no ciclo de desenvolvimento de uma aplicação robusta.

Hoje veremos uma forma efetiva de realizar o tratamento de exceções usando o .NET 6 com aplicações ASP .NET Core.

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.

Estrutura do bloco try-catch :

Try
 
 'Código que pode gerar um erro.
Catch
   '
Código para tratamento de erros.
Finally
  
'Código de execução obrigatória.

End Try

Tratando exceções com o bloco try-catch

Usar o bloco try-catch é a abordagem básica e tradicional para realizar o tratamento de exceções. Para mostrar a sua utilização temos a seguir um trecho de código de um projeto ASP .NET Core Web API:

[Route("[controller]")]
[ApiController]
public class ProdutosController : ControllerBase
{
    private readonly AppDbContext _context;
    private readonly ILogger<ProdutosController> _logger;

    public ProdutosController(AppDbContext context,
                ILogger<ProdutosController> logger)
    {
        _context = context;
        _logger = logger;
    }

    [HttpGet]
    public ActionResult<IEnumerable<Produto>> GetProdutos()

    {
        try
        {
            _logger.LogInformation("Obtendo todos os produtos");

            var produtos = _context.Produtos.ToList();
            if (produtos is null)
            {
                return NotFound();
            }
            throw new Exception  
            return produtos;
        }
        catch (Exception e)
        {
            _logger.LogError(e.Message);
            return BadRequest("Erro ao acessar produtos");
        }
    }
    ...
}

Neste código temos o endpoint definido no método GetProdutos que retorna uma lista de produtos onde temos um bloco try-catch que vai tratar possíveis erros que ocorram no código definido dentro do bloco try e exibir a mensagem : "Erro ao acessar produtos" ao usuário.

Abaixo temos um exemplo de acesso ao endpoint da API usando o Postman e o erro obtido sendo capturado e exibido :

Examinando a janela OutPut no Debug temos as mensagens de Log exibidas conforme esperado:

Temos assim um exemplo de try-catch que funciona e que é uma abordagem muito boa para o desenvolvimento para quem esta iniciando. Entretanto, esta abordagem tem uma desvantagem quando usada em grandes projetos :  a quantidade de código que deve ser incluída.

Imagine que você tem um projeto com 10 controladores e que cada controlador possua 10 métodos onde você vai precisar incluir o bloco try-catch para tratar os erros.

Além do grande trabalho braçal que você vai ter que fazer incluindo centenas de linhas de código isso vai aumentar a probabilidade de erros e acaba se tornando um código repetitivo.

Aqui vai entrar a segunda abordagem para realizar o tratamento de erros.

Tratamento de erros global com middleware customizado

Essa é uma abordagem boa e eficaz para lidar com o nível global de exceção.

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.

Vamos criar uma pasta Middlewares no projeto e nesta pasta vamos criar a classe ExceptionHandlingMiddleware:

public class ExceptionHandlingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ExceptionHandlingMiddleware> _logger;

    public ExceptionHandlingMiddleware(RequestDelegate next,
        ILogger<ExceptionHandlingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext httpContext)
    {
        try
        {
            await _next(httpContext);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(httpContext, ex);
        }
    }
    private async Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        context.Response.ContentType = "application/json";
        var response = context.Response;

        var errorResponse = new ErrorResponse
        {
            Success = false
        };
        switch (exception)
        {
            case ApplicationException ex:
                if (ex.Message.Contains("Token Inválido"))
                {
                    response.StatusCode = StatusCodes.Status403Forbidden;
                    errorResponse.Message = ex.Message;
                    break;
                }
                response.StatusCode = StatusCodes.Status400BadRequest;
                errorResponse.Message = ex.Message;
                break;
            case KeyNotFoundException ex:
                response.StatusCode = StatusCodes.Status404NotFound;
                errorResponse.Message = ex.Message;
                break;
            default:
                response.StatusCode = StatusCodes.Status500InternalServerError;
                errorResponse.Message = "Internal Server error. Verifique os Logs!";
                break;
        }
        _logger.LogError(exception.Message);
        var result = JsonSerializer.Serialize(errorResponse);
        await context.Response.WriteAsync(result);
    }
}

Temos acima o código do middleware personalizado para lidar com as exceções. Nesse caso, primeiro, precisamos registrar ILogger e RequestDelegateservices usando a injeção de dependência.

O parâmetro _next do tipo RequestDeleagate é um delegate de função que pode lidar com nossas solicitações HTTP. Além disso, o delegate de solicitação é passado para o middleware e isso é processado pelo middleware ou passado para o próximo middleware na cadeia.

Se a solicitação não for bem-sucedida, pode haver uma exceção e o método HandleExceptionAsync será executado para capturar a exceção de acordo com o tipo de exceção. Nesse caso, você pode alternar instruções para identificar o tipo de exceção e, em seguida, podemos usar o código de status adequado de acordo com a exceção como um exemplo de código acima.

Além disso, não precisamos passar as mensagens de exceção para o lado do cliente do projeto. Nesse caso, poderíamos passar a mensagem personalizada e usar o ILogger para registrar a mensagem de exceção como um erro. Então poderíamos identificar a mensagem de exceção verificando os logs.

O código também usa a classe ErrorResponse e temos que criar esta classe na pasta Models:


public class ErrorResponse
{
    public bool Success { get; set; }
    public string? Message { get; set; }
}

Agora só falta habilitar o middleware criado na classe Program:

...

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseMiddleware<ExceptionHandlingMiddleware>();

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

A seguir vamos ajustar o código no controlador ProdutosController removendo os blocos try-catch e definindo o código como abaixo:

[Route("[controller]")]
[ApiController]
public class ProdutosController : ControllerBase
{
    private readonly AppDbContext _context;
    private readonly ILogger<ProdutosController> _logger;

    public ProdutosController(AppDbContext context, ILogger<ProdutosController> logger)
    {
        _context = context;
        _logger = logger;
    }

    [HttpGet]
    public ActionResult<IEnumerable<Produto>> GetProdutos()

    {
            _logger.LogInformation("#### GetProdutos -> Obtendo todos os produtos");

            var produtos = _context.Produtos.ToList();
            if (produtos is null)
            {
                return NotFound();
            }

            if (produtos.Count == 0)
               throw new KeyNotFoundException("Nenhum produto encontrado...");

            throw new Exception("Erro ao acessar produtos");
            //return produtos;    
    }

    [HttpGet("{id:int}", Name = "ObterProduto")]
    public ActionResult<Produto> Get(int id)

    {
        var produto = _context.Produtos.FirstOrDefault(p => p.ProdutoId == id);
        if (produto is null)
        {
            //return NotFound("Produto não encontrado...");
            throw new KeyNotFoundException("Nenhum produto encontrado...");
        }
        return produto;
    }
   ...
}

Neste código definimos apenas dois métodos para testar a implementação do nosso middleware  customizado onde estamos lançando exceções para retornar todos os produtos e para obter um produto pelo id quando o valor do id não for encontrado.

Os resultados obtidos usando o Postman podem ser vistos abaixo:

1- Obter todos os produtos  

2- Obter um produto pelo seu Id

A abordagem de usar um middleware customizado para tratamento de erros é muito boa para grandes projetos e evita que você tenha que usar os blocos try-catch em todos os métodos dos seus controladores.

Isso aumenta a legibilidade do código, mantém o seu código limpo e mais fácil de ser reutilizado e continua realizando o tratamento de exceções.

Pegue o projeto aqui:   TrataErroGlobal.zip

"Jesus dizia a todos: "Se alguém quiser acompanhar-me, negue-se a si mesmo, tome diariamente a sua cruz e siga-me."
Lucas 9:23

Referências:


José Carlos Macoratti