Filters and the MVC Pipeline

This section documents ASP.NET Core on .NET 10 (LTS), the current release — the minimal hosting model, the middleware pipeline, dependency injection, Minimal APIs, MVC & Razor Pages, Blazor with the current render modes, SignalR and gRPC, EF Core, ASP.NET Core Identity and policy-based authorization, output caching, rate limiting, and Native-AOT-aware building — as described by the official documentation at Microsoft Learn, which is the reference these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. .NET ships a major release every November and its APIs continue to evolve: the examples here target .NET 10 / C# 14.

This section’s bibliography lists the reference material consulted while preparing these pages.

Filters let cross-cutting logic (logging, validation, caching, exception mapping) run at defined points around an action or page handler, without every action repeating that logic itself.

The filter pipeline

Filters run in a fixed sequence around the handler, and most kinds run both before and after it:

The MVC/Razor Pages filter pipeline: authorization, resource, action, and result filters wrapped by exception filters

The filter kinds

Filter Runs

IAuthorizationFilter

First, before model binding — can short-circuit the whole request (this is how [Authorize] itself is implemented). See Authorization.

IResourceFilter

Around model binding — the earliest point that can short-circuit and the latest point that can affect binding (e.g. output caching reading/writing a cache before binding even happens).

IActionFilter / IAsyncActionFilter

Immediately before and after the action method itself — can inspect or replace bound arguments, or short-circuit with a result before the action runs.

IExceptionFilter

Only when an unhandled exception propagates out of everything inside it — the filter analog of IExceptionHandler; see Error Handling, Logging, and Observability for the two compared.

IResultFilter / IAsyncResultFilter

Immediately before and after the IActionResult executes (writes the response).

IPageFilter / IAsyncPageFilter

The Razor Pages equivalent, combining authorization/resource/action-like hooks around a page handler; see Razor Pages.

public sealed class TimingFilter(ILogger<TimingFilter> logger) : IAsyncActionFilter
{
    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        var sw = Stopwatch.StartNew();
        var executed = await next();   // runs model-bound action; executed.Result is set if it ran
        logger.LogInformation("{Action} took {Ms}ms", context.ActionDescriptor.DisplayName, sw.ElapsedMilliseconds);
    }
}

Order and scope

A filter can be registered globally (o.Filters.Add<T>() in AddControllers/AddRazorPages), on a controller ([ServiceFilter(typeof(T))]), or on a single action — global filters run outermost, action-level filters innermost, for filters of the same kind. IOrderedFilter sets an explicit Order among filters that would otherwise tie, lower values running first (outer) on the way in:

public sealed class AuditFilter : IActionFilter, IOrderedFilter
{
    public int Order => -100;   // runs before default-ordered (Order == 0) action filters
    public void OnActionExecuting(ActionExecutingContext context) { }
    public void OnActionExecuted(ActionExecutedContext context) { }
}

Short-circuiting

Any filter can stop the request from reaching later stages: set context.Result in a synchronous filter, or simply not call next() in an async one. This is exactly how [Authorize] (an authorization filter) returns a 403 without ever invoking the action, and how output caching (a resource filter) can serve a cached response without ever running model binding.

DI in filters: ServiceFilter and TypeFilter

Attributes are compile-time constants, so a filter attribute cannot take constructor-injected dependencies directly. ServiceFilter resolves an already-DI-registered filter type; TypeFilter instantiates a type through DI without requiring it to be separately registered:

builder.Services.AddScoped<AuditFilter>();

[ServiceFilter(typeof(AuditFilter))]                 // AuditFilter must be registered above
public sealed class OrdersController : Controller { }

[TypeFilter(typeof(RateLimitFilter), Arguments = new object[] { 100 })]  // constructed via DI, extra ctor args allowed
public sealed class PublicApiController : ControllerBase { }

A filter implementing IFilterFactory (most attribute-based filters do this implicitly through TypeFilterAttribute) is how attributes like [ServiceFilter] bridge attribute syntax to DI-constructed instances.

Filters vs. middleware vs. endpoint filters

Mechanism Scope and capabilities

Middleware

Every request, before routing has even selected an endpoint — no access to bound action arguments, model state, or MVC-specific context.

MVC/Razor Pages filters (this page)

Only requests routed to a controller action or page handler — has full access to bound arguments, ModelState, and the eventual IActionResult.

IEndpointFilter (Minimal APIs)

The Minimal API equivalent of action filters — wraps a single endpoint’s handler with access to its bound arguments, without the controller/action machinery.

Choose middleware for concerns that apply to the whole app regardless of what handles the request (e.g. response compression); choose MVC filters or endpoint filters for concerns that need the bound arguments/action-specific context. See Comparing filters and middleware.