Skip to content

Filters & Action Pipeline

Key Points

  • Five filter types wrap MVC action execution: Authorization, Resource, Action, Result, Exception. Run in this order.
  • IAsyncActionFilter is the modern shape — single OnActionExecutionAsync(ctx, next).
  • ServiceFilter / TypeFilter apply DI-resolved filters via attributes.
  • For Minimal APIs, equivalent is endpoint filters (IEndpointFilter).
  • Global filters apply to all controllers/actions; combine with attributes for per-action overrides.
  • Order controls execution within the same scope; lower runs first.

Concepts (deep dive)

Filter pipeline diagram

   Request
   [Authorization filter] — short-circuit on 401/403
   [Resource filter] — wraps the rest; can short-circuit before model binding
   Model binding
   [Action filter] — before/after action method
   Action method runs
   [Action filter] — after
   [Result filter] — before/after result execution
   Result executes (writes response)
   [Result filter] — after
   [Resource filter] — after (cleanup)

   [Exception filter] — runs on exception anywhere above

Authorization filter

public class CustomAuth : IAsyncAuthorizationFilter
{
    public Task OnAuthorizationAsync(AuthorizationFilterContext ctx)
    {
        if (!ctx.HttpContext.User.Identity?.IsAuthenticated == true)
            ctx.Result = new UnauthorizedResult();
        return Task.CompletedTask;
    }
}

💡 Most apps don't write authorization filters — use the built-in policy system ([Authorize(Policy = "X")]).

Resource filter

Wraps everything (binding, action, result):

public class CacheFilter : IAsyncResourceFilter
{
    public async Task OnResourceExecutionAsync(ResourceExecutingContext ctx, ResourceExecutionDelegate next)
    {
        var key = ctx.HttpContext.Request.Path;
        if (Cache.TryGet(key, out var cached))
        {
            ctx.Result = new ContentResult { Content = cached };
            return;   // short-circuit
        }
        var executed = await next();
        if (executed.Result is ContentResult c) Cache.Set(key, c.Content);
    }
}

Action filter

public class TimingFilter(ILogger<TimingFilter> log) : IAsyncActionFilter
{
    public async Task OnActionExecutionAsync(ActionExecutingContext ctx, ActionExecutionDelegate next)
    {
        var sw = Stopwatch.StartNew();
        var executed = await next();
        log.LogInformation("{Action} took {Ms}ms",
            ctx.ActionDescriptor.DisplayName, sw.ElapsedMilliseconds);
    }
}

Exception filter

public class ProblemFilter : IExceptionFilter
{
    public void OnException(ExceptionContext ctx)
    {
        if (ctx.Exception is NotFoundException)
        {
            ctx.Result = new NotFoundResult();
            ctx.ExceptionHandled = true;
        }
    }
}

💡 Prefer IExceptionHandler (.NET 8+) middleware over exception filters for app-wide error handling. Exception filters are scoped to MVC actions.

Result filter

public class HeaderFilter : IAsyncResultFilter
{
    public async Task OnResultExecutionAsync(ResultExecutingContext ctx, ResultExecutionDelegate next)
    {
        ctx.HttpContext.Response.Headers.Append("X-Custom", "value");
        await next();
    }
}

Applying filters

// Globally
builder.Services.AddControllers(opts =>
{
    opts.Filters.Add<TimingFilter>();
});

// Per controller / action
[ServiceFilter(typeof(TimingFilter))]
public class OrdersController : ControllerBase { ... }

[TypeFilter(typeof(TimingFilter), Order = -1)]
public IActionResult Get() { ... }
  • ServiceFilter — filter is resolved from DI (registered with AddSingleton/AddScoped).
  • TypeFilter — instance per call; constructor args from DI.
  • Order — execute order (lower runs first; defaults to 0).

Filter scopes

  • Global — applies to all actions.
  • Controller — applied via [Filter] on the controller.
  • Action — applied via [Filter] on the action.

Execution order: Global → Controller → Action (entering); reversed (exiting).

Endpoint filters (Minimal APIs)

app.MapPost("/orders", Create)
   .AddEndpointFilter<ValidationFilter>()
   .AddEndpointFilter<TimingFilter>();

Equivalent for Minimal APIs. Class-based filters implement IEndpointFilter. See Minimal APIs.

Filter vs middleware vs IExceptionHandler

Concern Use
App-wide unhandled exceptions IExceptionHandler (recommended in .NET 8+)
Cross-cutting at HTTP level Middleware
MVC action wrapping Action / result filter
Authentication / authorization UseAuthentication / UseAuthorization middleware
Per-endpoint cross-cutting in Minimal APIs Endpoint filter

Code: correct vs wrong

❌ Wrong: heavy work in a global action filter

opts.Filters.Add<HeavyComputeFilter>();   // runs for every action

✅ Correct: scope to controllers/actions that need it

[ServiceFilter(typeof(HeavyComputeFilter))]
public IActionResult Specific() { ... }

❌ Wrong: filter that throws

public Task OnActionExecutionAsync(...)
{
    throw new Exception();   // ❌ propagates; may not be caught by filter chain
}

✅ Correct: handle in filter

public async Task OnActionExecutionAsync(ActionExecutingContext ctx, ActionExecutionDelegate next)
{
    try { await next(); }
    catch (Exception ex) { /* log; rethrow if appropriate */ throw; }
}

❌ Wrong: mixing exception filter and IExceptionHandler

// IExceptionHandler in middleware AND filter for same exception types
// Order is: filters run first; if ExceptionHandled = false, middleware runs

✅ Correct: pick one layer

App-wide: IExceptionHandler. MVC-specific: filter. Don't double-handle.


Design patterns for this topic

Pattern 1 — "Filter for MVC-action concerns; middleware for HTTP concerns"

  • Intent: layer-appropriate.

Pattern 2 — "ServiceFilter for DI-friendly filters"

  • Intent: test-friendly; reuse instances.

Pattern 3 — "IExceptionHandler for app-wide errors (.NET 8+)"

  • Intent: centralized error → ProblemDetails.

Pattern 4 — "Endpoint filter for Minimal APIs"

  • Intent: equivalent to MVC action filters.

Pros & cons / trade-offs

Filter Pros Cons
Authorization Standard Built-in policies usually suffice
Resource Wraps everything Confusing scope
Action Common cross-cutting Per-action only
Exception Catch / transform Limited to MVC
Result Modify response Runs late

When to use / when to avoid

  • Use action filters for MVC-specific cross-cutting (auditing, timing).
  • Use middleware for HTTP-level concerns.
  • Use IExceptionHandler for app-wide error handling.
  • Avoid global filters with heavy logic — slow every action.
  • Avoid duplicating between middleware and filter.

Interview Q&A

Q1. Order of MVC filter types? Authorization → Resource → Action → Result → Exception (the last runs at any point on exception).

Q2. Difference between ServiceFilter and TypeFilter? ServiceFilter uses a DI-registered filter instance. TypeFilter creates a new instance per call (with DI for constructor args).

Q3. When prefer IExceptionHandler over exception filter? For app-wide, framework-agnostic error handling. Exception filters are MVC-specific.

Q4. Difference between filter and middleware? Middleware is HTTP-pipeline-level; filter is MVC-action-level. Filters know the matched action; middleware doesn't (unless after UseRouting and inspecting endpoint metadata).

Q5. How does Order affect filters? Filters run in Order ascending (entering); reversed (exiting). Default 0.

Q6. What's an endpoint filter? Minimal APIs equivalent of action filter. Implements IEndpointFilter.

Q7. Can a filter short-circuit? Yes — set ctx.Result (in resource/action filter) before calling next (or don't call it). ctx.ExceptionHandled = true for exception filters.

Q8. Example of resource filter use case? Caching: check cache before model binding; return cached response if hit.

Q9. Why might a filter not run? Wrong scope (action attribute applied to controller doesn't propagate down by default — check [ServiceFilter] placement). Or short-circuited by an earlier filter.

Q10. How do you test a filter? Construct it directly, fake ActionExecutingContext / ActionExecutionDelegate, assert on side effects (header set, log called).


Gotchas / common mistakes

  • ⚠️ Filter without [ServiceFilter]/[TypeFilter] — may not have access to scoped DI.
  • ⚠️ Filter throwing — may bypass downstream filters.
  • ⚠️ Heavy global filter — slows every action.
  • ⚠️ Duplicating logic in filter and middleware — double execution.
  • ⚠️ OnActionExecuting writing to response — happens before action runs; can short-circuit unintentionally.

Further reading