Tratamento de exceções personalizado na API .NET
Exceções são ruins, nós sabemos, certo? Mas e se tivermos que lidar com eles?
O que acontece quando temos uma exceção, por exemplo, em uma API, ela exibe uma mensagem na pilha que inclui muitas informações que podemos querer remover da resposta que nossos usuários recebem.
Para a demonstração, criei uma API dotnet e adicionei um método que lançará uma exceção:
[HttpGet]
[Route("GetWithoutExceptionHandler")]
public Task GetWithoutExceptionHandler()
{
throw new Exception("This is a custom exception!");
}
Se lançarmos uma exceção, ficará assim:
System.Exception: This is a custom exception!
at CustomExceptionHandleDemo.Controllers.WeatherForecastController.GetWithoutExceptionHandler()
at lambda_method16(Closure , Object , Object[] )
at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.AwaitableResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Logged|12_1(ControllerActionInvoker invoker)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
Isso parece normal, é isso que você obtém de uma exceção, mas para mim está exibindo muitas informações, se você tiver bibliotecas e outras coisas, pode mostrar informações sensatas sobre seu cliente, seu projeto ou outras coisas para alguém que possa estar tentando ver as coisas.
Isto é o que parece no Swagger:

Criando uma exceção personalizada
Esta etapa poderia ser evitada, pois sabemos qual exceção será lançada, neste caso um Exception, mas para mim, ter suas exceções personalizadas é melhor, pois você tem mais controle do que está lançando.
Neste caso, acabei de criar um objeto CustomException que herda de Exception:
namespace CustomExceptionHandleDemo.Exceptions
{
public class CustomException : Exception
{
/// <summary>
/// Constructor for <see cref="CustomException"/>
/// </summary>
public CustomException() { }
/// <summary>
/// Constructor for <see cref="CustomException"/>
/// </summary>
/// <param cref="string" name="message">Parameter for message</param>
public CustomException(string message) : base(message) { }
/// <summary>
/// Constructor for <see cref="CustomException"/>
/// </summary>
/// <param cref="string" name="message">Parameter for message</param>
/// <param cref="Exception" name="inner">Parameter for inner</param>
public CustomException(string message, Exception inner) : base(message, inner) { }
}
}
Depois de criarmos nossa exceção personalizada, vamos atualizar nosso método para lançar CustomException em vez de Exception:
[HttpGet]
[Route("GetWithoutExceptionHandler")]
public Task GetWithoutExceptionHandler()
{
throw new CustomException("This is a custom exception!");
}
Isso por enquanto não mudará nada, mas o stack trace mostrará que o objeto que foi lançado é um CustomException em vez de Exception, dê uma olhada no início do stack trace:
CustomExceptionHandleDemo.Exceptions.CustomException: This is a custom exception!
at CustomExceptionHandleDemo.Controllers.WeatherForecastController.GetWithoutExceptionHandler()
at lambda_method24(Closure , Object , Object[] )
at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.AwaitableResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Logged|12_1(ControllerActionInvoker invoker)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
Criando um ExceptionFilterAttribute
A Microsoft nos deu uma maneira de lidar com exceções depois que elas foram lançadas. Você pode verificar mais informações aqui.
Mas até agora a documentação que eles nos fornecem é:
Um filtro abstrato que é executado de forma assíncrona após uma ação lançar uma exceção. As subclasses devem substituir OnException(ExceptionContext) ou OnExceptionAsync(ExceptionContext) mas não ambos.
Então vamos criar um, vamos criar CustomExceptionFilterAttribute no qual vamos substituir OnException:
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc;
using CustomExceptionHandleDemo.Exceptions;
namespace CustomExceptionHandleDemo.ExceptionFilterAttributes
{
public class CustomExceptionFilterAttribute : ExceptionFilterAttribute
{
/// <summary>
/// OnException
/// </summary>
/// <param cref="ExceptionContext" name="context">Parameter for context</param>
public override void OnException(ExceptionContext context)
{
if (context.Exception is CustomException)
{
context.HttpContext.Response.StatusCode = 500;
context.Result = new ObjectResult(context.Exception.Message);
}
}
}
}
Como você pode ver, estamos dando uma olhada no ExceptionContext, quando a exceção é um tipo de CustomException, fazemos algo.
Esse algo está atualizando a resposta e o código de status do que iremos retornar.
Para atualizar o código de status, devemos atualizar context.HttpContext.Response.StatusCode e para retornar o resultado, temos que atualizar o context.Result fornecendo a ele um objeto que é herdado de ActionResult.
Este é um filtro, então significa que temos que adicionar algo adicionando [CustomExceptionFilter].
Usando o filtro
Agora, vamos replicar o método que temos e adicionar este filtro para que ele entre em ação, nosso endpoint da API ficará assim:
using CustomExceptionHandleDemo.ExceptionFilterAttributes;
using CustomExceptionHandleDemo.Exceptions;
using Microsoft.AspNetCore.Mvc;
namespace CustomExceptionHandleDemo.Controllers
{
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
public WeatherForecastController()
{
}
[HttpGet]
[Route("GetWithoutExceptionHandler")]
public Task GetWithoutExceptionHandler()
{
throw new CustomException("This is a custom exception!");
}
[HttpGet]
[Route("GetWithExceptionHandler")]
[CustomExceptionFilter]
public Task GetWithExceptionHandler()
{
throw new CustomException("This is a custom exception!");
}
}
}
Como você pode ver, temos um novo método chamado GetWithExceptionHandler, que tem a mesma lógica que GetWithoutExceptionHandler tem, mas neste caso, adicionamos o filtro [CustomExceptionFilter] ao método.
O resultado é o seguinte após executarmos o método, vou exibir uma imagem, pois ela não está mais mostrando o stack trace:
Com isso criamos uma exceção personalizada, um filtro para substituir o que acontece quando lançamos uma exceção e a usamos em um método.
Isso pode ser usado para muitas coisas, como registrar e saber o que, quando e onde o erro acontece.