Action Filters in ASP.NET Core Web API

Action Filters in ASP.NET Core Web API

In this article, I will discuss Action Filters in ASP.NET Core Web API Applications with Examples. Please read our previous article discussing Resource Filters in ASP.NET Core Web API Applications. Action Filters in ASP.NET Core Web API are a powerful way to encapsulate logic that should run before or after the execution of action methods. While Resource Filters operate at the very start and end of the pipeline, Action Filters focus specifically on action method-level behaviour like logging, validation, timing, exception monitoring, etc.

What is an Action Filter?

In ASP.NET Core Web API, an Action Filter is a component that allows us to inject custom logic:

  • Pre-Action Processing: Before an action method runs (OnActionExecuting) to validate inputs, modify parameters, set up required context, or short-circuit the request.
  • Post-Action Processing: After an action method completes (OnActionExecuted) to modify the action result, perform logging, or clean up resources.

Action Filters provide a clean, reusable way to encapsulate cross-cutting concerns related to action execution, such as logging, validation, transformation, or monitoring.

When Should We Use Action Filters in ASP.NET Core Web API?

We need to use Action Filters in ASP.NET Core Web API when we need to implement cross-cutting concerns that require executing custom logic immediately before and/or after an action method runs. They are ideal for scenarios like input validation, logging, authorization checks, response formatting, or measuring performance, where this behavior needs to be applied consistently across multiple actions or controllers without cluttering the business logic. Action Filters help keep controllers clean, promote code reuse, and centralize common processing related to action execution.

Example: Restaurant Waiter

When Should We Use Action Filters in ASP.NET Core Web API?

Imagine an action filter as a waiter in a restaurant:

  • Before you order (OnActionExecuting): The waiter checks if your reservation is valid, takes your preferences, and notes any allergies.
  • After you finish eating (OnActionExecuted): The waiter brings you the bill, asks for feedback, or offers dessert, ensuring you had a good experience.

The waiter (action filter) handles these tasks for each individual guest (request/action), not for the whole restaurant (resource).

Online Shopping Order Validation

Action Filters in ASP.NET Core Web API Applications with Examples

Suppose you want to place an order online from an e-commerce website.

  • Before placing an order (OnActionExecuting): The system checks if the customer’s shopping cart is not empty and if all items are in stock. If the cart is empty or items are out of stock, it immediately stops the order from going through and sends back a message like “Your cart is empty” or “Some items are unavailable.”
  • After placing the order (OnActionExecuted): Once the order is successfully placed, the system sends a confirmation email and updates inventory counts.

The “before” check is like OnActionExecuting, where you can validate input and short-circuit the action. The “after” step is like OnActionExecuted, where you perform follow-up tasks such as notifications or cleanup.

How Do Action Filters Work in ASP.NET Core Web API?

ASP.NET Core provides two interfaces for creating action filters:

IActionFilter: For synchronous operations with two methods:

  • OnActionExecuting(ActionExecutingContext context)
  • OnActionExecuted(ActionExecutedContext context)

IAsyncActionFilter: For asynchronous operations with one method:

  • Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)

You can implement either interface or inherit from the base class ActionFilterAttribute, which provides default implementations and allows easy attribute-based usage. Action Filters execute at two critical points in the HTTP request pipeline:

OnActionExecuting (Before Action Execution)
  • This method runs just before the action method is invoked.
  • You can inspect or modify input parameters, perform validation, or even short-circuit the pipeline by setting a custom response.
OnActionExecuted (After Action Execution)
  • This method runs immediately after the action method has finished.
  • You can inspect or modify the returned action result, perform logging, or handle post-processing tasks.
Real-Time Example to Understand Action Filters in ASP.NET Core Web API

Let us develop a new ASP.NET Core Web API application to demonstrate real-world scenarios using Action Filters. The example will demonstrate how to use IActionFilter, IAsyncActionFilter, and ActionFilterAttribute.

Create a new ASP.NET Core Web API project:

First, create a new ASP.NET Core Web API Project named ActionFilterDemo.

Define the Action Filters

Now, we will create four Action Filters using four different approaches. First, create a folder named Filters in the project root directory where we will create all our Action Filters.

Request and Response Logging (Using IActionFilter interface):

Capturing both incoming request parameters (such as query strings, route data, and headers) and outgoing response details for every API call is crucial for creating comprehensive audit trails. This approach not only supports debugging by allowing you to trace exactly what data was sent and received but also enhances security and compliance by providing a record of all interactions with the system. Using a custom Action Filter, you can automatically log these details for every action method, ensuring consistency and reducing repetitive code across controllers.

So, create a new class file named RequestResponseLoggingFilter.cs within the Filters folder and then copy and paste the following code. The filter logs incoming request details before the action executes, including method, path, query, route params, and headers. Then, it logs the outgoing response status code after the action is complete. This helps create an audit trail for each API call for debugging and monitoring purposes.

using Microsoft.AspNetCore.Mvc.Filters;
using System.Text.Json;

namespace ActionFilterDemo.Filters
{
    // Custom action filter implementing synchronous IActionFilter interface
    public class RequestResponseLoggingFilter : IActionFilter
    {
        // Logger instance to log information, injected via constructor
        private readonly ILogger<RequestResponseLoggingFilter> _logger;

        // Constructor receives ILogger<RequestResponseLoggingFilter> via Dependency Injection
        public RequestResponseLoggingFilter(ILogger<RequestResponseLoggingFilter> logger)
        {
            _logger = logger; // Assign logger instance to private field
        }

        // This method executes just before the action method runs
        public void OnActionExecuting(ActionExecutingContext context)
        {
            // Get HttpContext from the current action context
            var httpContext = context.HttpContext;

            // Get the incoming HTTP request from HttpContext
            var request = httpContext.Request;

            // Serialize the query string parameters to JSON string for logging
            var query = JsonSerializer.Serialize(request.Query);

            // Serialize the route data (like URL parameters) to JSON string for logging
            var routeData = JsonSerializer.Serialize(context.RouteData.Values);

            // Serialize all HTTP request headers to JSON string for logging
            var headers = JsonSerializer.Serialize(request.Headers);

            // Log detailed information about the incoming HTTP request
            // Including HTTP method (GET, POST, etc.), request path, query parameters, route data, and headers
            _logger.LogInformation($"Request Incoming: Method={request.Method}, Path={request.Path}, Query={query}, RouteData={routeData}, Headers={headers}");
        }

        // This method executes immediately after the action method has run
        public void OnActionExecuted(ActionExecutedContext context)
        {
            // Get HttpContext from the current action context
            var httpContext = context.HttpContext;

            // Get the HTTP response that will be sent to the client
            var response = httpContext.Response;

            // Log the HTTP status code of the outgoing response (e.g., 200, 404, 500)
            _logger.LogInformation($"Response Outgoing: StatusCode={response.StatusCode}");
        }
    }
}
When Should We Use IActionFilter in ASP.NET Core Web API?

We need to use the IActionFilter interface when our custom action filter logic is simple and synchronous, involving quick, CPU-bound operations that do not require awaiting asynchronous tasks. This approach is ideal when we want explicit control over pre- and post-action execution behavior without dealing with asynchronous complexity, such as logging, input validation, or modifying action parameters in a straightforward manner.

Input Validation Before Action Execution using IAsyncActionFilter:

Implement complex and business-specific validation rules that go beyond basic model validation, such as checking interdependent fields, verifying external constraints, or enforcing conditional logic before the action method runs. This early validation prevents invalid data from proceeding further into the system, reducing errors and improving data integrity while keeping action methods clean and focused. Implementing this logic in a custom Action Filter allows you to intercept requests and perform complex validations centrally. If any rule fails, the filter can immediately return an error response, preventing invalid data from reaching your business logic and maintaining the integrity of your application.

So, create a new class file named ComplexInputValidationFilter.cs within the Filters folder and then copy and paste the following code. The filter asynchronously intercepts the request before the action executes. It checks if both “StartDate” and “EndDate” parameters exist and are valid DateTime values. If StartDate is later than EndDate, it returns a 400 Bad Request with an error message immediately, skipping the action execution. Otherwise, it continues processing by invoking the next delegate (next()), which calls the action method or next filter.

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

namespace ActionFilterDemo.Filters
{
    // Custom asynchronous action filter implementing IAsyncActionFilter interface
    public class ComplexInputValidationFilter : IAsyncActionFilter
    {
        // This method runs asynchronously before and after the action method execution
        // 'context' gives info about the current request and action arguments
        // 'next' delegate is used to invoke the next action filter or action method itself
        public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
        {
            // Try to get the "StartDate" argument from the action method parameters
            // and assign it to 'startDateObj' if it exists
            // Similarly, try to get "EndDate" argument and assign to 'endDateObj'
            if (context.ActionArguments.TryGetValue("StartDate", out var startDateObj) &&
                context.ActionArguments.TryGetValue("EndDate", out var endDateObj) &&
                // Check if both objects are of type DateTime, and cast them accordingly
                startDateObj is DateTime startDate && endDateObj is DateTime endDate)
            {
                // Validate that StartDate is NOT later than EndDate
                if (startDate > endDate)
                {
                    // If validation fails, set the context.Result to a BadRequestObjectResult
                    // This creates a 400 Bad Request HTTP response with a JSON payload
                    context.Result = new BadRequestObjectResult(new
                    {
                        Status = 400,
                        Message = "StartDate cannot be later than EndDate."
                    });

                    // Short-circuit the pipeline here, preventing the action method from executing
                    return;
                }
            }

            // If validation passes or parameters don't exist, proceed to next filter or action method
            await next();
        }
    }
}
When Should We Use IAsyncActionFilter in ASP.NET Core Web API?

We need to use the IAsyncActionFilter when our action filter needs to perform asynchronous operations, such as calling remote services, database queries, or any I/O-bound tasks, during the execution pipeline. Implementing this interface ensures the filter does not block the request thread while awaiting long-running tasks, improving scalability and responsiveness in high-load or latency-sensitive APIs.

Standardized API Response Formatting:

Enforce a consistent response structure across all API endpoints, ensuring that every client receives responses in a uniform format, typically including status codes, descriptive messages, and data payloads. This standardization simplifies client-side parsing and error handling, improves API usability, and decouples response formatting concerns from individual controllers or actions.

So, create a new class file named APIResponseWrapperFilter.cs within the Filters folder and then copy and paste the following code. After the action completes, this filter wraps any ObjectResult or EmptyResult into a consistent JSON format. The wrapped response always contains Status, Message, and Data fields for uniform client handling. This approach centralizes response formatting and improves API consistency without modifying individual controller actions.

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

namespace ActionFilterDemo.Filters
{
    // Custom action filter inheriting from ActionFilterAttribute for easy attribute usage
    public class APIResponseWrapperFilter : ActionFilterAttribute
    {
        // This method runs after the action method has executed
        public override void OnActionExecuted(ActionExecutedContext context)
        {
            // Check if the action returned an ObjectResult (typical JSON/data response)
            if (context.Result is ObjectResult objectResult)
            {
                // Create a new anonymous object to wrap the original response value
                // Standardizing the response format to include Status, Message, and Data fields
                var wrappedResponse = new
                {
                    Status = objectResult.StatusCode ?? 200, // Use existing status code or default to 200 OK
                    Message = "Success",                      // Fixed success message
                    Data = objectResult.Value                 // Original response data payload
                };

                // Replace the original result with a new JsonResult wrapping the standardized response
                context.Result = new JsonResult(wrappedResponse)
                {
                    StatusCode = objectResult.StatusCode // Preserve the original status code in the new response
                };
            }
            // If the action returned an EmptyResult (no content to send)
            else if (context.Result is EmptyResult)
            {
                // Create a wrapped response indicating no content
                var wrappedResponse = new
                {
                    Status = 204,          // HTTP 204 No Content status
                    Message = "No content",// Informative message for no content
                    Data = null as object  // Null data payload
                };

                // Replace the original EmptyResult with a JsonResult wrapping the standardized no-content response
                context.Result = new JsonResult(wrappedResponse)
                {
                    StatusCode = 204       // Set HTTP status to 204 No Content
                };
            }

            // Call the base method to ensure any additional processing is performed
            base.OnActionExecuted(context);
        }
    }
}
When Should We Use ActionFilterAttribute in ASP.NET Core Web API?

We need to use the ActionFilterAttribute as a convenient base class when we want to implement custom filters with minimal repetitive code and support both synchronous and asynchronous override methods. It is especially helpful when we want to create reusable attribute-based filters that can be easily applied declaratively on controllers or actions, and when we want to use built-in virtual methods for pre- and post-action logic without manually implementing interfaces.

Measuring Execution Time of Specific Endpoints

Understanding how long each API endpoint takes to process requests is crucial for maintaining a high-performance application, particularly for critical operations such as checkout, payment processing, or search features. By using a custom Action Filter to measure and log the execution duration of selected action methods, you can quickly identify slow or resource-intensive endpoints. This data enables optimization, ensures compliance with service-level agreements (SLAs), and helps maintain a responsive user experience and reliable API behavior under varying loads.

So, create a new class file named ExecutionTimeLoggingFilter.cs within the Filters folder and then copy and paste the following code. The filter measures how long an action method takes to execute using a Stopwatch. It starts timing before the action executes and stops timing immediately after. The elapsed time is then logged with the action method’s name. This helps identify slow endpoints and optimize performance.

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

namespace ActionFilterDemo.Filters
{
    // Custom asynchronous action filter inheriting from ActionFilterAttribute
    public class ExecutionTimeLoggingFilter : ActionFilterAttribute
    {
        // Stopwatch instance to measure elapsed time; nullable to allow re-initialization
        private Stopwatch? _stopwatch;

        // Logger instance to log execution time info, injected via constructor
        private readonly ILogger<ExecutionTimeLoggingFilter> _logger;

        // Constructor receives ILogger<ExecutionTimeLoggingFilter> via Dependency Injection
        public ExecutionTimeLoggingFilter(ILogger<ExecutionTimeLoggingFilter> logger)
        {
            _logger = logger; // Assign logger to private field
        }

        // Asynchronous override method that runs before and after the action method execution
        public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
        {
            // Start a new Stopwatch to begin timing the action execution
            _stopwatch = Stopwatch.StartNew();

            // Invoke the next delegate/middleware in the pipeline which runs the action method
            var executedContext = await next();

            // Stop the Stopwatch after action execution completes
            _stopwatch.Stop();

            // Get the display name of the action method being executed (for logging)
            var actionName = context.ActionDescriptor.DisplayName;

            // Log the elapsed time in milliseconds for the action execution
            _logger.LogInformation($"Execution Time for {actionName} : {_stopwatch.ElapsedMilliseconds} ms");
        }
    }
}
API Controller Demonstrating Action Filters

Now, create an API Empty Controller named DemoController within the Controllers folder and then copy and paste the following code. The following controller code is self-explained, so please read the comment lines for a better understanding.

using ActionFilterDemo.Filters;
using Microsoft.AspNetCore.Mvc;

namespace ActionFilterDemo.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class DemoController : ControllerBase
    {
        // This endpoint demonstrates Request and Response Logging
        // The RequestResponseLoggingFilter is applied here using TypeFilter attribute for dependency injection support
        [HttpGet("log-request-response")]                      // HTTP GET method mapped to route api/demo/log-request-response
        [TypeFilter(typeof(RequestResponseLoggingFilter))]    // Attach the custom RequestResponseLoggingFilter to this action
        public IActionResult LogRequestResponseDemo([FromQuery] string sampleParam)  // Accepts a query string parameter 'sampleParam'
        {
            // Return HTTP 200 OK with a simple JSON response confirming logging worked and echoing the query param
            return Ok(new { Message = "Request and response logged successfully.", Param = sampleParam });
        }

        // This endpoint demonstrates Input Validation filter usage
        // ComplexInputValidationFilter applied via TypeFilter attribute for DI
        [HttpGet("validate-input")]                             // HTTP GET method mapped to api/demo/validate-input
        [TypeFilter(typeof(ComplexInputValidationFilter))]     // Attach ComplexInputValidationFilter to validate inputs before action runs
        public IActionResult ValidateInputDemo([FromQuery] DateTime StartDate, [FromQuery] DateTime EndDate) // Accepts StartDate and EndDate query parameters
        {
            // Return 200 OK with a JSON confirming successful validation and echoing the input dates
            return Ok(new { Message = "Input validated successfully.", StartDate, EndDate });
        }

        // This endpoint demonstrates Standardized API Response Wrapping
        // Uses the APIResponseWrapperFilter attribute directly for automatic response formatting
        [HttpGet("standardized-response")]                       // HTTP GET method mapped to api/demo/standardized-response
        [APIResponseWrapperFilter]                               // Apply the custom attribute filter that wraps the response in a standard format
        public IActionResult StandardizedResponseDemo()
        {
            // Sample data object returned from the action method
            var data = new { Value = 123, Description = "Some data" };

            // Return 200 OK with the sample data; response filter will wrap this automatically
            return Ok(data);
        }

        // This endpoint demonstrates Execution Time Logging filter usage
        // ExecutionTimeLoggingFilter applied via TypeFilter attribute to measure execution duration
        [HttpGet("execution-time")]                              // HTTP GET method mapped to api/demo/execution-time
        [TypeFilter(typeof(ExecutionTimeLoggingFilter))]        // Attach the custom execution time logging filter
        public IActionResult ExecutionTimeDemo()
        {
            // Simulate processing delay of 500 milliseconds to mimic some workload
            Thread.Sleep(500);

            // Return 200 OK with a simple confirmation message
            return Ok(new { Message = "Execution time logged." });
        }
    }
}
Modify the Program class:

Next, modify the Program class as follows:

namespace ActionFilterDemo
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // Add services to the container.
            builder.Services.AddControllers()
            .AddJsonOptions(options =>
            {
                // Disable camelCase in JSON output, preserve property names as defined in C# classes
                options.JsonSerializerOptions.PropertyNamingPolicy = null;
            });

            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();
        }
    }
}

Action Filters in ASP.NET Core Web API provide powerful, reusable, and centralized ways to manage cross-cutting concerns at the action level. By implementing IActionFilter or IAsyncActionFilter, we can encapsulate cross-cutting concerns, logging, validation, and response shaping, cleanly and consistently, keeping our action methods focused on business logic.

In the next article, I will discuss Result Filters in ASP.NET Core Web API Applications. In this article, I explain Action Filters in ASP.NET Core Web API Applications. I hope you enjoy this Action Filters in ASP.NET Core Web API article.

Leave a Reply

Your email address will not be published. Required fields are marked *