aspnetcore-filtros-de-accion

Filtros de acción en ASP.NET Core: guía práctica

  • 5 min

Un filtro de acción en ASP.NET Core es un componente que ejecuta código antes o después de una acción de un controlador.

Sirve para aplicar lógica transversal sin repetirla en todos los métodos: medir tiempos, validar condiciones, añadir logs, modificar respuestas o cortar la ejecución cuando algo no cumple las reglas.

Piensa en esos casos en los que tienes veinte acciones y en todas quieres hacer lo mismo antes de entrar al método. Copiar y pegar ese if veinte veces funciona… hasta que deja de funcionar, que suele ser bastante pronto.

Dónde encajan los filtros

Los filtros viven dentro del subsistema MVC de ASP.NET Core. Eso significa que se ejecutan después del middleware de routing, cuando ASP.NET Core ya sabe qué controlador y qué acción va a ejecutar.

No hay que confundirlos con los middlewares:

  • Un middleware trabaja a nivel HTTP: petición, respuesta, cabeceras, cuerpo, etc.
  • Un filtro trabaja a nivel MVC: controlador, acción, argumentos, ModelState y resultado.

El flujo simplificado sería algo así:

Petición -> Middleware -> Routing -> Filtro -> Acción -> Filtro -> Respuesta
Copied!

Si necesitamos actuar sobre cualquier petición, incluso archivos estáticos o rutas inexistentes, normalmente queremos un middleware. Si necesitamos saber qué acción se va a ejecutar, entonces los filtros encajan mejor.

Tipos de filtros

ASP.NET Core tiene varios tipos de filtros, según el momento en el que se ejecutan.

TipoCuándo se ejecutaUso típico
IAuthorizationFilterAntes de casi todoAutorización personalizada
IResourceFilterAntes y después del model bindingCaché, cortocircuitos tempranos
IActionFilterAntes y después de la acciónValidaciones, logs, métricas
IExceptionFilterCuando hay una excepciónManejo de errores en MVC
IResultFilterAntes y después del resultadoModificar respuestas

En esta entrada nos centramos en los filtros de acción, que son los que más se usan cuando trabajamos con controladores.

Crear un filtro de acción

La forma más cómoda para empezar es heredar de ActionFilterAttribute. Así podemos aplicar el filtro como un atributo sobre una acción o controlador.

using Microsoft.AspNetCore.Mvc.Filters;
using System.Diagnostics;

public class TimeLogFilterAttribute : ActionFilterAttribute
{
    public override async Task OnActionExecutionAsync(
        ActionExecutingContext context,
        ActionExecutionDelegate next)
    {
        var stopwatch = Stopwatch.StartNew();
        var actionName = context.ActionDescriptor.DisplayName;

        Console.WriteLine($"Iniciando ejecución de {actionName}");

        var resultContext = await next();

        stopwatch.Stop();
        Console.WriteLine($"Finalizado {actionName} en {stopwatch.ElapsedMilliseconds} ms");
    }
}
Copied!

La pieza importante es await next(). Esa llamada continúa el pipeline y permite que se ejecute la acción del controlador.

Si no llamamos a next(), la acción no se ejecuta. Esto no es un bug, es una característica. Sirve para cortar la petición cuando queremos devolver una respuesta antes de llegar al controlador.

Aplicar el filtro

Podemos aplicar un filtro en tres niveles distintos.

[HttpGet]
[TimeLogFilter]
public IActionResult Get()
{
    return Ok("Hola mundo");
}
Copied!

Aquí el filtro solo afecta a ese método.

[ApiController]
[Route("api/[controller]")]
[TimeLogFilter]
public class UsersController : ControllerBase
{
    // Acciones del controlador
}
Copied!

En este caso se aplica a todas las acciones del controlador.

También podemos registrar un filtro para todos los controladores de la aplicación.

builder.Services.AddControllers(options =>
{
    options.Filters.Add<TimeLogFilterAttribute>();
});
Copied!

Esto es útil para métricas, logging común o políticas transversales que queramos aplicar en toda la API.

Filtros con inyección de dependencias

El ejemplo anterior usa Console.WriteLine, que nos vale para aprender, pero en una aplicación real usaríamos ILogger.

Los atributos en C# no son el mejor sitio para inyectar dependencias complejas. Para eso podemos crear un filtro como servicio e implementarlo con IAsyncActionFilter.

using Microsoft.AspNetCore.Mvc.Filters;

public class LogActionFilter : IAsyncActionFilter
{
    private readonly ILogger<LogActionFilter> _logger;

    public LogActionFilter(ILogger<LogActionFilter> logger)
    {
        _logger = logger;
    }

    public async Task OnActionExecutionAsync(
        ActionExecutingContext context,
        ActionExecutionDelegate next)
    {
        _logger.LogInformation("Iniciando acción {Action}",
            context.ActionDescriptor.DisplayName);

        await next();

        _logger.LogInformation("Acción finalizada");
    }
}
Copied!

Lo registramos en el contenedor:

builder.Services.AddScoped<LogActionFilter>();
Copied!

Y lo usamos con [ServiceFilter]:

[HttpGet]
[ServiceFilter(typeof(LogActionFilter))]
public IActionResult Get()
{
    return Ok();
}
Copied!

ASP.NET Core resolverá el filtro desde el contenedor de DI, con sus dependencias incluidas.

Cortar la ejecución

Un filtro también puede devolver una respuesta sin ejecutar la acción. Para ello asignamos context.Result.

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

public class ValidatePositiveIdAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        if (!context.ActionArguments.TryGetValue("id", out var value))
        {
            return;
        }

        if (value is int id && id <= 0)
        {
            context.Result = new BadRequestObjectResult(
                "El id debe ser mayor que cero");
        }
    }
}
Copied!

Si el id es negativo o cero, el controlador no llega a ejecutarse. La respuesta se devuelve directamente al cliente.

Cuándo usarlos

Los filtros de acción son útiles, pero tampoco hace falta convertirlos en un cajón desastre.

Usamos filtros de acción cuando necesitamos lógica común vinculada a controladores y acciones: argumentos, ModelState, atributos, resultados MVC, etc.

Para lógica puramente HTTP o global de la aplicación, normalmente es mejor un middleware.