How to Add a custom action filter in ASP.NET Core
By FoxLearn 3/12/2025 3:41:14 AM 19
By customizing these filters, we can handle various tasks, such as logging, authorization, or modifying the response.
The simplest approach to creating a custom action filter is to inherit from ActionFilterAttribute
and override the appropriate methods based on whether you want to inspect the request, result, or both.
Log User IP Address in Requests
In this example, we'll create an action filter to log the IP address of the user making the request. We'll override OnActionExecuting()
to access the request's IP address and log it before the action method is invoked.
using Microsoft.AspNetCore.Mvc.Filters; public class LogUserIp : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext context) { var ipAddress = context.HttpContext.Connection.RemoteIpAddress?.ToString(); Console.WriteLine($"Request from IP: {ipAddress} routed to {context.Controller.GetType().Name}"); base.OnActionExecuting(context); } }
In this case, the LogUserIp
action filter is capturing the user's IP address from context.HttpContext.Connection.RemoteIpAddress
and logging it to the console.
Applying the Action Filter to Specific Action Methods
You can apply this action filter to a specific action method, or even an entire controller.
[ApiController] [Route("[controller]")] public class UserController : ControllerBase { [HttpGet()] [LogUserIp()] public IActionResult GetUserInfo() { return Ok(); } }
When a request is made to the GetUserInfo()
action, the LogUserIp
action filter logs the user’s IP address to the console:
Request from IP: 192.168.1.1 routed to UserController
Apply the Action Filter Globally
You can also register the action filter globally, making it apply to all action methods across your entire application. This is done in the service registration step of the application initialization:
var builder = WebApplication.CreateBuilder(args); // Register services builder.Services.AddControllers(options => options.Filters.Add(new LogUserIp())); var app = builder.Build(); // Application setup
This setup will ensure that the LogUserIp
action filter logs the user's IP address for every request handled by the application.
How the Framework Creates Action Filter Instances
By default, ASP.NET Core creates a single instance of the action filter per registration, meaning that the same instance is shared across multiple requests. If your action filter maintains state (like logging an ID or user data), this shared instance can cause issues in a multi-threaded environment.
For example, consider this action filter that logs its instance ID:
public class LogUserIp : ActionFilterAttribute { public readonly string Id = Guid.NewGuid().ToString(); public override void OnActionExecuting(ActionExecutingContext context) { var ipAddress = context.HttpContext.Connection.RemoteIpAddress?.ToString(); Console.WriteLine($"Id={Id} Request from IP: {ipAddress} routed to {context.Controller.GetType().Name}"); base.OnActionExecuting(context); } }
If you apply this to multiple action methods, you’ll see the same instance ID for multiple requests:
[HttpGet()] [LogUserIp()] public IActionResult GetUserInfo() { return Ok(); } [HttpPost()] [LogUserIp()] public IActionResult PostUserInfo([FromBody] User user) { return Ok(); }
Multiple requests will show the same Id
:
Id=9f4b3787-0216-4f7b-9a2e-739779d101f0 Request from IP: 192.168.1.1 routed to UserController Id=9f4b3787-0216-4f7b-9a2e-739779d101f0 Request from IP: 192.168.1.2 routed to UserController
This behavior could lead to issues if your action filter stores state in instance variables. To avoid this, it's recommended to use type-activation registration for better thread safety.
Use Type-Activation Registration for Thread Safety and Dependency Injection
Type-activation ensures that a new instance of the action filter is created per request, and it allows for proper dependency injection (DI). This can be done by registering the action filter as a service and using the [ServiceFilter]
attribute to apply it:
var builder = WebApplication.CreateBuilder(args); // Register the action filter as a scoped service builder.Services.AddScoped<LogUserIp>(); var app = builder.Build();
Now, instead of applying the action filter directly, use the [ServiceFilter]
attribute in your controller:
[HttpGet()] [ServiceFilter(typeof(LogUserIp))] public IActionResult GetUserInfo() { return Ok(); }
This ensures that a new instance of LogUserIp
is created for each request, solving the thread-safety issue.
Require a Custom Header in the Request
Sometimes you may want to enforce a specific header for your requests, such as requiring an API key. Here’s an action filter that checks for the presence of a custom header:
public class RequireApiKeyHeader : ActionFilterAttribute { private readonly string RequiredHeader; public RequireApiKeyHeader(string requiredHeader) { RequiredHeader = requiredHeader; } public override void OnActionExecuting(ActionExecutingContext context) { if (!context.HttpContext.Request.Headers.ContainsKey(RequiredHeader)) { context.Result = new ContentResult() { StatusCode = 400, Content = $"Missing required header: {RequiredHeader}" }; } } }
You can apply this action filter to your controller or action method:
[ApiController] [Route("[controller]")] public class UserController : ControllerBase { [HttpGet()] [RequireApiKeyHeader("ApiKey")] public IActionResult GetUserInfo() { return Ok(); } }
When a request is sent without the ApiKey
header, the response will indicate a 400 Bad Request:
Status: 400 - Bad Request Body: Missing required header: ApiKey
Add a Custom Response Header
Another common scenario is adding custom headers to your responses for debugging or tracking purposes. You can use an action filter to add a custom response header like this:
public class AddCustomHeader : ActionFilterAttribute { public override void OnActionExecuted(ActionExecutedContext context) { context.HttpContext.Response.Headers.Add("X-Debug-Info", "Custom Header Value"); base.OnActionExecuted(context); } }
Apply it to your action:
[ApiController] [Route("[controller]")] public class UserController : ControllerBase { [HttpGet()] [AddCustomHeader()] public IActionResult GetUserInfo() { return Ok(); } }
The response headers will include the X-Debug-Info
header:
Content-Length=0 X-Debug-Info=Custom Header Value
Track Action Duration
If you want to track how long an action method takes to execute, you can use a Stopwatch
to measure the time in your action filter:
public class TrackActionDuration : ActionFilterAttribute { public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { var stopwatch = Stopwatch.StartNew(); var actionExecutedContext = await next(); stopwatch.Stop(); actionExecutedContext.HttpContext.Response.Headers.Add("X-Action-Duration", stopwatch.Elapsed.ToString()); } }
This action filter will add a X-Action-Duration
header to the response, showing how long the action method took to execute:
[ApiController] [Route("[controller]")] public class UserController : ControllerBase { [HttpGet()] [TrackActionDuration()] public IActionResult GetUserInfo() { return Ok(); } }
The response will include:
X-Action-Duration=00:00:00.0037
- Global exception event handlers in C#
- How to Add or overwrite a value in ConcurrentDictionary in C#
- Handling Posted Form Data in an API Controller
- How to Get all classes with a custom attribute in C#
- How to Map query results to multiple objects with Dapper in C#
- How to Update appsettings.json in C#
- Injecting ILogger into Dapper Polly Retry Policy in C#
- Properly Disposing HttpContent When Using HttpClient in C#