Skip to main content

.NET Filters

Introduction

Filters are powerful components in ASP.NET Core that allow you to execute code before or after specific stages in the request processing pipeline. Think of filters as middleware that's scoped to controller actions or controllers themselves, rather than the entire application. They provide a way to encapsulate cross-cutting concerns such as logging, error handling, authorization, and caching.

In this guide, we'll explore the different types of filters available in ASP.NET Core, when to use them, and how to implement them effectively in your web applications.

Understanding the Filter Pipeline

Before diving into specific filter types, it's important to understand where filters fit in the ASP.NET Core request pipeline. Filters run within the ASP.NET Core MVC action invocation pipeline, which is different from the middleware pipeline.

The filter pipeline executes in a specific order:

  1. Authorization filters
  2. Resource filters (before action)
  3. Model binding
  4. Action filters (before action)
  5. Action execution
  6. Action filters (after action)
  7. Result filters (before result)
  8. Result execution
  9. Result filters (after result)
  10. Resource filters (after action)
  11. Exception filters (if needed)

This sequential execution allows for fine-grained control at different stages of request processing.

Types of Filters in ASP.NET Core

1. Authorization Filters

Authorization filters run first and are responsible for determining whether the current user is authorized for the requested action.

Example: Creating a Custom Authorization Filter

csharp
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

public class MinimumAgeRequirementFilter : IAuthorizationFilter
{
private readonly int _minimumAge;

public MinimumAgeRequirementFilter(int minimumAge)
{
_minimumAge = minimumAge;
}

public void OnAuthorization(AuthorizationFilterContext context)
{
var userAge = context.HttpContext.User.FindFirst("Age")?.Value;

if (string.IsNullOrEmpty(userAge) || int.Parse(userAge) < _minimumAge)
{
context.Result = new ForbidResult();
}
}
}

How to Apply the Filter:

csharp
[TypeFilter(typeof(MinimumAgeRequirementFilter), Arguments = new object[] { 18 })]
public IActionResult AdultOnlyAction()
{
return View();
}

2. Resource Filters

Resource filters implement the IResourceFilter interface and run after authorization but before model binding. They can short-circuit the pipeline or perform actions after the rest of the pipeline completes.

Example: Caching Resource Filter

csharp
public class CachingResourceFilter : IResourceFilter
{
private static readonly Dictionary<string, object> _cache = new Dictionary<string, object>();
private readonly string _cacheKey;

public CachingResourceFilter(string cacheKey)
{
_cacheKey = cacheKey;
}

public void OnResourceExecuting(ResourceExecutingContext context)
{
if (_cache.TryGetValue(_cacheKey, out var cachedResult))
{
context.Result = cachedResult as IActionResult;
}
}

public void OnResourceExecuted(ResourceExecutedContext context)
{
if (!context.Canceled && context.Result != null)
{
_cache[_cacheKey] = context.Result;
}
}
}

Usage:

csharp
[TypeFilter(typeof(CachingResourceFilter), Arguments = new object[] { "HomeIndex" })]
public IActionResult Index()
{
return View(DateTime.Now);
}

3. Action Filters

Action filters run before and after action execution. They're useful for manipulating the arguments passed to an action or the result returned from it.

Example: Action Filter for Logging

csharp
public class LogActionFilter : IActionFilter
{
private readonly ILogger<LogActionFilter> _logger;

public LogActionFilter(ILogger<LogActionFilter> logger)
{
_logger = logger;
}

public void OnActionExecuting(ActionExecutingContext context)
{
// Log before the action executes
_logger.LogInformation(
$"Action {context.ActionDescriptor.DisplayName} is executing");
}

public void OnActionExecuted(ActionExecutedContext context)
{
// Log after the action executes
_logger.LogInformation(
$"Action {context.ActionDescriptor.DisplayName} executed");
}
}

As an Attribute:

csharp
public class LogActionAttribute : TypeFilterAttribute
{
public LogActionAttribute() : base(typeof(LogActionFilter))
{
}
}

Usage:

csharp
[LogAction]
public IActionResult About()
{
return View();
}

4. Exception Filters

Exception filters handle exceptions that occur during action execution, result execution, or other filter stages.

Example: Global Exception Filter

csharp
public class GlobalExceptionFilter : IExceptionFilter
{
private readonly ILogger<GlobalExceptionFilter> _logger;
private readonly IHostEnvironment _env;

public GlobalExceptionFilter(
ILogger<GlobalExceptionFilter> logger,
IHostEnvironment env)
{
_logger = logger;
_env = env;
}

public void OnException(ExceptionContext context)
{
_logger.LogError(context.Exception, "Unhandled exception occurred");

var result = new ViewResult { ViewName = "Error" };

// Add more details in development environment
if (_env.IsDevelopment())
{
result.ViewData["Exception"] = context.Exception;
}

context.Result = result;
context.ExceptionHandled = true;
}
}

Registration in Startup:

csharp
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews(options =>
{
options.Filters.Add<GlobalExceptionFilter>();
});
}

5. Result Filters

Result filters run before and after action results are executed. They're ideal for manipulating the output being sent to the client.

Example: Response Caching Filter

csharp
public class ResponseCachingFilter : IResultFilter
{
public void OnResultExecuting(ResultExecutingContext context)
{
// Set caching headers before the result executes
context.HttpContext.Response.Headers.Add(
"Cache-Control", "public, max-age=60");
}

public void OnResultExecuted(ResultExecutedContext context)
{
// Code that runs after the result has executed
}
}

Usage:

csharp
[TypeFilter(typeof(ResponseCachingFilter))]
public IActionResult Privacy()
{
return View();
}

Filter Scopes

Filters can be applied at three different scopes:

  1. Global filters - Apply to all controllers and actions
  2. Controller filters - Apply to all actions within a controller
  3. Action filters - Apply to a specific action method

Example: Registering Global Filters

csharp
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews(options =>
{
options.Filters.Add(new LogActionAttribute());
options.Filters.Add<GlobalExceptionFilter>();
});
}

Example: Controller-Level Filter

csharp
[ServiceFilter(typeof(LogActionFilter))]
public class HomeController : Controller
{
// All actions in this controller will use the LogActionFilter
}

Example: Action-Level Filter

csharp
public class ProductController : Controller
{
[TypeFilter(typeof(CachingResourceFilter), Arguments = new object[] { "ProductDetails" })]
public IActionResult Details(int id)
{
return View();
}
}

Built-in Filters

ASP.NET Core provides several built-in filters that handle common scenarios:

1. Authorization Filter

csharp
[Authorize]
public IActionResult SecureData()
{
return View();
}

2. Response Cache Filter

csharp
[ResponseCache(Duration = 60)]
public IActionResult Index()
{
return View();
}

3. Action Filter to Validate Model State

csharp
[ValidateAntiForgeryToken]
[HttpPost]
public IActionResult Create(ProductViewModel product)
{
if (!ModelState.IsValid)
{
return View(product);
}

// Process the product
return RedirectToAction("Index");
}

Real-World Application: API Request/Response Logging

Let's create a comprehensive logging filter for API endpoints that logs both request and response data:

csharp
public class ApiLoggingFilter : IActionFilter
{
private readonly ILogger<ApiLoggingFilter> _logger;
private Stopwatch _timer;

public ApiLoggingFilter(ILogger<ApiLoggingFilter> logger)
{
_logger = logger;
}

public void OnActionExecuting(ActionExecutingContext context)
{
_timer = Stopwatch.StartNew();

// Log request details
var request = context.HttpContext.Request;
var requestBody = "Not available";

if (request.Body.CanSeek)
{
request.EnableBuffering();
using var reader = new StreamReader(request.Body, leaveOpen: true);
requestBody = reader.ReadToEndAsync().Result;
request.Body.Position = 0;
}

_logger.LogInformation(
$"API Request - {request.Method} {request.Path} - Body: {requestBody}");
}

public void OnActionExecuted(ActionExecutedContext context)
{
_timer.Stop();

// Log response details
var response = context.HttpContext.Response;
var responseContent = "Not available";

if (context.Result is ObjectResult objectResult)
{
responseContent = JsonSerializer.Serialize(objectResult.Value);
}

_logger.LogInformation(
$"API Response - Status: {response.StatusCode} - " +
$"Time: {_timer.ElapsedMilliseconds}ms - Body: {responseContent}");
}
}

Using as an Attribute:

csharp
public class ApiLoggingAttribute : TypeFilterAttribute
{
public ApiLoggingAttribute() : base(typeof(ApiLoggingFilter))
{
}
}

In an API Controller:

csharp
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IProductRepository _repository;

public ProductsController(IProductRepository repository)
{
_repository = repository;
}

[HttpGet]
[ApiLogging]
public async Task<ActionResult<IEnumerable<Product>>> GetProducts()
{
var products = await _repository.GetAllAsync();
return Ok(products);
}
}

Best Practices for Using Filters

  1. Choose the right filter type - Select the filter type based on when you need to intervene in the pipeline.
  2. Keep filters focused - Each filter should handle a single responsibility.
  3. Consider performance - Be mindful of performance implications, especially for filters that run on every request.
  4. Use dependency injection - Use ServiceFilter or TypeFilter when your filter requires dependencies.
  5. Global vs. scoped filters - Only register filters globally when they truly need to run for all requests.
  6. Order matters - Remember that filters run in a specific order - be careful with dependencies between filters.

Summary

Filters in ASP.NET Core provide a powerful way to implement cross-cutting concerns in your web applications. They offer a more granular approach than middleware, allowing you to target specific controllers or actions. We've explored the different types of filters, how to implement them, and real-world applications.

Key takeaways:

  • Filters run at specific stages in the request processing pipeline
  • There are five main types of filters: authorization, resource, action, exception, and result
  • Filters can be applied globally, to controllers, or to individual actions
  • Built-in filters handle common scenarios like authorization and caching
  • Custom filters can be created to handle application-specific requirements

Additional Resources

Exercises

  1. Create a custom action filter that times how long actions take to execute and logs the duration.
  2. Implement a resource filter that caches responses for GET requests for a configurable duration.
  3. Build an exception filter that returns custom JSON error responses for API controllers.
  4. Create a result filter that adds a custom header to all responses.
  5. Implement a global authentication filter that verifies a custom API key header.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)