Back to: ASP.NET Core Web API Tutorials
Resource Filters in ASP.NET Core Web API
In this article, I will discuss Resource Filters in ASP.NET Core Web API Applications with Examples. Please read our previous article discussing Authorization Filters in ASP.NET Core Web API Applications.
What are Resource Filters in ASP.NET Core Web API?
Resource Filters are a specific type of filter in ASP.NET Core that execute:
- After Authorization Filters, ensure the request is authenticated and authorized.
- Before Model Binding, interaction with the HTTP request and response should be allowed early in the request pipeline.
This makes Resource Filters ideal for scenarios requiring pre-processing logic (e.g., caching, validation) or initializing resources before the action method executes.
When to Use Resource Filters:
Resource Filters are particularly useful in scenarios where we need to execute logic after authentication and authorization and before the model binding process. Common use cases include
- Caching: Since Resource Filters execute before model binding, we can use them to check if a response has already been cached and directly return it, avoiding the need to execute the controller action entirely. This is useful for optimizing performance by avoiding repeated processing of the same requests.
- Global Logic: They can be used to implement resource-level checks such as validating headers or initializing context data that multiple action methods might require.
- Resource Initialization: Resource Filters are also useful for initializing objects, services, or data that the controller actions will need, but only after the request has passed through authorization.
How to Use Resource Filters in ASP.NET Core Web API:
To implement a Resource Filter in an ASP.NET Core Web API application, we need to follow the below steps:
- Create a Custom Filter Class: Define a class that either implements the IAsyncResourceFilter or IResourceFilter interface.
- Override the Appropriate Method: Implement the OnResourceExecutionAsync (for async operations) or OnResourceExecuting/OnResourceExecuted methods for synchronous operations. Define the custom logic to be executed before and/or after the action method.
- Register the Filter: Apply the filter to specific controllers or actions using attributes, or register it globally for all controllers using dependency injection in Program.cs.
Understanding Resource Filter Execution
- Before the Action Pipeline: Code in OnResourceExecuting (or before await next() in OnResourceExecutionAsync) runs before model binding and the rest of the action pipeline.
- After the Action Pipeline: Code in OnResourceExecuted (or after await next() in OnResourceExecutionAsync) runs after the action result has been executed.
- Short-Circuiting: By setting context.Result, you can short-circuit the request pipeline, preventing further processing.
Implementing Response Caching and Header Validation using Custom Resource Filters
Suppose we have a Web API application that provides product data for an e-commerce application. Fetching this data is resource-intensive due to database interactions or external API calls. To improve performance, we can use a custom Resource Filter to cache the responses so that repeated requests return cached data within a certain timeframe.
Create the ASP.NET Core Web API Project
First, create a new ASP.NET Core Web API project with the name ResourceFilterDemo.
Define the Products Controller
Create a Products Controller with an action that returns product data. So, create an empty API Controller named ProductsController within the Controllers folder and copy and paste the following code.
using Microsoft.AspNetCore.Mvc; namespace ResourceFilterDemo.Controllers { [Route("api/[controller]")] [ApiController] public class ProductsController : ControllerBase { [HttpGet] public async Task<IActionResult> GetProductsAsync() { // Simulate a resource-intensive operation await Task.Delay(3000); // 3-second delay List<string> _products = new() { "Laptop", "Smartphone", "Tablet" }; return Ok(_products); } } }
Create the Custom Resource Filter for Response Caching
Implement a custom Resource Filter to handle response caching. This filter checks if a response for the current request is already cached and serves it if available. Otherwise, it executes the action and caches the response for future requests.
So, create a class file named CustomResponseCachingFilter.cs within the Models folder and copy and paste the following code. As you can see, this class implements the IAsyncResourceFilter interface.
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; namespace ResourceFilterDemo.Models { public class CustomResponseCachingFilter :IAsyncResourceFilter { private readonly IMemoryCache _cache; private readonly TimeSpan _cacheDuration; public CustomResponseCachingFilter(IMemoryCache cache, TimeSpan cacheDuration) { _cache = cache; _cacheDuration = cacheDuration; } public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next) { var cacheKey = GenerateCacheKeyFromRequest(context.HttpContext.Request); if (_cache.TryGetValue(cacheKey, out IActionResult? cachedResponse)) { // Return cached response context.Result = cachedResponse; return; //Short-circuit the Request } // Proceed to execute the action var executedContext = await next(); // Cache the response if it's successful if (executedContext.Result is OkObjectResult okResult) { _cache.Set(cacheKey, okResult, _cacheDuration); } } private string GenerateCacheKeyFromRequest(Microsoft.AspNetCore.Http.HttpRequest request) { var key = $"{request.Path}"; foreach (var (keyName, value) in request.Query) { key += $"|{keyName}-{value}"; } return key; } } }
Understanding OnResourceExecutionAsync Method:
- Generate Cache Key: To ensure consistent caching, a unique cache key is generated based on the request path and query parameters.
- Check Cache: Tries to retrieve a cached response. If found, it sets the context.Result to the cached response and short-circuits the pipeline.
- Action Execution: If no cached response is found, it proceeds with the action execution by calling await next().
- Caching the Response: After action execution, if the result is an OkObjectResult, it caches the response.
Add the API Key to the Configuration
To secure the API, we’ll require clients to provide an API key. To do so, add an ApiKey setting to the appsettings.json file. Modify the appsettings.json file as follows. We want the client to include one header and pass the ApiKey value to consider the request a valid request.
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "ApiKey": "YourSecretApiKey123" }
Custom Request Validation Filter:
The following filter ensures that incoming requests include the correct API key in the headers. If the API key is missing or invalid, the request is rejected. So, create a class file named CustomRequestValidationFilter.cs within the Models folder and copy and paste the following code.
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc; namespace ResourceFilterDemo.Models { public class CustomRequestValidationFilter : IAsyncResourceFilter { private readonly string? _apiKey; public CustomRequestValidationFilter(IConfiguration configuration) { _apiKey = configuration.GetValue<string>("ApiKey"); } public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next) { if(!string.IsNullOrEmpty(_apiKey)) { // Example: Validate that the request has a required query parameter "api_key" if (!context.HttpContext.Request.Headers.TryGetValue("X-API-KEY", out var extractedApiKey)) { context.Result = new ContentResult { StatusCode = 401, Content = "API Key was not provided." }; return; // Short-circuit the request } if (!_apiKey.Equals(extractedApiKey)) { context.Result = new ContentResult { StatusCode = 401, Content = "Unauthorized client." }; return; } } // Proceed to execute the action await next(); } } }
Code Explanation:
- API Key Retrieval: Fetches the ApiKey value from the configuration.
- Header Validation: Checks if the X-API-KEY header is present in the request.
- Missing Header: Returns a 401 Unauthorized response with an appropriate message.
- Invalid Key: Returns a 401 Unauthorized response if the provided API key does not match the configured key.
- Valid Request: Proceeds to execute the action method if the API key is valid.
Register the Filters and Memory Cache Service
Configure the services and register the custom filters in Program.cs to make them available throughout the application. So, modify the Program class as follows.
using Microsoft.Extensions.Caching.Memory; using ResourceFilterDemo.Models; namespace ResourceFilterDemo { public class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); // Add services to the application's dependency injection container. builder.Services.AddControllers() .AddJsonOptions(options => { // Configure JSON serialization to retain property names as defined in the C# model. // This disables the default camelCase naming policy. options.JsonSerializerOptions.PropertyNamingPolicy = null; }); // Register MemoryCache service builder.Services.AddMemoryCache(); // Register the custom ResponseCachingFilter with a cache duration of 60 seconds builder.Services.AddScoped<CustomResponseCachingFilter>(serviceProvider => { var cache = serviceProvider.GetRequiredService<IMemoryCache>(); return new CustomResponseCachingFilter(cache, TimeSpan.FromSeconds(60)); }); // Register the custom CustomRequestValidationFilter builder.Services.AddScoped<CustomRequestValidationFilter>(); // Optionally, register the filter globally builder.Services.AddControllers(options => { options.Filters.AddService<CustomResponseCachingFilter>(); options.Filters.AddService<CustomRequestValidationFilter>(); }); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run(); } } }
Code Explanation:
- AddMemoryCache: Registers the in-memory caching service essential for caching responses.
- Register Custom Filters:
- CustomResponseCachingFilter: Registered with a scoped lifetime and configured to cache responses for 60 seconds.
- CustomRequestValidationFilter: Registered with a scoped lifetime to validate incoming API keys.
- Global Registration: Custom filters are added globally so that they apply to all controllers and action methods without needing to decorate each one individually.
- JSON Options: Configured to preserve property names as defined in the C# models by disabling the default camelCase naming policy.
- Swagger Configuration: Enables Swagger for API documentation and testing in the development environment.
Testing the Application:
Now, Test the API endpoint, and it should work as expected.
Using IResourceFilter to Create Custom Resource Filter:
Let us modify the CustomResponseCachingFilter to use the IResourceFilter interface. Now, the class will inherit from the IResourceFilter interface instead of IAsyncResourceFilter and needs to implement the OnResourceExecuting and OnResourceExecuted methods. So, please modify the CustomResponseCachingFilter as follows. The following code is self-explained, so please read the comment lines for a better understanding.
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; namespace ResourceFilterDemo.Models { public class CustomResponseCachingFilter : IResourceFilter { private readonly IMemoryCache _cache; private readonly TimeSpan _cacheDuration; public CustomResponseCachingFilter(IMemoryCache cache, TimeSpan cacheDuration) { _cache = cache; _cacheDuration = cacheDuration; } // Synchronous version of the resource filter method which execue before the action method execution public void OnResourceExecuting(ResourceExecutingContext context) { // Generate a unique cache key based on the request's path and query parameters var cacheKey = GenerateCacheKeyFromRequest(context.HttpContext.Request); // Try to retrieve a cached response for the generated cache key if (_cache.TryGetValue(cacheKey, out IActionResult? cachedResponse)) { // If a cached response exists, set it as the result to short-circuit the pipeline context.Result = cachedResponse; return; // End the pipeline execution here } } // Synchronous version of the resource filter method which execue after the action method execution public void OnResourceExecuted(ResourceExecutedContext context) { // Generate the cache key again based on the request path and query parameters var cacheKey = GenerateCacheKeyFromRequest(context.HttpContext.Request); // Check if the action execution resulted in a successful response if (context.Result is OkObjectResult okResult) { // Store the response in the in-memory cache with the specified cache duration _cache.Set(cacheKey, okResult, _cacheDuration); } } // Private helper method to generate a unique cache key for a given HTTP request private string GenerateCacheKeyFromRequest(HttpRequest request) { // Start with the request path as the base of the cache key var key = $"{request.Path}"; // Append each query parameter and its value to the cache key foreach (var (keyName, value) in request.Query) { key += $"|{keyName}-{value}"; } return key; // Return the constructed cache key } } }
Key Changes
The IResourceFilter interface doesn’t support asynchronous execution, making it suitable for operations like caching where asynchronous behavior isn’t critical. We have replaced IAsyncResourceFilter with IResourceFilter to handle the logic synchronously.
- OnResourceExecuting handles logic before the action executes (e.g., checking the cache and short-circuiting the request).
- OnResourceExecuted handles logic after the action executes (e.g., storing the response in the cache).
With the above changes in place, test the application, and it should work as expected.
Applying Filters at Controller and Action Method Level:
To apply custom filters at Controller and Action Method Levels, we need to use TypeFilter and ServiceFilter attributes. We will discuss their differences and when to use which one in our upcoming articles. For now, register the custom filters using these attributes at the controller or action method level. So, please modify the ProductsController as follows:
using Microsoft.AspNetCore.Mvc; using ResourceFilterDemo.Models; namespace ResourceFilterDemo.Controllers { [ServiceFilter(typeof(CustomResponseCachingFilter))] // Apply caching filter at Controller Level [Route("api/[controller]")] [ApiController] public class ProductsController : ControllerBase { [HttpGet] [TypeFilter(typeof(CustomRequestValidationFilter))] // Apply validation filter at action method level public async Task<IActionResult> GetProductsAsync() { // Simulate a resource-intensive operation await Task.Delay(3000); // 3-second delay List<string> _products = new() { "Laptop", "Smartphone", "Tablet" }; return Ok(_products); } } }
With the above changes in place, test the application, and it should work as expected.
Resource Filters in ASP.NET Core Web API provide a powerful mechanism for executing code before and after the action pipeline. They are ideal for implementing cross-cutting concerns like caching, request validation, and resource initialization. By using Resource Filters effectively, we can enhance our Web API applications’ performance, security, and maintainability.
In the next article, I will discuss Exception Filters in ASP.NET Core Web API Applications. In this article, I explain Resource Filters in ASP.NET Core Web API Applications. I hope you enjoy this Resource Filters in ASP.NET Core Web API article.
Exception Filters in ASP.NET Core Web API is missing