Coding, Tech and Developers Blog
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.
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:
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:
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
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.
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.
Great question and the answer is: Yes, and it is surprisingly simple. But let us distinguish two different scenarios here.
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.
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!
Be the first to know when a new post was released
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.