aspnetcore-filtros-de-accion

Action Filters in ASP.NET Core: A Practical Guide

  • 5 min

An action filter in ASP.NET Core is a component that executes code before or after a controller action.

It’s used to apply cross-cutting logic without repeating it across all methods: measuring times, validating conditions, adding logs, modifying responses, or stopping execution when something doesn’t meet the rules.

Think about those cases where you have twenty actions and you want to do the same thing before entering each method. Copy-pasting that if twenty times works… until it doesn’t, which is usually quite soon.

Where filters fit

Filters live within the MVC subsystem of ASP.NET Core. This means they execute after the routing middleware, when ASP.NET Core already knows which controller and which action it’s going to execute.

Don’t confuse them with middlewares:

  • A middleware works at the HTTP level: request, response, headers, body, etc.
  • A filter works at the MVC level: controller, action, arguments, ModelState, and result.

The simplified flow would be something like this:

Request -> Middleware -> Routing -> Filter -> Action -> Filter -> Response
Copied!

If we need to act on any request, even static files or non-existent routes, we usually want a middleware. If we need to know which action is about to be executed, then filters are a better fit.

Filter types

ASP.NET Core has several types of filters, depending on when they execute.

TypeWhen it executesTypical use
IAuthorizationFilterBefore almost everythingCustom authorization
IResourceFilterBefore and after model bindingCaching, early short-circuits
IActionFilterBefore and after the actionValidations, logs, metrics
IExceptionFilterWhen an exception occursError handling in MVC
IResultFilterBefore and after the resultModifying responses

In this post we focus on action filters, which are the most commonly used when working with controllers.

Creating an action filter

The easiest way to start is by inheriting from ActionFilterAttribute. This way, we can apply the filter as an attribute on an action or controller.

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($"Starting execution of {actionName}");

        var resultContext = await next();

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

The important piece is await next(). This call continues the pipeline and allows the controller action to execute.

If we don’t call next(), the action won’t execute. This isn’t a bug, it’s a feature. It’s used to short-circuit the request when we want to return a response before reaching the controller.

Applying the filter

We can apply a filter at three different levels.

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

Here, the filter only affects that method.

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

In this case, it applies to all actions of the controller.

We can also register a filter for all controllers in the application.

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

This is useful for metrics, common logging, or cross-cutting policies we want to apply across the entire API.

Filters with dependency injection

The previous example uses Console.WriteLine, which is fine for learning, but in a real application we would use ILogger.

Attributes in C# are not the best place to inject complex dependencies. For that, we can create a filter as a service and implement it with 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("Starting action {Action}",
            context.ActionDescriptor.DisplayName);

        await next();

        _logger.LogInformation("Action finished");
    }
}
Copied!

We register it in the container:

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

And we use it with [ServiceFilter]:

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

ASP.NET Core will resolve the filter from the DI container, complete with its dependencies.

Short-circuiting execution

A filter can also return a response without executing the action. To do this, we assign 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(
                "The id must be greater than zero");
        }
    }
}
Copied!

If the id is negative or zero, the controller doesn’t get executed. The response is returned directly to the client.

When to use them

Action filters are useful, but that doesn’t mean you should turn them into a catch-all.

We use action filters when we need common logic tied to controllers and actions: arguments, ModelState, attributes, MVC results, etc.

For purely HTTP or application-wide logic, a middleware is usually a better choice.