Implementing Async Action Filters in ASP.NET Core

Async Action Filters in ASP.NET Core allow you to execute custom logic before and after a controller action is invoked. They are particularly useful for handling tasks like logging, input validation, enriching responses, and interacting with external APIs. By leveraging the asynchronous capabilities of these filters, you can build scalable and efficient applications.

This guide provides practical examples of Async Action Filters, including the recommended practices for injecting or dynamically resolving services.

What Are Async Action Filters?#

Async Action Filters are a type of filter in ASP.NET Core that let you inject logic around the execution of an action method:

  1. Before the Action Executes: Inspect or modify inputs, or prepare resources.
  2. After the Action Completes: Log results, modify responses, or clean up resources.

They are implemented using the OnActionExecutionAsync method, which wraps the execution of an action method.

Diagram illustrating ASP .NET Core filters pipeline

Diagram illustrating ASP .NET Core filters pipeline. Source: https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-8.0

The ASP.NET Core Request Pipeline

Filters operate within the MVC pipeline after middleware processes a request.

The diagram from Microsoft shows the execution order of filters.

Filters are executed in the following order:

  1. Authorization Filters: Validate access control.
  2. Resource Filters: Handle tasks like caching or resource initialization.
  3. Exception Filters: Handle unhandled exceptions.
  4. Action Filters: Execute logic before and after the action method.
  5. Result Filters: Modify the action result before sending it to the client.

Async Action Filters specifically wrap around the execution of action methods.

To understand how action filters work in ASP .NET Core - I encourage you to read Filters in ASP.NET Core on the MS website.

Async Action Filters Real-Life Examples#

1. Logging Requests and Responses

Log every incoming request and its corresponding response.

using System.Diagnostics;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;

public class LoggingFilter : IAsyncActionFilter
{
    private readonly ILogger<LoggingFilter> _logger;

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

    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        var stopwatch = Stopwatch.StartNew();
        _logger.LogInformation($"[Request] {context.HttpContext.Request.Method} {context.HttpContext.Request.Path}");

        var resultContext = await next(); // Execute the action

        stopwatch.Stop();
        _logger.LogInformation($"[Response] Status: {resultContext.HttpContext.Response.StatusCode}, Time Taken: {stopwatch.ElapsedMilliseconds}ms");
    }
}

Register the Filter:

services.AddScoped<LoggingFilter>();

Apply the Filter:

[ServiceFilter(typeof(LoggingFilter))]
public IActionResult GetData()
{
    return Ok("Data fetched successfully!");
}

2. Input Validation

Prevent invalid API requests from executing the action.

public class InputValidationFilter : IAsyncActionFilter
{
    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        if (!context.ActionArguments.ContainsKey("id") || (int)context.ActionArguments["id"] <= 0)
        {
            context.Result = new BadRequestObjectResult(new { Error = "Invalid ID provided" });
            return;
        }

        await next(); // Proceed to the action if validation passes
    }
}

Register:

services.AddScoped<InputValidationFilter>();

Apply:

[ServiceFilter(typeof(InputValidationFilter))]
public IActionResult GetItem(int id)
{
    return Ok(new { Id = id, Name = "Sample Item" });
}

3. Enriching Responses with Headers

Add a custom header to responses after the action executes.

public class AddHeaderFilter : IAsyncActionFilter
{
    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        var resultContext = await next(); // Execute the action

        resultContext.HttpContext.Response.Headers.Add("X-Custom-Header", "This is a custom header");
    }
}

Register:

services.AddScoped<AddHeaderFilter>();

Apply:

[ServiceFilter(typeof(AddHeaderFilter))]
public IActionResult GetInfo()
{
    return Ok(new { Info = "This is an enriched response" });
}

4. Dynamically Resolving Services

Dynamically retrieve services using RequestServices for conditionally required services.

using Microsoft.Extensions.DependencyInjection;

public class ConditionalServiceFilter : IAsyncActionFilter
{
    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        var serviceProvider = context.HttpContext.RequestServices;
        var logger = serviceProvider.GetService<ILogger<ConditionalServiceFilter>>();

        logger?.LogInformation($"[Executing] {context.ActionDescriptor.DisplayName}");

        var resultContext = await next(); // Execute the action

        logger?.LogInformation($"[Executed] {context.ActionDescriptor.DisplayName}");
    }
}

Register:

services.AddScoped<ConditionalServiceFilter>();

Apply:

[ServiceFilter(typeof(ConditionalServiceFilter))]
public IActionResult FetchData()
{
    return Ok("Data fetched with dynamic service resolution!");
}

Async Action Filters Best Practices#

1. Call await next():
Ensure you call await next() to maintain the pipeline flow unless you intentionally short-circuited the request.

2. Keep Filters Lightweight:
Avoid heavy processing inside filters. Delegate complex logic to services to improve maintainability and performance.

3. Scope Filters Appropriately:

  • Use global filters for shared concerns like logging.
  • Use action-specific filters like input validation for localized logic.

4. Prefer Constructor Injection for Frequently Used Services:
Inject essential services directly into the filter’s constructor. This is the recommended approach for strong typing and easier testability.

5. Use RequestServices for Conditionally Required Services:
Dynamically resolve services using RequestServices only when they are optional or infrequently needed to avoid unnecessary dependencies.

Async Action Filters Common Pitfalls#

1. Overusing Filters for Global Concerns
Use middleware for application-wide tasks like authentication or error handling instead of filters.

2. Blocking Asynchronous Code
Avoid .Wait() or .Result in filters, as these can cause deadlocks. Always use await for asynchronous operations.

3. Forgetting to Register Filters
All filters used with [ServiceFilter] or [TypeFilter] must be registered in the DI container. Forgetting to register leads to runtime errors.

Conclusion#

Async Action Filters are a powerful way to add logic around controller actions, whether for logging, validation, or response customization.

Always register your filters in the DI container and ensure proper scoping for effective and maintainable implementations.

↑ Top ↑