Overview
Filters in ASP.NET Core are components that run before or after specific stages of the MVC or API action execution pipeline. They are commonly used for cross-cutting behavior such as authorization checks, request validation, response customization, caching decisions, exception handling, and result transformation.
Middleware and filters are related, but they operate at different levels. Middleware belongs to the broader ASP.NET Core request pipeline and works with low-level HTTP concepts through HttpContext. Filters belong to the MVC/action invocation pipeline and work with higher-level concepts such as selected actions, model binding, action arguments, ModelState, action results, and controller metadata.
This topic matters because many production APIs need behavior that is applied consistently across many endpoints. Examples include authentication, authorization, logging, exception handling, validation, caching, response headers, audit behavior, and standardized error responses. Choosing the wrong extension point can cause duplicated logic, incorrect execution order, missed exceptions, broken authorization, or inconsistent API behavior.
This is important for interviews because it tests whether a developer understands how ASP.NET Core processes requests beyond simply writing controller actions. A strong candidate should know when to use middleware, when to use filters, how different filter types execute, how filters can short-circuit the pipeline, and why authorization policies and exception middleware are often better choices than custom filters for certain scenarios.
Core Concepts
Middleware vs Filters
Middleware is part of the ASP.NET Core request pipeline. It is configured in Program.cs with methods such as Use, Run, Map, UseRouting, UseAuthentication, UseAuthorization, and MapControllers.
Middleware works at the HTTP pipeline level:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
var app = builder.Build();
app.UseExceptionHandler("/error");
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Filters work inside the MVC/action pipeline after routing has selected an endpoint and ASP.NET Core is preparing to execute a controller action.
A simplified flow looks like this:
HTTP request
-> middleware pipeline
-> routing
-> authentication middleware
-> authorization middleware
-> endpoint execution
-> MVC filter pipeline
-> authorization filters
-> resource filters
-> model binding
-> action filters
-> action method
-> exception filters
-> result filters
-> result execution
-> HTTP response
The key difference is scope:
The Filter Pipeline
Filters run in a defined order. Each filter type exists for a specific stage of request processing.
The common MVC filter types are:
- Authorization filters
- Resource filters
- Action filters
- Exception filters
- Result filters
The order matters because each filter type sees the request at a different stage.
Authorization filters
-> Resource filters
-> Model binding
-> Action filters
-> Action method
-> Exception filters
-> Result filters
-> Result execution
-> Resource filters after-code
This order explains why each filter is useful for different problems. For example, a resource filter can run before model binding, but an action filter runs after model binding. An exception filter can handle exceptions thrown by an action, but it does not catch exceptions thrown by earlier middleware.
Authorization Filters
Authorization filters run first in the filter pipeline. Their job is to determine whether the current user is allowed to access the selected action.
Most applications should use built-in authorization attributes and policies instead of writing custom authorization filters:
[Authorize(Policy = "CanManageOrders")]
[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
[HttpDelete("{id:int}")]
public IActionResult DeleteOrder(int id)
{
return NoContent();
}
}
Authorization filters can short-circuit the pipeline if the request is not authorized. In practice, this usually results in a challenge, forbid response, or an authorization failure.
Important habits:
- Prefer authorization policies and requirements for business authorization rules.
- Avoid throwing exceptions from authorization filters.
- Do not use action filters for authorization.
- Keep authentication in middleware and authorization policy logic in the authorization system.
- Use resource-based authorization when the decision depends on a specific resource loaded from the database.
Example resource-based authorization pattern:
[Authorize]
[ApiController]
[Route("api/documents")]
public class DocumentsController : ControllerBase
{
private readonly IAuthorizationService _authorizationService;
private readonly IDocumentRepository _documents;
public DocumentsController(
IAuthorizationService authorizationService,
IDocumentRepository documents)
{
_authorizationService = authorizationService;
_documents = documents;
}
[HttpGet("{id:int}")]
public async Task<IActionResult> GetDocument(int id)
{
var document = await _documents.GetByIdAsync(id);
if (document is null)
{
return NotFound();
}
var result = await _authorizationService.AuthorizeAsync(
User,
document,
"CanReadDocument");
if (!result.Succeeded)
{
return Forbid();
}
return Ok(document);
}
}
Resource Filters
Resource filters run after authorization filters and before model binding. They wrap most of the MVC pipeline.
They are useful when behavior must happen before model binding or when the request can be short-circuited early.
Common use cases:
- Caching a full action response before model binding and action execution.
- Disabling form value model binding for large file uploads.
- Setting up data needed by model binding or action execution.
- Short-circuiting expensive requests before the action runs.
Example resource filter that short-circuits a request:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
public sealed class MaintenanceModeFilter : IResourceFilter
{
private readonly IConfiguration _configuration;
public MaintenanceModeFilter(IConfiguration configuration)
{
_configuration = configuration;
}
public void OnResourceExecuting(ResourceExecutingContext context)
{
var isEnabled = _configuration.GetValue<bool>("MaintenanceMode");
if (isEnabled)
{
context.Result = new ObjectResult(new
{
message = "The API is temporarily unavailable."
})
{
StatusCode = StatusCodes.Status503ServiceUnavailable
};
}
}
public void OnResourceExecuted(ResourceExecutedContext context)
{
}
}
Register it globally:
builder.Services.AddScoped<MaintenanceModeFilter>();
builder.Services.AddControllers(options =>
{
options.Filters.Add<MaintenanceModeFilter>();
});
Because resource filters run before model binding, they should not depend on action parameters already being bound.
Action Filters
Action filters run immediately before and after a controller action method executes. By this stage, routing has selected the action and model binding has usually populated action parameters.
Action filters can:
- Inspect action arguments.
- Modify action arguments.
- Validate request state.
- Short-circuit before the action executes.
- Inspect or replace the action result after the action executes.
- Add action-level audit or business logging.
Example action filter that validates ModelState manually:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
public sealed class ValidateModelFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(context.ModelState);
}
}
public void OnActionExecuted(ActionExecutedContext context)
{
}
}
Usage:
[ServiceFilter<ValidateModelFilter>]
[HttpPost]
public IActionResult CreateOrder(CreateOrderRequest request)
{
return CreatedAtAction(nameof(GetOrder), new { id = 123 }, request);
}
However, for modern ASP.NET Core APIs using [ApiController], automatic model validation already returns a 400 response for invalid model state. Therefore, a custom validation action filter is not always needed.
Action filters are often a good fit for behavior that is closely tied to controller actions, such as:
- Auditing action arguments.
- Enforcing action-specific conventions.
- Normalizing action results.
- Adding metadata based on controller/action attributes.
They are not a good fit for low-level HTTP concerns such as CORS, HTTPS redirection, authentication, static files, or global exception handling across the entire app.
Exception Filters
Exception filters handle unhandled exceptions that occur during action execution or result execution, before the response body has been written.
Example exception filter:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
public sealed class ApiExceptionFilter : IExceptionFilter
{
private readonly ILogger<ApiExceptionFilter> _logger;
public ApiExceptionFilter(ILogger<ApiExceptionFilter> logger)
{
_logger = logger;
}
public void OnException(ExceptionContext context)
{
_logger.LogError(context.Exception, "Unhandled exception in MVC action.");
context.Result = new ObjectResult(new
{
title = "Unexpected error",
detail = "An unexpected error occurred while processing the request."
})
{
StatusCode = StatusCodes.Status500InternalServerError
};
context.ExceptionHandled = true;
}
}
Exception filters are useful for MVC-specific exception policies, but they have important limitations:
- They do not catch exceptions thrown by middleware.
- They do not catch exceptions thrown during routing.
- They do not catch exceptions thrown during model binding.
- They may not be useful once the response has already started.
- They only apply to MVC/action execution.
For most production APIs, centralized exception handling middleware is usually preferred for global error handling:
app.UseExceptionHandler("/error");
app.Map("/error", () =>
{
return Results.Problem(
title: "Unexpected error",
statusCode: StatusCodes.Status500InternalServerError);
});
A practical rule is:
- Use middleware for app-wide exception handling.
- Use exception filters only for MVC-specific exception transformation or controller/action-specific exception policies.
Result Filters
Result filters run immediately before and after action results execute. They are useful for behavior that surrounds result execution, such as formatting, response headers, or result transformation.
Example result filter that adds a response header:
using Microsoft.AspNetCore.Mvc.Filters;
public sealed class AddResponseHeaderFilter : IResultFilter
{
public void OnResultExecuting(ResultExecutingContext context)
{
context.HttpContext.Response.Headers["X-Api-Version"] = "1.0";
}
public void OnResultExecuted(ResultExecutedContext context)
{
}
}
Result filters are useful when you need to affect the final result execution stage. For APIs, this often means:
- Adding headers.
- Applying response metadata.
- Wrapping successful responses in a consistent shape.
- Adding timing or diagnostic information after the action result is known.
Result filters usually run only when the action method executes successfully. They are not a replacement for exception handling.
Synchronous vs Asynchronous Filters
Most filter types have both synchronous and asynchronous interfaces.
For example, action filters can be written using either IActionFilter or IAsyncActionFilter.
Synchronous example:
public sealed class SampleActionFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
// Before action.
}
public void OnActionExecuted(ActionExecutedContext context)
{
// After action.
}
}
Asynchronous example:
public sealed class SampleAsyncActionFilter : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(
ActionExecutingContext context,
ActionExecutionDelegate next)
{
// Before action.
var executedContext = await next();
// After action.
}
}
Use asynchronous filters when the filter needs to perform asynchronous work, such as calling a database, cache, remote service, or authorization dependency.
Important habit:
- Do not implement both sync and async versions of the same filter type in the same class.
- Prefer async filters when any I/O is involved.
- Avoid blocking calls such as
.Resultor.Wait()inside filters.
Filter Scope and Execution Order
Filters can be applied at different scopes:
- Globally to all controllers and actions.
- At the controller level.
- At the action level.
Example:
builder.Services.AddControllers(options =>
{
options.Filters.Add<GlobalAuditFilter>();
});
[ServiceFilter<ControllerAuditFilter>]
[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
[ServiceFilter<ActionAuditFilter>]
[HttpPost]
public IActionResult CreateOrder(CreateOrderRequest request)
{
return Ok();
}
}
By default, filters of the same stage nest by scope:
Global before
Controller before
Action before
Action method
Action after
Controller after
Global after
A filter with a lower Order value runs earlier on the way in and later on the way out.
public sealed class OrderedAuditFilter : IActionFilter, IOrderedFilter
{
public int Order => -100;
public void OnActionExecuting(ActionExecutingContext context)
{
}
public void OnActionExecuted(ActionExecutedContext context)
{
}
}
Use explicit ordering carefully. Too much ordering logic can make request behavior hard to understand.
Dependency Injection in Filters
Filters often need dependencies such as loggers, repositories, caches, or configuration.
A common mistake is trying to inject services directly into an attribute constructor:
// Avoid this pattern for services.
// Attribute constructor arguments must be known where the attribute is applied.
public sealed class BadFilterAttribute : Attribute, IActionFilter
{
public BadFilterAttribute(IMyService service)
{
}
public void OnActionExecuting(ActionExecutingContext context) { }
public void OnActionExecuted(ActionExecutedContext context) { }
}
Better options include:
- Registering the filter type globally.
- Using
[ServiceFilter<TFilter>]. - Using
[TypeFilter]. - Implementing
IFilterFactory.
Example with ServiceFilter:
builder.Services.AddScoped<AuditActionFilter>();
[ServiceFilter<AuditActionFilter>]
[HttpPost]
public IActionResult CreateOrder(CreateOrderRequest request)
{
return Ok();
}
Example filter:
public sealed class AuditActionFilter : IAsyncActionFilter
{
private readonly ILogger<AuditActionFilter> _logger;
public AuditActionFilter(ILogger<AuditActionFilter> logger)
{
_logger = logger;
}
public async Task OnActionExecutionAsync(
ActionExecutingContext context,
ActionExecutionDelegate next)
{
_logger.LogInformation(
"Executing action {ActionName}",
context.ActionDescriptor.DisplayName);
var executedContext = await next();
_logger.LogInformation(
"Executed action {ActionName} with result {ResultType}",
context.ActionDescriptor.DisplayName,
executedContext.Result?.GetType().Name);
}
}
When adding filter instances directly to MVC options, be careful because the same instance can be reused across requests. Avoid mutable state inside filters unless the lifetime and thread-safety behavior are clearly understood.
Short-Circuiting
Both middleware and filters can short-circuit execution, but they do it differently.
Middleware short-circuits by not calling next:
app.Use(async (context, next) =>
{
if (!context.Request.Headers.ContainsKey("X-Correlation-Id"))
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
await context.Response.WriteAsync("Missing correlation id.");
return;
}
await next();
});
Filters short-circuit by setting context.Result:
public sealed class RequireHeaderFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
if (!context.HttpContext.Request.Headers.ContainsKey("X-Correlation-Id"))
{
context.Result = new BadRequestObjectResult(new
{
error = "Missing X-Correlation-Id header."
});
}
}
public void OnActionExecuted(ActionExecutedContext context)
{
}
}
Use short-circuiting carefully. It is powerful, but it can make behavior harder to trace if many filters and middleware components can stop the request.
Choosing the Right Tool
A practical decision guide:
Middleware Filter Boundary
ASP.NET Core also supports running middleware inside the filter pipeline with middleware filters. This is advanced and should not be the default choice.
The reason this exists is that some middleware-like behavior may need MVC route values or action context. However, in most applications, normal middleware or normal filters are easier to understand and maintain.
Use middleware filters only when:
- The behavior is naturally middleware-shaped.
- It must run at the resource filter stage.
- It needs MVC/action context that ordinary middleware does not have.
- The team understands the lifecycle and ordering implications.
Common Mistakes
Common mistakes include:
- Using an action filter for authentication or authorization instead of the built-in authentication and authorization systems.
- Using an exception filter as the only global error handler and expecting it to catch middleware or routing exceptions.
- Writing a custom model validation filter when
[ApiController]already provides automatic validation behavior. - Blocking async work inside filters with
.Resultor.Wait(). - Putting mutable request-specific state in a filter instance that may be reused.
- Assuming result filters run for failed actions or exceptions.
- Applying too many filters with custom
Ordervalues, making execution order hard to reason about. - Using filters for concerns that belong in middleware, such as CORS, compression, static files, HTTPS redirection, or global request logging.
Best Practices
Good production habits include:
- Use middleware for app-wide HTTP concerns.
- Use filters for MVC/controller/action-level cross-cutting concerns.
- Prefer authorization policies over custom authorization filters.
- Prefer exception middleware for global exception handling.
- Keep filters small and focused.
- Prefer asynchronous filters for I/O.
- Avoid mutable state in filters.
- Register filters through DI when they need services.
- Use global filters for truly global MVC behavior.
- Use controller/action filters only when the behavior is intentionally scoped.
- Document any non-obvious filter order.
- Test filters separately when they contain business rules or important behavior.