Filters in ASP.NET Core
In ASP.NET Core, filters are components that run custom logic at specific stages of the request processing pipeline. They are a tool for handling cross-cutting concerns, such as logging, validation, authorization, and caching, without cluttering the business logic in your controllers.
Types of Filters
ASP.NET Core provides six main types of filters, each operating at a distinct point in the request execution pipeline:
- Authorization filters: The initial filters to run, responsible for verifying user permissions. If authorization fails, the pipeline is halted.
- Resource filters: Execute after authorization but before model binding. They can be used for caching and can terminate the pipeline by returning a cached response.
- Action filters: Surround the execution of a controller action method, allowing modification of arguments or results.
- Exception filters: Handle unhandled exceptions during the execution of actions or other filters, enabling centralized error management.
- Result filters: Run just before and after the execution of an action result, useful for modifying responses or adding headers.
- Endpoint filters: A newer, lightweight filter type designed for Minimal APIs, functioning similarly to action filters for route-handler-based endpoints.
Why Use Filters?
- Separation of concerns: They help isolate cross-cutting concerns from core business logic, contributing to cleaner code.
- Code reusability: Common logic can be placed in reusable filters, reducing duplication.
- Global policies: Filters can enforce consistent policies across applications, such as uniform error handling.
- Pipeline control: They provide control over the request pipeline at various stages.
Benefits of Using Filters
- Avoid code duplication across controllers and actions.
- Centralize logic for concerns like authorization, logging, or exception handling.
- Control execution flow before and after actions or results.
- Improve testability and readability by separating infrastructure logic from business logic.
Using Filters
We can apply filters in three ways:
-
Globally: in
Startup.cs or Program.cs
services.AddControllersWithViews(options =>
{
options.Filters.Add(typeof(LogActionFilter)); // Applies to all actions
});
-
At Controller Level: by decorating the controller class with filter attributes.
[Authorize]
[ServiceFilter(typeof(LogActionFilter))]
public class AdminController : Controller
{
// All actions here inherit the filters
}
-
At Action Level: by decorating individual action methods with filter attributes.
[HttpGet]
[TypeFilter(typeof(LogActionFilter))]
public IActionResult GetData()
{
return Ok("Filtered!");
}
Custom Filter
You can create custom filters by implementing specific filter interfaces or inheriting from provided base classes. Examples include:
- Creating a custom action filter to log execution time.
- Creating a global exception filter to handle API errors consistently.
- Then register it via DI and apply using
[ServiceFilter] or [TypeFilter].
Example: Custom Action Filter
using Microsoft.AspNetCore.Mvc.Filters;
public class LogActionFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
Console.WriteLine("Before action executes");
}
public void OnActionExecuted(ActionExecutedContext context)
{
Console.WriteLine("After action executes");
}
}
Applying the Filter
[ServiceFilter(typeof(LogActionFilter))]
public IActionResult MyAction()
{
return Ok("Filtered!");
}
Best Practices for Using Filters
- Single responsibility: Design filters to handle one specific concern. Use global filters for logging, error handling, and authorization.
- Appropriate scope: Apply filters at the narrowest necessary level (globally, controller, or action).
- Avoid business logic: Keep core business logic out of filters. Keep filters focused on infrastructure concerns—not business logic.
- Dependency Injection: Use
[ServiceFilter] or [TypeFilter] for filters requiring dependencies.
- Asynchronous operations: Prefer async filters for non-blocking operations. Use asynchronous filter interfaces for I/O-bound tasks.
- Middleware for non-MVC logic: Use middleware for logic not dependent on MVC context.
- Short-circuit wisely in authorization or caching filters.
- Test filters independently for predictable behavior.
Disadvantages of Filters in ASP.NET Core
While filters offer powerful extensibility in the MVC pipeline, they come with trade-offs that developers should consider when designing applications.
1. Tight Coupling to MVC Pipeline
- Filters are bound to the MVC lifecycle and cannot be reused outside controller contexts.
- Middleware is preferred for cross-cutting concerns outside MVC.
2. Limited Granularity
- Filters execute at fixed stages (e.g., before/after action), limiting fine-grained control.
- Complex scenarios may require custom middleware or endpoint filters.
3. Testing Complexity
- Filters often depend on
ActionContext, HttpContext, or DI services.
- Unit testing requires mocks and setup for pipeline simulation.
4. Fragmented Logic Risk
- Overuse of filters can scatter logic across layers, making debugging harder.
- May violate cohesion if not scoped and documented properly.
5. Confusing Execution Order
- Global, controller, and action-level filters may interact in unexpected ways.
- Misunderstanding the order can lead to skipped logic or duplicated behavior.
6. Not Suitable for Business Logic
- Filters should handle infrastructure concerns like logging, auth, and caching.
- Embedding domain logic in filters reduces maintainability and clarity.
Mitigation Strategies
- Use middleware for non-MVC concerns.
- Design filters with single responsibility.
- Apply filters at the narrowest necessary scope.
- Use
[ServiceFilter] or [TypeFilter] for DI support.
- Prefer async filters for non-blocking operations.
- Document filter usage clearly in architecture guides.