Photo by Markus Spiske on Unsplash
Creating Overridable Hierarchical Attribute Filters for ASP.net 6
Control which instance of the filter executes
You have worked with them and probably haven't given them a second thought: Attribute filters that can be placed at the controller level as well as the action level, and it is expected that the action-level one "overrides" whatever setting/functionality is set at the controller level.
Recently I answered a question in Stack Overflow about how to implement this behavior. The main problem the asker had was that he/she believed that just because the attribute filter was the same, ASP.net would automagically run just the innermost one, merely because it was repeated. This is not how it works.
First of all, let's remind ourselves about filters in ASP.net by reading this Microsoft resource.
In a nutshell, there are three important pieces of information that must stand out in the context of this article:
- Authorization, resource, action, exception and result filters run after the routing mechanism has selected the action to run. The technique demonstrated in this article can be used for any of these filter types.
- The different types of filters have a specific order of execution.
- Authorization and exception filters are run only once. The other filter types execute twice.
So let's get started.
The Goal
Create a filter attribute that, when placed at the action level, overrides itself at the controller level, if present.
In a code example:
[ApiController]
[Route("[controller]")]
[MyAuth("At Controller")]
public class SomeController : ControllerBase
{
private readonly ILogger<OrderController> _logger;
public OrderController(ILogger<OrderController> logger)
{
_logger = logger;
}
[HttpPost]
[MyAuth("At Action")]
public Task<IActionResult> AddOrder(Order order)
{
return Task.FromResult<IActionResult>(Ok());
}
}
The above example shows the MyAuthAttribute
attribute specified in two places: At the controller level, and at the action level, in the AddOrder
action specifically. The idea here is to prevent the controller-level MyAuth
attribute to execute whenever the AddOrder
action is called because we only want the filter attribute version that was applied at the action level to execute.
There are probably several ways to accomplish this, but the most elegant way (which is the object of this write-up) is probably with the use of the FilterContext.FindEffectivePolicy
method.
FilterContext.FindEffectivePolicy
At the time of writing, the only information in the provided link says only this:
Returns the most effective (most specific) policy of type
TMetadata
applied to the action associated with the FilterContext.
Yes, indeed Microsoft documentation has gone down the drain in the recent years. But I digress. Back to topic.
This means that for given a type TMetadata
, the method will search instances of said type through the controller hierarchy, and will return the instance that is most specific, being the most specific position the action position.
In light of this and accounting for our goal, we now have a way to determine exactly which version of the attribute is supposed to run.
The Solution
As an (incomplete) example, I will start creating a custom authorization attribute. We want only the most specific version of our authorization attribute to be enforced, and we'll use the base class TypeFilterAttribute
so our implementation can obtain services from the IoC container if needed.
using Microsoft.AspNetCore.Mvc;
namespace my.app;
// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] <-- This already comes with TypeFilterAttribute.
public class MyAuthAttribute : TypeFilterAttribute
{
public MyAuthAttribute(string name)
: base(typeof(MyAuth))
{
Arguments = new object[] { name };
}
}
Hopefully the above is not weird. It is just how to use TypeFilterAttribute
. Our actual authorization code will reside inside the MyAuth
class. In this example, we have added a Name
property so as to be able to track what's happening in the debug console.
using Microsoft.AspNetCore.Mvc.Filters;
namespace my.app;
public class MyAuth : IAsyncAuthorizationFilter
{
#region Properties
public string Name { get; }
#endregion
#region Constructors
public MyAuth(string name)
{
Name = name;
}
#endregion
#region IAsyncAuthorizationFilter
public Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
System.Diagnostics.Debug.Print($"My name: {Name}");
var effectiveAtt = context.FindEffectivePolicy<MyAuth>();
System.Diagnostics.Debug.Print($"Effective filter's name: {effectiveAtt?.Name}");
System.Diagnostics.Debug.Print($"Am I the effective attribute? {this == effectiveAtt}");
if (this == effectiveAtt)
{
// Do stuff since this is the effective attribute (policy).
}
else
{
// ELSE part probably not needed. We just want the IF to make sure the code runs only once.
}
return Task.CompletedTask;
}
#endregion
}
This is it. One call to FindEffectivePolicy
plus a simple if
statement is all you need to implement a filter attribute that can override itself whenever it is applied at the controller and action levels simultaneously.
If you use the above code plus the example code in the Goal section, you'll see the following output in the debug console whenever you attempt to call the AddOrder
action:
My name: At Controller
Effective filter's name: At Action
Am I the effective attribute? False
My name: At Action
Effective filter's name: At Action
Am I the effective attribute? True
Caveat with Filters that Run Twice
As stated by Microsoft, some filter types run before and after certain step. If you implement the asynchronous version of the method (which I think you should), then you're probably OK. However, if you implement the synchronous versions, you'll have the before and after methods, and you'll have to do the check in both.
Feel free to drop any comments, and happy coding.