Gestión de excepciones en ASP.NET Core: Diferenciando errores de negocio de fallos del sistema

En el desarrollo de APIs robustas con .NET Core, es fundamental separar los errores técnicos (excepciones no controladas) de los errores de lógica de negocio (validaciones o estados esperados). Una técnica eficiente es configurar el middleware de manejo de excepciones mediante un delegado anónimo, lo que permite centralizar la respuesta sin necesidad de un contorlador dedicado.

app.UseExceptionHandler(handler =>
{
    handler.Run(async context =>
    {
        var errorDetail = context.Features.Get<IExceptionHandlerPathFeature>();
        var exception = errorDetail?.Error;

        // Intentamos identificar si es una excepción de negocio conocida
        if (exception is IDomainException businessError)
        {
            context.Response.StatusCode = StatusCodes.Status200OK;
            context.Response.ContentType = "application/json; charset=utf-8";

            var result = new 
            {
                Code = businessError.ErrorCode,
                Message = businessError.Message,
                Type = "BusinessLogic"
            };

            await context.Response.WriteAsJsonAsync(result);
        }
        else
        {
            // Error inesperado: registrar log y devolver 500
            var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
            logger.LogError(exception, "Fallo crítico del sistema detectado");

            context.Response.StatusCode = StatusCodes.Status500InternalServerError;
            await context.Response.WriteAsJsonAsync(new 
            {
                Code = 999,
                Message = "Ocurrió un error interno en el servidor"
            });
        }
    });
});

Estrategia de Códigos de Estado HTTP

La decisión de devolver un HTTP 200 para errores de negocio y un HTTP 500 para fallos desconocidos tiene una base operativa. Los sistemas de monitoreo y alertas suelen rastrear la tasa de respuestas 5xx. Si cada validación de usuario (como "contraseña incorrecta") lanzara un 500, el sistema de alertas generaría falsos positivos constantes, ocultando problemas reales de infraestructura o bugs críticos. Al usar 200 para la lógica de dominio, garantizamos que el monitoreo sea sensible únicamente a la inestabilidad real del servicio.

Uso de Filtros de Excepción en MVC

Otra alternativa es el uso de IExceptionFilter. A diferencia del middlweare global, los filtros operan dentro del ciclo de vida de MVC, lo que les da acceso al contexto de la acción ejecutada. Esto es útil cuando el manejo de errores debe ser específico para los controladores de la Web API.

public class CustomExceptionFilter : IExceptionFilter
{
    public void OnException(ExceptionContext context)
    {
        if (context.Exception is IDomainException domainEx)
        {
            context.Result = new JsonResult(new 
            {
                Status = "Error",
                domainEx.ErrorCode,
                domainEx.Message
            }) 
            { 
                StatusCode = StatusCodes.Status200OK 
            };
        }
        else
        {
            // Registro de errores imprevistos
            var log = context.HttpContext.RequestServices.GetService<ILogger<CustomExceptionFilter>>();
            log?.LogError(context.Exception, "Error no gestionado en el controlador");

            context.Result = new JsonResult(new { Message = "Error de sistema" }) 
            { 
                StatusCode = StatusCodes.Status500InternalServerError 
            };
        }
        context.ExceptionHandled = true;
    }
}

Para aplicar este filtro de forma global en la aplicación, se debe registrar durante la configuración de los servicios en el archivo de inicio:

builder.Services.AddControllers(options =>
{
    options.Filters.Add<CustomExceptionFilter>();
});

Atributos para control granular

Si se requiere un control más específico, se puede heredar de ExceptionFilterAttribute. Esto permite aplicar la lógica de manejo de errores solo a ciertos controladores o métodos de acción específicos mediante atributos decoradores.

public class BusinessPolicyAttribute : ExceptionFilterAttribute
{
    public override void OnException(ExceptionContext context)
    {
        // Lógica de transformación de excepción similar al filtro anterior
        // ...
    }
}

// Uso en el controlador
[BusinessPolicy]
public class OrdersController : ControllerBase 
{
    // ...
}

Esta arquitectura permite definir contratos claros para el frotnend. Al estandarizar las interfaces de error (ya sea mediante una interfaz como IDomainException o una clase base), el equipo de desarrollo puede asegurar que el cliente siempre reciba una estructura de datos predecible, ocultando detalles técnicos sensibles como el Stack Trace en entornos de producción, mientras se mantiene una trazabilidad completa en los logs del servidor.

Etiquetas: ASP.NET-Core Middleware exception-handling web-api CSharp

Publicado el 7-20 06:38