Filters are one of the most powerful features in ASP.NET Core MVC. They allow you to run code before or after specific stages in the request processing pipeline — but only for MVC/API actions (unlike middleware which runs for every request). Filters are a top interview topic and essential for cross-cutting concerns like logging, validation, caching, and exception handling.
📌 What Are Filters?
Filters are attributes or classes that execute logic at specific points during the execution of an MVC action. They run inside the MVC pipeline after routing has selected the action and before the action executes.
Key difference from Middleware:
- Middleware runs for every request (static files, images, etc.)
- Filters run only for actions (controllers/endpoints)
🔁 Filter Pipeline Execution Order (Critical)
Filters execute in a specific order. Understanding this is crucial for interviews:
- Authorization Filters – Run first. Determine if user is authorized.
- Resource Filters – Run after authorization. Can handle caching, model binding, etc.
- Action Filters – Run before and after action method execution.
- Exception Filters – Run when an unhandled exception occurs.
- Result Filters – Run before and after action result execution.
⚠️ Interview Tip: Remember the acronym A-R-A-E-R (Authorization, Resource, Action, Exception, Result).
📋 The 5 Filter Types – Detailed
1. 🔐 Authorization Filters
Interface: IAuthorizationFilter or IAsyncAuthorizationFilter
When: First to run, before model binding and action execution.
Use cases: Custom authorization logic, role checks, token validation.
public class CustomAuthFilter : IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
if (!context.HttpContext.User.Identity.IsAuthenticated)
{
context.Result = new UnauthorizedResult();
}
}
}
2. 💾 Resource Filters
Interface: IResourceFilter or IAsyncResourceFilter
When: After authorization, before model binding. Runs around the entire action pipeline.
Use cases: Caching, logging, disabling model binding temporarily.
public class CacheResourceFilter : IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
// Before everything – check cache
}
public void OnResourceExecuted(ResourceExecutedContext context)
{
// After action and result – store cache
}
}
3. ⚡ Action Filters
Interface: IActionFilter or IAsyncActionFilter
When: Immediately before and after the action method executes.
Use cases: Input validation, logging, measuring execution time, modifying parameters.
public class LoggingActionFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
Console.WriteLine($"Action {context.ActionDescriptor.DisplayName} started");
}
public void OnActionExecuted(ActionExecutedContext context)
{
Console.WriteLine($"Action finished. Exception: {context.Exception?.Message}");
}
}
4. 🚨 Exception Filters
Interface: IExceptionFilter or IAsyncExceptionFilter
When: Only when an unhandled exception occurs in the action or subsequent filters.
Use cases: Global error handling, logging exceptions, returning custom error responses.
public class CustomExceptionFilter : IExceptionFilter
{
public void OnException(ExceptionContext context)
{
// Log the exception
// context.ExceptionHandled = true; // Mark as handled
context.Result = new ObjectResult("Something went wrong")
{
StatusCode = 500
};
}
}
5. 🎨 Result Filters
Interface: IResultFilter or IAsyncResultFilter
When: Before and after the action result executes (e.g., ViewResult, JsonResult).
Use cases: Modifying response headers, compressing output, logging results.
public class HeaderResultFilter : IResultFilter
{
public void OnResultExecuting(ResultExecutingContext context)
{
context.HttpContext.Response.Headers.Add("X-Custom-Header", "MyValue");
}
public void OnResultExecuted(ResultExecutedContext context) { }
}
🏗️ Creating and Applying Filters
Way 1: As an Attribute (Most Common)
public class MyActionFilterAttribute : Attribute, IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context) { }
public void OnActionExecuted(ActionExecutedContext context) { }
}
// Apply to controller or action
[MyActionFilter]
public class HomeController : Controller { }
Way 2: As a Service (With Dependency Injection)
public class LoggingFilter : IActionFilter
{
private readonly ILogger _logger;
public LoggingFilter(ILogger<LoggingFilter> logger) => _logger = logger;
// ... implementation
}
// Register in Program.cs
builder.Services.AddScoped<LoggingFilter>();
builder.Services.AddControllers(options =>
{
options.Filters.Add<LoggingFilter>(); // Global registration
});
Way 3: Using TypeFilter or ServiceFilter
[TypeFilter(typeof(LoggingFilter))] // Resolves from DI
public IActionResult Index() { }
🌐 Registering Filters at Different Levels
- Global Level – Applies to all controllers/actions in the app.
builder.Services.AddControllers(options => options.Filters.Add(new MyGlobalFilter())); - Controller Level – Applies to all actions in a controller.
[MyFilter] public class HomeController : Controller { } - Action Level – Applies only to a specific action.
[MyFilter] public IActionResult Index() { }
📌 Execution Order: Global → Controller → Action (outermost to innermost)
🎯 Real-World Examples
1. 🕒 Performance Profiling Filter
public class PerformanceFilter : IActionFilter
{
private Stopwatch _stopwatch;
public void OnActionExecuting(ActionExecutingContext context) =>
_stopwatch = Stopwatch.StartNew();
public void OnActionExecuted(ActionExecutedContext context)
{
_stopwatch.Stop();
Console.WriteLine($"Action took {_stopwatch.ElapsedMilliseconds} ms");
}
}
2. ✅ Model Validation Filter (Automatic)
public class ValidateModelFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(context.ModelState);
}
}
public void OnActionExecuted(ActionExecutedContext context) { }
}
3. 🔁 Caching with Resource Filter
public class CacheResourceFilter : IResourceFilter
{
private static readonly Dictionary<string, object> _cache = new();
public void OnResourceExecuting(ResourceExecutingContext context)
{
string key = context.HttpContext.Request.Path;
if (_cache.ContainsKey(key))
{
context.Result = new ObjectResult(_cache[key]);
}
}
public void OnResourceExecuted(ResourceExecutedContext context)
{
if (context.Result is ObjectResult result && result.Value != null)
{
_cache[context.HttpContext.Request.Path] = result.Value;
}
}
}
🧠 Interview Questions & Answers (Must Know)
Q1: What is the difference between Middleware and Filters?
A: Middleware runs for every request (static files, images, etc.) and is part of the core pipeline. Filters run only for MVC/API actions and have access to MVC-specific features like model state, action arguments, and results. Filters also have a more granular lifecycle (OnActionExecuting, OnResultExecuted, etc.).
Q2: How do you short-circuit a request from a filter?
A: Set context.Result to an ActionResult (e.g., BadRequestResult). The pipeline will skip the action and any remaining filters of that type.
Q3: What is the difference between OnActionExecuted and OnResultExecuting?
A: OnActionExecuted runs after the action method but before the result is executed. OnResultExecuting runs after the result has been created but before it's executed (before writing to response).
Q4: Can a filter access the action's arguments or change them?
A: Yes, in OnActionExecuting, you can access and modify context.ActionArguments. This is useful for validation or transformation.
Q5: How do you handle exceptions in filters vs middleware?
A: Exception filters catch exceptions only from MVC actions. For global exception handling including static files, use UseExceptionHandler middleware. Combine both: middleware for general errors, exception filters for API-specific error responses.
✅ Best Practices
- Keep filters single-responsibility – one filter does one thing.
- Use async filters (
IAsyncActionFilter) for I/O operations like database calls. - Prefer service filters over instantiating filters manually when DI is needed.
- Don't overuse filters – middleware might be better for cross-cutting concerns that apply to all requests.
- Always call
base.OnActionExecuting(context)if inheriting from built-in filter attributes. - For APIs, use exception filters to return standardized error responses (e.g., Problem Details).
📊 Quick Reference Table
| Filter Type | Interface | When It Runs | Common Use |
|---|---|---|---|
| Authorization | IAuthorizationFilter | First, before everything | Custom auth logic |
| Resource | IResourceFilter | After auth, before model binding | Caching |
| Action | IActionFilter | Before/after action method | Logging, validation |
| Exception | IExceptionFilter | When unhandled exception occurs | Error handling |
| Result | IResultFilter | Before/after result execution | Modifying response |
🎯 Final Key Takeaway
Filters give you fine-grained control over MVC action execution. They are perfect for cross-cutting concerns specific to your API or web app. Understanding the filter pipeline order (Authorization → Resource → Action → Exception → Result) and when to use each type is critical for both real-world applications and .NET interviews.
✅ Combine filters with middleware for a robust, maintainable ASP.NET Core application.