dateo. Coding Blog

Coding, Tech and Developers Blog

asp.net
architecture
rest

Action filters in ASP.NET - A quick guide

Dennis Frühauff on November 7th, 2022

Following up on our article on how to design good controllers in ASP.NET, I'd like to introduce you to a different set of tools to help you clean up your architecture in ASP.NET Core projects. Let's have a look at custom filters and how you can use them to your benefits.


ASP.NET Core is a framework providing a plethora of possibilities that can help you structure your API projects and solve the problems you might face with out-of-the-box tooling.
Among those are action filters that you can apply to your controllers or endpoints to change their behavior to your liking.


So let's have a look at those.


Filters and middleware

Many of you already have worked with middleware approaches in .NET Core. In general, can be thought of as a filter layer as well with the following properties:


  • Middleware is a layer that sits in between an incoming HTTP request and your actual endpoint, i.e., it can filter incoming data.
  • If you specify your application to use middleware, by default, all requests will go through it. You can make exceptions from this rule (e.g., think of specific endpoints that don't require authorization), but this is the default.

Filters on the other hand are an application layer that can be applied both on incoming data as well as outgoing data (or even both at the same time), i.e., you can have a filter that works on incoming requests, or one that checks response data before they are returned. And, most importantly, you can easily specify only a single endpoint to use as a custom filter.


So, where exactly do they sit in the lifecycle of a simple HTTP request to your application?
We could have a look into the official documentation but, just for fun, let's figure this out by ourselves!


To do this, let's populate an ASP.NET Core API with a set of filters and a custom middleware:


Middleware


public class CustomMiddleware
{
    private static readonly ILogger Log = LogManager.CreateLogger<CustomMiddleware>();

    private readonly RequestDelegate next;

    public CustomMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public Task Invoke(HttpContext httpContext)
    {
        Log.LogInformation("Custom middleware called");
        return next(httpContext);
    }
}

Filters


public class CustomActionFilter : IAsyncActionFilter
{
    private static readonly ILogger Log = LogManager.CreateLogger<CustomActionFilter>();
    
    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        Log.LogInformation("CustomActionFilter before next called");
        
        await next();
        
        Log.LogInformation("CustomActionFilter after next called");
    }
}

public class CustomResultFilter : IAsyncResultFilter
{
    private static readonly ILogger Log = LogManager.CreateLogger<CustomActionFilter>();
    
    public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
    {
        Log.LogInformation("CustomResultFilter before next called");
        
        await next();
        
        Log.LogInformation("CustomResultFilter after next called");
    }
}

Program.cs


...
  builder.Services.AddScoped<CustomResultFilter>();
  builder.Services.AddScoped<CustomActionFilter>();
...
  app.UseMiddleware<CustomMiddleware>();
...

and the actual endpoint:


[HttpPost]
[ServiceFilter(typeof(CustomActionFilter))]
[ServiceFilter(typeof(CustomResultFilter))]
public Task SaveAsync([FromBody] SaveTweetDto saveTweet)
{
    Log.LogInformation("Post tweet called");
    return Task.CompletedTask;
}

As you may have noticed, I populated the implementation with nothing but log messages. If we now run this code and make an actual request against this endpoint, we get the following output in the debug console:
From this, we can infer, that the custom middleware is triggered right before everything else. Afterward, object validation kicks in (I added a log message inside that DTO as well). Then, the action filter is called. After the actual controller method is executed, we return to the action filter and end up in the execution of the result filter. A fun experiment, but let's visualize this:


Screenshot 1


For matters of completeness, I also added the authorization middleware and an exception filter.
Filter types all derive from Microsoft.AspNetCore.Mvc.Filters.IFilterMetadata and you can have as many and as few as you like using the ServiceFilter attribute (you can apply this attribute both on the class (controller) and method (endpoint) level.
There are several more specific interfaces that you can choose from to design a custom filter:


  • IAsyncActionFilter
  • IAsyncExceptionFilter
  • IAsyncResourceFilter
  • IAsyncResultFilter
  • ...and some more.

By the way, this is a perfect example of the implementation of a pipes and filters architectural pattern. You can interchange the order of all your filters via the applied attributes on your methods and adjust the behavior to your liking.


Why are filters useful?

I have spent a great deal on emphasizing here why clean controllers are a good thing to have in your applications. Filters can be a great way of achieving good results on that matter. You can implement filters, that will perform special validation tasks for incoming requests (something that your controller will not have to do then). You can implement filters to handle specific kinds of exceptions and turn them into meaningful messages for your consumers. And you can have filters that validate the results of your endpoints in a certain way if that is what you need.


All of this can help you design your controllers with SoC in mind.


Got it. But can filters be made configurable?

Great question and the answer is: Yes, and it is surprisingly simple. But let us distinguish two different scenarios here.


Global configuration via dependency injection

Say you have a custom filter that you want to configure globally, e.g., you want to be able to enable or disable it via a configuration object from your appsettings.json.


Since the filter is already registered in your service collection, dependency injecting a configuration object will pretty much work right away:


Program.cs


  builder.Services.AddOptions<CustomFilterConfiguration>();

Filter


public class CustomActionFilter : IAsyncActionFilter
{
    private readonly CustomFilterConfiguration configuration;

    public CustomActionFilter(IOptions<CustomFilterConfiguration> configuration)
    {
        this.configuration = configuration.Value;
    }
    
    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        if (this.configuration.IsFilterEnabled)
        {
            // ...
        }
        
        await next();
    }
}

Note that you can (technically) inject anything into your filter. Services, repositories, logger instances - your call.


Configuration via custom attributes

Sometimes, though, the configuration for your filter might be endpoint specific. Let's imagine an endpoint of the following form where users might retrieve several tweets from our social network:


[HttpGet]
[Route("{numberOfItems:int}")]
public ActionResult<IEnumerable<Tweet>> GetAsync(int numberOfItems)
{
    Log.LogInformation("Get tweets called");
    ...
}

And now, let's also assume, that we want to limit the number of items that can be retrieved from this endpoint (let's also forget about other ways to do that in ASP.NET - consider it an exercise). So, say we want to implement a filter that validates the requested number of items via a configurable parameter. To achieve this, we need a custom attribute:


public class LimitItemsFilterAttribute : ServiceFilterAttribute
{
    public LimitItemsFilterAttribute(int maximumNumberOfItems) : base(typeof(LimitItemsFilter))
    {
        MaximumNumberOfItems = maximumNumberOfItems;
    }

    public int MaximumNumberOfItems { get; }
}

Then, we can specify the parameter on the endpoint, exactly where it is needed:


[HttpGet]
[Route("{numberOfItems:int}")]
[LimitItemsFilter(10)]
public ActionResult<IEnumerable<Tweet>> GetAsync(int numberOfItems)
{
  ...
}

In the actual implementation of the filter, we need to retrieve the filter attribute from the action's execution context as well as the action's arguments. Afterward, we can do as we please:
With this implementation, we can specify a different parameter for every endpoint where this is needed. In such a way, we can compose a highly adaptive API with a clear separation of concerns.
TLDR: Filter can be a great addition to your application design. ASP.NET Core already provides a ton of functionality here, so feel free to experiment!



Please share on social media, stay in touch via the contact form, and subscribe to our post newsletter!

Be the first to know when a new post was released

We don’t spam!
Read our Privacy Policy for more info.

We use cookies on our website to give you the most relevant experience by remembering your preferences and repeat visits. By clicking “Accept All”, you consent to the use of ALL the cookies. However, you may visit "Cookie Settings" to provide a controlled consent.