Manejo de excepciones personalizado en .NET API
Las excepciones son malas, lo sabemos, ¿verdad? ¿Pero qué pasa si tenemos que manejarlos?
¿Qué sucede cuando tenemos una excepción? Por ejemplo, en una API, muestra un mensaje de pila que incluye mucha información que quizás queramos eliminar de la respuesta que reciben nuestros usuarios.
Para la demostración, creé una API dotnet y agregué un método que generará una excepción:
[HttpGet]
[Route("GetWithoutExceptionHandler")]
public Task GetWithoutExceptionHandler()
{
throw new Exception("This is a custom exception!");
}
Si lanzamos una excepción, se ve así:
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)
Esto parece normal, esto es lo que se obtiene de una excepción, pero para mí, muestra mucha información, si tiene bibliotecas y demás, podría mostrar información sensible sobre su cliente, su proyecto u otras cosas a alguien que podría estar intentando ver cosas.
Esto es lo que se ve en Swagger:

Creando una excepción personalizada
Este paso podría evitarse, ya que sabemos qué excepción se generará, en este caso una Exception, pero para mí, tener excepciones personalizadas es mejor ya que tienes más control de lo que estás lanzando.
En este caso, acabo de crear un objeto CustomException que hereda 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) { }
}
}
Después de haber creado nuestra excepción personalizada, actualicemos nuestro método para lanzar CustomException en lugar de Exception:
[HttpGet]
[Route("GetWithoutExceptionHandler")]
public Task GetWithoutExceptionHandler()
{
throw new CustomException("This is a custom exception!");
}
Esto por ahora no cambiará nada, pero el seguimiento de la pila mostrará que el objeto que se lanzó es un CustomException en lugar de Exception, eche un vistazo al inicio del seguimiento de la pila:
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)
Creando un atributo de filtro de excepción
Microsoft nos ha brindado una forma de manejar las excepciones después de que se hayan generado; puede consultar más información aquí.
Pero hasta el momento la documentación que nos dan es:
Un filtro abstracto que se ejecuta de forma asincrónica después de que una acción haya generado una excepción. Las subclases deben anular OnException(ExceptionContext) o OnExceptionAsync(ExceptionContext), pero no ambos.
Entonces creemos uno, vamos a crear CustomExceptionFilterAttribute en el cual vamos a anular 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 puedes ver, estamos echando un vistazo al ExceptionContext, cuando la excepción es un tipo de CustomException, hacemos algo.
Este algo es actualizar la respuesta y el código de estado de lo que vamos a devolver.
Para actualizar el código de estado, debemos actualizar context.HttpContext.Response.StatusCode y para devolver el resultado, tenemos que actualizar el context.Result dándole un objeto que se hereda de ActionResult.
Este es un filtro, por lo que significa que tenemos que agregarle algo agregando [CustomExceptionFilter].
Usando el filtro
Ahora, repliquemos el método que tenemos y agreguemos este filtro para que entre en acción, nuestro punto final API terminará así:
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 puedes ver, tenemos un nuevo método llamado GetWithExceptionHandler, que tiene la misma lógica que tiene GetWithoutExceptionHandler, pero en este caso, le hemos agregado el filtro [CustomExceptionFilter] al método.
El resultado es el siguiente, después de ejecutar el método, mostraré una imagen, porque ya no muestra el seguimiento de la pila:
Entonces, con esto hemos creado una excepción personalizada, un filtro para anular lo que sucede cuando lanzamos una excepción y la usamos en un método.
Esto se puede utilizar para muchas cosas, como registrar y saber qué, cuándo y dónde ocurre el error.