Implementar una estrategia uniforme para el manejo de errores es fundamental en el desarrollo de APIs robustas. Al heredar de ExceptionFilterAttribute y sobrescribir el método OnException, es posible capturar cualquier fallo de ejecución de manera global, formatear la salida y devolver una respuesta coherante al cliente.
Estructura del Modelo de Respuesta
Para garantizar que el cliente siempre reciba el mismo esquema de datos, definimos un enumerado de estados y una clase de envoltura para la respuesta JSON.
public enum CodigoEstado
{
[Description("Operación exitosa")]
Exito = 200,
[Description("Error en el servidor")]
Fallo = 500
}
public class RespuestaBase
{
public CodigoEstado Codigo { get; set; }
public string Mensaje { get; set; }
public object Resultado { get; set; }
}
Implementación del Filtro de Excepciones
El siguiente atributo captura las excepciones no controladas durante la ejecución de las acciones en los controladores. Este componente intercepta el contexto de la excepción y asigna un ContentResult formateado como JSON.
public class ManejadorExcepcionesAttribute : ExceptionFilterAttribute
{
public override void OnException(ExceptionContext contexto)
{
// Se construye la respuesta estandarizada extrayendo el mensaje de la excepción
var contenido = FabricaResultados.Generar(
null,
CodigoEstado.Fallo,
contexto.Exception.Message
);
contexto.Result = contenido;
contexto.ExceptionHandled = true; // Indica que la excepción ha sido gestionada
}
}
Generador de Resultados JSON
Dado que en ciertas versiones de .NET Core el manejo de JsonResult puede variar, se recomienda utilizar un generador que encapsule la lógica de serialización mediante ContentResult.
public static class FabricaResultados
{
public static ContentResult Generar(object datos, CodigoEstado estado = CodigoEstado.Exito, string errorDetalle = null)
{
var descripcionEstado = ObtenerDescripcionEnum(estado);
var objetoRespuesta = new RespuestaBase
{
Codigo = estado,
Mensaje = string.IsNullOrEmpty(errorDetalle)
? descripcionEstado
: $"{descripcionEstado}: {errorDetalle}",
Resultado = datos
};
return new ContentResult
{
ContentType = "application/json",
Content = JsonConvert.SerializeObject(objetoRespuesta),
StatusCode = (int)estado
};
}
private static string ObtenerDescripcionEnum(Enum valor)
{
var campo = valor.GetType().GetField(valor.ToString());
var atributo = campo.GetCustomAttributes(typeof(DescriptionAttribute), false)
.FirstOrDefault() as DescriptionAttribute;
return atributo?.Description ?? valor.ToString();
}
}
Integración en el Controlador Base
Para aplicar esta lógica de forma automática en todos los endpoints, se debe decorar un controlador base del cual heredarán los demás controladores de la aplicación. Esto también permite integrar validaciones de modelos de forma centralizada.
[ApiController]
[Route("api/[controller]")]
[ManejadorExcepciones] // Filtro de errores global
public abstract class ControladorBase : ControllerBase
{
protected ContentResult RespuestaJson(object data = null)
{
return FabricaResultados.Generar(data);
}
}
Ejemplo de Uso en un Endpoint
Al heredar de ControladorBase, cualquier excepción lanzada dentro de un método de acción será interceptada por el filtro, transformándola en un error 500 con el formato definido.
public class UsuarioController : ControladorBase
{
[HttpPost("login")]
public IActionResult Autenticar(LoginRequest peticion)
{
// Simulación de un error inesperado
if (peticion == null)
{
throw new Exception("Los datos de acceso son obligatorios.");
}
// Lógica de negocio...
return RespuestaJson(new { Token = "abc-123", Expira = DateTime.Now.AddHours(1) });
}
}