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.
- Authorization filters run first and can short-circuit unauthorized requests.
- Resource filters run around most of the rest of the pipeline and can short-circuit early.
- Action filters run before and after the action method.
- Exception filters observe unhandled exceptions from action execution.
- Result filters run before and after the action result is executed.
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
- Running blocking I/O inside sync filters can hurt throughput because request threads are blocked; use async filters for I/O work. A sync
IActionFilterthat calls a remote validation API with.Resultinstead of usingIAsyncActionFilterwithawaitblocked thread-pool threads under load — at 200 concurrent requests, thread starvation caused p99 latency to spike from 50ms to 12 seconds and triggered 503 responses. - Putting authentication or authorization checks into custom action filters often duplicates policy logic and causes drift; prefer built-in
AddAuthentication,AddAuthorization, and[Authorize]policies. - Expecting exception filters to handle everything is risky; they only catch exceptions thrown during action execution (action method, action filters, and result execution). Exceptions in middleware, model binding before action selection, or authorization filters bypass exception filters entirely — a
JsonExceptionduring[FromBody]deserialization returned a raw 500 instead of the structured error the team expected because the exception filter never fired.
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
Expected answer:
- Middleware when concern is app-wide and independent of controller/action internals.
- Action filter when concern needs
ActionExecutingContext, action arguments, or action result wrapping. - Middleware runs earlier in pipeline and can affect all endpoints.
- Filters are more granular for controller/action scope.
Why this matters: this is a common architecture tradeoff in API design interviews.
Expected answer:
- Authorization -> Resource -> Action -> Exception -> Result (with before/after phases where applicable).
- Scope affects order: global, then controller, then action.
IOrderedFiltercan override default order.
Why this matters: ordering mistakes cause hidden bugs in validation, caching, and error handling.
Expected answer:
- Register filter/service in DI container.
- Use
ServiceFilter/TypeFilteroroptions.Filters.AddService<TFilter>(). - Prefer constructor injection and async interfaces.
- Avoid
RequestServices.GetServiceinside filter bodies unless absolutely necessary.
Why this matters: DI misuse in filters causes brittle code and testing pain.
Links
- Filters in ASP.NET Core — official reference covering all filter types, execution order, DI registration, and cancellation.
- Middleware in ASP.NET Core — use alongside this page to understand when middleware is the better choice.
- Minimal API endpoint filters — endpoint-scoped filter equivalent for Minimal APIs.
- Filter pipeline in ASP.NET Core (ABP blog) — practitioner walkthrough of filter ordering and real-world usage patterns.