Filters and Attributes — Hooks Around an Action
Middleware sees every request but knows nothing about which controller action will run. Some rules need that knowledge: "log the arguments of every borrow action", "reject this action if the book has no copies left". Filters hook in around the action itself, and attributes stick them right where they apply.
The rule
Middleware wraps every request; filters wrap the chosen action.
An attribute is metadata in square brackets on a class, method or parameter. You have already used several: [HttpGet], [Route], [ApiController], [FromBody], and validation rules like [Required] and [Range(1, 10)]. Some attributes attach filters, such as [ServiceFilter] below. [Authorize] is metadata too: the authorization middleware reads it and refuses the request before your action is reached.
MVC and Razor Pages run filters in fixed stages:
- Authorization filters — custom "may this caller run it?" checks.
- Resource filters — before model binding; good for caching.
- (model binding and validation)
- Action filters — just before and after the action; can see its arguments.
- Exception filters — handle exceptions thrown by the action.
- Result filters — around the writing of the result.
Any filter can short-circuit by setting a result, and then the action never runs.
public class CopiesAvailableFilter(LibraryDb db) : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(
ActionExecutingContext context, ActionExecutionDelegate next)
{
var bookId = (int)context.ActionArguments["bookId"]!;
if (await db.CopiesAvailableAsync(bookId) == 0)
{
context.Result = new ConflictObjectResult("No copies left.");
return; // the action is skipped
}
await next(); // run the action
}
}
[HttpPost("{bookId:int}/borrow")]
[ServiceFilter(typeof(CopiesAvailableFilter))]
public IActionResult Borrow(int bookId) { /* ... */ }Register the filter with builder.Services.AddScoped<CopiesAvailableFilter>(); so [ServiceFilter] can build it. Minimal APIs have their own version: endpoint filters, added with .AddEndpointFilter(...).
The shortcut
Ask two questions. Does the rule apply to every request? Use middleware. Does it need the action or its bound arguments? Use a filter — an action filter if it needs the arguments.
Log every request's duration
Every request, no action knowledge needed. Middleware.
The trap: parsing the URL inside middleware to guess which action will run. That is exactly what model binding and filters already do.
Drills
Answer fast — trust the rule.
Which filter runs first for an MVC action?
A rule needs the action's bound bookId argument. Which filter fits?
How does a filter stop the action from running?
What does [Range(1, 10)] on a property do in an [ApiController]?
Remember
- Middleware wraps every request; filters wrap the chosen action.
- Authorization → resource → binding → action → exception → result.
- Need the bound arguments? Action filter, attached with an attribute.
