Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

ASP.NET Core MVC filters add reusable behavior around controller actions and other MVC stages. They are useful for authorization-related checks, request timing, validation, caching, exception translation, response headers, and short-circuiting an action before it runs.

This guide targets ASP.NET Core 5, not the separate legacy ASP.NET MVC 5 framework. ASP.NET Core 5 is unsupported as of August 18, 2026, so the examples use its Startup-based hosting model and should be treated as version-specific maintenance guidance.

What is an MVC filter?

An MVC filter is a reusable component that runs at a defined point in the ASP.NET Core MVC action pipeline. It can inspect the request, controller, action arguments, model state, exceptions, or action result; perform work before and after a stage; or stop later MVC processing by assigning a result.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Filters are not LINQ or data filters. They do not filter rows from a collection. They participate in the HTTP and MVC execution lifecycle.

Where filters run

The simplified pipeline is:

Middleware
  → Routing and action selection
  → Authorization filters
  → Resource filters
  → Model binding
  → Action filters
  → Controller action
  → Exception filters
  → Result filters
  → Action-result execution
  → Resource filters unwind
  → Middleware unwinds

Authorization filters run first and determine whether the request is allowed. Resource filters run after authorization and before model binding. Action filters surround action-method execution. Exception filters can handle eligible exceptions raised during MVC action, filter, or result execution. Result filters surround successful action-result execution. Middleware surrounds MVC more broadly and runs at a lower level.

For the official pipeline details, see Microsoft’s ASP.NET Core filters documentation.

Choose the right mechanism

Requirement Preferred mechanism
Require a role or policy [Authorize] and authorization policies
Run before model binding Resource filter
Inspect or validate action arguments Action filter
Measure an MVC action Action filter
Convert an MVC exception into an MVC result Exception filter
Add headers around a successful MVC result Result filter
Handle exceptions across the application Exception-handling middleware
Apply behavior to static files or non-MVC endpoints Middleware
Filter Minimal API handlers Endpoint filters in newer ASP.NET Core versions, not ASP.NET Core 5 MVC filters

Use filters when you need MVC-specific information such as the selected action, action arguments, model state, or IActionResult. Use middleware for broad request behavior, global exception handling, correlation IDs, or processing that must also cover non-MVC endpoints.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Create a basic action filter

For a concise attribute-based filter, derive from ActionFilterAttribute:

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

public sealed class RequestTimingFilterAttribute : ActionFilterAttribute
{
    private readonly Stopwatch _stopwatch = new Stopwatch();

    public override void OnActionExecuting(ActionExecutingContext context)
    {
        _stopwatch.Start();
    }

    public override void OnActionExecuted(ActionExecutedContext context)
    {
        _stopwatch.Stop();

        Console.WriteLine($"{context.ActionDescriptor.DisplayName} took " +
                          $"{_stopwatch.ElapsedMilliseconds} ms.");
    }
}

Apply it to one action:

[RequestTimingFilter]
public IActionResult Details(int id)
{
    return View(id);
}

Or apply it to a controller:

[RequestTimingFilter]
public class ProductsController : Controller
{
}

For production code, prefer an injected logger or metrics service over Console.WriteLine. Also note that ASP.NET Core’s ActionFilterAttribute implements both action-filter and result-filter interfaces. Do not assume that every subclass affects only the action stage.

Use asynchronous filters for I/O

Database, network, and other asynchronous operations should use IAsyncActionFilter rather than blocking with .Result or .Wait():

using Microsoft.AspNetCore.Mvc.Filters;

public sealed class AuditFilter : IAsyncActionFilter
{
    private readonly IAuditWriter _auditWriter;

    public AuditFilter(IAuditWriter auditWriter)
    {
        _auditWriter = auditWriter;
    }

    public async Task OnActionExecutionAsync(
        ActionExecutingContext context,
        ActionExecutionDelegate next)
    {
        await _auditWriter.WriteAsync(
            $"Starting {context.ActionDescriptor.DisplayName}");

        ActionExecutedContext executedContext = await next();

        await _auditWriter.WriteAsync(
            $"Finished {context.ActionDescriptor.DisplayName}");

        // Inspect executedContext.Exception or executedContext.Result here.
    }
}

Calling next() allows subsequent filters and the action to execute. If the filter does not call it, the action is short-circuited.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Short-circuit an action

Assign context.Result and return when a request should not proceed:

public sealed class RequireHeaderFilter : ActionFilterAttribute
{
    public override void OnActionExecuting(
        ActionExecutingContext context)
    {
        if (!context.HttpContext.Request.Headers.ContainsKey("X-Tenant"))
        {
            context.Result = new BadRequestObjectResult(
                new { error = "X-Tenant header is required." });
        }
    }
}

The asynchronous equivalent is:

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

    await next();
}

A short-circuiting authorization or resource filter prevents later MVC stages from running. Ordinary result filters do not necessarily run in that situation. A result filter can cancel action-result execution, but it should provide an appropriate response. Headers and status codes generally cannot be changed after the response has started.

Register filters in ASP.NET Core 5

Global registration with Startup

ASP.NET Core 5 uses Startup, not the later WebApplication.CreateBuilder hosting model:

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<AuditFilter>();

    services.AddControllersWithViews(options =>
    {
        options.Filters.Add<AuditFilter>();
    });
}

This adds the filter to the entire MVC application. An instance can also be added:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
services.AddControllersWithViews(options =>
{
    options.Filters.Add(new RequestTimingFilterAttribute());
});

Be careful with instance registration. A filter instance supplied directly to Add can be reused like a singleton. Mutable fields, such as a request stopwatch, can then create thread-safety and cross-request bugs. Prefer type-based registration and dependency injection for filters with state or services.

Action and controller registration

A filter without dependencies can be applied directly as an attribute:

[RequestTimingFilter]
public IActionResult Details(int id) => View(id);

For a filter resolved from dependency injection, register it first and use ServiceFilter:

services.AddScoped<AuditFilter>();

[ServiceFilter(typeof(AuditFilter))]
public IActionResult Create() => View();

ServiceFilterAttribute requires the filter type to be registered. TypeFilterAttribute is often more convenient when the filter itself is not registered as a service:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
[TypeFilter(typeof(AuditFilter))]
public IActionResult Create() => View();

Both attributes support constructor dependencies, but explicit registration is usually clearer for application-wide filters.

Dependency injection and lifetimes

Inject dependencies through the constructor rather than resolving services manually:

public sealed class TenantFilter : IAsyncActionFilter
{
    private readonly ITenantResolver _tenantResolver;

    public TenantFilter(ITenantResolver tenantResolver)
    {
        _tenantResolver = tenantResolver;
    }

    public async Task OnActionExecutionAsync(
        ActionExecutingContext context,
        ActionExecutionDelegate next)
    {
        var tenant = await _tenantResolver.ResolveAsync(context.HttpContext);

        if (tenant == null)
        {
            context.Result = new NotFoundResult();
            return;
        }

        await next();
    }
}
services.AddScoped<ITenantResolver, TenantResolver>();
services.AddScoped<TenantFilter>();

A filter that depends on request-scoped services must not be treated as a singleton. Avoid reusable filters with request-specific mutable fields, static request state, or manually constructed dependencies.

Filter scope and execution order

By default, filters nest in this order:

  1. Global filters
  2. Controller filters
  3. Action filters

Before methods run from the outside inward, while after methods unwind in reverse order:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Global before
  Controller before
    Action before
      Action method
    Action after
  Controller after
Global after

Implement IOrderedFilter, or set the Order property on an attribute, when explicit ordering is necessary:

public sealed class OrderedAuditFilter : ActionFilterAttribute
{
    public OrderedAuditFilter()
    {
        Order = 10;
    }
}

Lower order values run first on the way in and last on the way out. Use extreme values sparingly because ordering across multiple libraries quickly becomes difficult to maintain.

Authorization filters

Authentication establishes who the caller is. Authorization decides whether that identity may perform an operation.

For normal role- and policy-based authorization, use the built-in authorization system:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
[Authorize(Policy = "CanEditProducts")]
public IActionResult Edit(int id)
{
    return View(id);
}

Do not create a custom authorization filter merely to duplicate a policy check. Use authorization policies and custom policy handlers instead. Authorization filters have a before stage but no corresponding after stage, and exceptions thrown there are not handled by exception filters.

Resource filters

Resource filters run after authorization and before model binding. They are useful when processing should stop before expensive MVC work begins, such as a cache lookup, a request-level precondition, or a large-upload scenario where form-value model binding must be disabled.

They are not the default choice for ordinary action validation. Use an action filter when model binding and action arguments should already be available.

Exception filters

An exception filter can translate a known MVC exception into an action result:

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
public sealed class DomainExceptionFilter : IExceptionFilter
{
    public void OnException(ExceptionContext context)
    {
        if (context.Exception is ProductNotFoundException)
        {
            context.Result = new NotFoundObjectResult(
                new { error = context.Exception.Message });

            context.ExceptionHandled = true;
        }
    }
}

Exception filters cover exceptions raised during MVC action, filter, or result execution. They do not replace exception-handling middleware and should not be expected to handle failures from earlier middleware, routing, or other pipeline stages. Use an exception filter when the response genuinely depends on the selected MVC controller or action; otherwise prefer UseExceptionHandler or other exception-handling middleware.

Result filters

Result filters surround execution of an IActionResult. They are suitable for behavior tied to MVC result execution, such as adding a response header:

public sealed class CorrelationHeaderFilter : IResultFilter
{
    public void OnResultExecuting(ResultExecutingContext context)
    {
        context.HttpContext.Response.Headers["X-Correlation-Id"] =
            context.HttpContext.TraceIdentifier;
    }

    public void OnResultExecuted(ResultExecutedContext context)
    {
    }
}

Set headers in OnResultExecuting, before the response starts. Code in OnResultExecuted may run too late to change headers or the body. Ordinary result filters do not always run after authorization or resource short-circuiting, or after every exception path. If behavior must run for every MVC result, investigate IAlwaysRunResultFilter or IAsyncAlwaysRunResultFilter.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Middleware versus filters

Use middleware for static files, WebSockets, non-MVC endpoints, requests before action selection, broad request logging, correlation IDs, and global exception handling. Middleware does not directly understand action arguments, model binding, controller metadata, or MVC results.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use a filter for MVC-specific lifecycle behavior. A controller base class may be better when behavior is tightly coupled to shared controller methods. A service decorator is usually better for application concerns such as repository retries, domain caching, transaction boundaries, or auditing an operation independently of HTTP.

Best Value
Sale
Programming ASP.NET Core (Developer Reference)
  • Applying all key ASP.NET Core components, including MVC for HTML generation, .NET Core, EF Core, ASP.NET Identity, dependency injection, and more
  • Integrating ASP.NET Core with leading client-side frameworks, including Bootstrap
  • ASP.NET Core code for implementing business logic and data transformations
  • Handling configuration, routing, controllers, views, and common tasks (including posting forms and presenting data)
  • Performing complementary tasks: error handling, logging, application design, authentication, localization, and more

Common problems and fixes

The filter never runs

  • Confirm the filter is registered with AddControllersWithViews or AddControllers.
  • Confirm the request is handled by MVC and reaches the expected controller action.
  • Check attribute placement and namespace imports.
  • Check whether middleware or an earlier filter short-circuits the request.
  • Do not attach MVC action filters to Razor Pages handler methods; Razor Pages use page-filter interfaces.

Dependency injection fails

  • Register the filter and every constructor dependency.
  • Do not use ServiceFilter without registering the filter type.
  • Do not manually instantiate a filter that has dependencies.
  • Check for a singleton filter depending on a scoped service.

The action does not execute

Look for an assigned context.Result, authorization failure, resource-filter cache hit, failed header or model-state validation, or an exception in an earlier filter.

A response header cannot be changed

Headers must be modified before the response starts. Move the code from OnResultExecuted to OnResultExecuting, or use middleware at the appropriate stage.

A custom model-validation filter is redundant

API controllers marked with [ApiController] automatically produce a 400 response for invalid model state. Add a custom filter only when the application needs behavior beyond that default.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Testing a filter

Test filters independently with mocked dependencies and MVC context objects. At minimum, verify that:

  • the injected dependency is called;
  • the action proceeds when validation succeeds;
  • the action delegate is not called after short-circuiting;
  • the expected result and status code are assigned;
  • only intended exceptions are translated; and
  • multiple filters execute in the documented order.

A useful short-circuit test sets up a request without X-Tenant, invokes the filter, and asserts that the result is BadRequestObjectResult while the action delegate was never invoked.

ASP.NET Core 5 versus current ASP.NET Core

Do not copy current minimal-hosting examples into an ASP.NET Core 5 application. ASP.NET Core 5 uses Startup.ConfigureServices and Startup.Configure; later applications may use WebApplication.CreateBuilder. MVC filters remain distinct from endpoint filters used by newer Minimal API applications.

For upgraded applications, consult the documentation for the exact target framework and verify hosting, filter, authorization, and endpoint APIs before migrating examples.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Quick Recap

Bestseller No. 2
SaleBestseller No. 5
Programming ASP.NET Core (Developer Reference)
Programming ASP.NET Core (Developer Reference)
Integrating ASP.NET Core with leading client-side frameworks, including Bootstrap; ASP.NET Core code for implementing business logic and data transformations
$24.99

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.