Filters

Intro

Filters in ASP.NET Core let you run logic before and after specific stages of controller action execution.
They are useful for cross-cutting concerns that are tightly coupled to MVC actions, such as action-level validation, response shaping, and controller-scoped auditing — for example, an action filter that validates an X-Correlation-Id header on every inbound request and returns a 400 if missing, saving you from duplicating that check across 80+ controller actions.
This matters because putting all of that logic inside actions quickly creates duplication and inconsistent behavior.
Reach for filters when middleware is too broad and endpoint code is too local.

Filters run inside the MVC pipeline after routing selects an action.

Execution order is determined by scope (global, controller, action) and optionally by IOrderedFilter.

Example

Use an async action filter to require a custom header for selected endpoints:

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

public sealed class RequireCorrelationIdFilter : IAsyncActionFilter
{
    public async Task OnActionExecutionAsync(
        ActionExecutingContext context,
        ActionExecutionDelegate next)
    {
        if (!context.HttpContext.Request.Headers.ContainsKey("X-Correlation-Id"))
        {
            context.Result = new BadRequestObjectResult(new
            {
                error = "X-Correlation-Id header is required"
            });
            return;
        }

        await next();
    }
}

// Program.cs
builder.Services.AddScoped<RequireCorrelationIdFilter>();

builder.Services.AddControllers(options =>
{
    // Global registration
    options.Filters.AddService<RequireCorrelationIdFilter>();
});

If this rule should apply only to one endpoint, apply it with [ServiceFilter(typeof(RequireCorrelationIdFilter))] instead of registering globally.

Exception filter to catch and shape unhandled action exceptions:

public sealed class ApiExceptionFilter(ILogger<ApiExceptionFilter> logger) : IAsyncExceptionFilter
{
    public Task OnExceptionAsync(ExceptionContext context)
    {
        logger.LogError(context.Exception, "Unhandled exception in {Action}",
            context.ActionDescriptor.DisplayName);

        context.Result = context.Exception is NotFoundException
            ? new NotFoundObjectResult(new { error = context.Exception.Message })
            : new ObjectResult(new { error = "An unexpected error occurred." }) { StatusCode = 500 };

        context.ExceptionHandled = true;
        return Task.CompletedTask;
    }
}

Register globally: builder.Services.AddControllers(opts => opts.Filters.Add<ApiExceptionFilter>());

Pitfalls

Tradeoffs

Option Best for Weakness
Middleware App-wide cross-cutting concerns (logging, auth, exception handling) No direct MVC action context
MVC filters Concerns tied to controllers/actions and model/action context Only applies to MVC pipeline
Endpoint filters Minimal API endpoint-scoped behavior Not used by MVC controllers

Questions


Whats next