1. Code Example: Action Filter vs. Result Filter
To understand the difference, look at what they wrap:
- Action Filters wrap the execution of the C# method itself (they can inspect method arguments, intercept model state, or stop the action from running).
- Result Filters wrap the **execution of the
IActionResult**(they execute after the action has returned its result, right before the data is serialized and written to the HTTP response wire).
The Code Implementation
using Microsoft.AspNetCore.Mvc.Filters;
// 1. Action Filter: Intercepts before/after the controller method runs
public class PerformanceActionFilter : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
// BEFORE the action method runs:
// You can inspect or modify incoming action arguments!
if (context.ActionArguments.TryGetValue("id", out var idVal) && idVal is int id && id < 0)
{
context.Result = new BadRequestObjectResult("ID cannot be negative.");
return; // Short-circuit: The controller action will NEVER run!
}
Console.WriteLine("--> [Action Filter] Before action execution");
// Let the action execute
var executedContext = await next();
Console.WriteLine("--> [Action Filter] After action execution");
}
}
// 2. Result Filter: Intercepts before/after the IActionResult is rendered/serialized
public class CustomHeaderResultFilter : IAsyncResultFilter
{
public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutingDelegate next)
{
// BEFORE the IActionResult renders (e.g., before JSON is serialized)
// You can modify HTTP headers or response properties directly
context.HttpContext.Response.Headers["X-Custom-Server-Time"] = DateTime.UtcNow.ToString("O");
Console.WriteLine("--> [Result Filter] Before result execution");
await next(); // Executes the result (writes JSON to wire)
Console.WriteLine("--> [Result Filter] After result execution");
}
}
2. Filters vs. Middleware: What’s the Difference?
Developers coming from Java/Spring often confuse filters with servlet filters or interceptors. Here is how they stack up in ASP.NET Core:
| Feature | Middleware (IMiddleware / Request Delegate) | Filters (IActionFilter, IAuthorizationFilter, etc.) |
|---|---|---|
| Pipeline Level | Global / Server-level (Kestrel). Operates on raw bytes, headers, and request paths. | MVC / Endpoint-level. Operates within the context of controllers and actions. |
| Scope | Executes for every single request hitting the server (including static files, CSS, health checks, 404s). | Executes only when a request successfully routes to a specific MVC controller or Minimal API endpoint. |
| Context Awareness | Knows nothing about controllers, action methods, model binding, or validation states. | Deeply aware of C# types, method parameters, attributes, model states, and ActionResults. |
3. Why Do We Have Both Authorization Middleware AND Authorization Filters?
This is a classic architecture question. If we have Authorization Middleware (app.UseAuthorization()), why do we also need Authorization Filters ([Authorize])?
They serve two different layers of granularity:
A. Authorization Middleware (Global Gatekeeper)
- Role: Acts as the bouncer at the front door of the nightclub. It looks at the incoming JWT token, authenticates the user, populates
HttpContext.User, and can enforce global or route-group rules (e.g., “Block anything starting with/api/adminunless authenticated”). - Limitation: It is coarse-grained. It doesn’t know the deep internal arguments of a specific C# method.
B. Authorization Filters (Fine-Grained Action Controller Guard)
- Role: Acts as the security guard at a specific VIP room inside the club. It handles attribute-based permissions directly on controllers or methods:
[Authorize(Policy "CanDeleteUser")]
[HttpDelete("{id}")]
public IActionResult DeleteUser(int id) { ... }
- Why both? The middleware ensures the user is authenticated globally and handles routing security efficiently at the pipeline layer. The filter handles declarative, attribute-driven, resource-specific authorization right before the controller action executes, with full access to route data and action arguments.