Control the execution order of your filters in ASP.NET Web API

Β· 1356 words Β· 7 minutes to read

One of the few minor annoyances in ASP.NET Web API is that you lack the ability to control the order in which the attributes/filters are executed. If you have several of those applied to one action, they will not run in the order they have been declared in the code, and that sometimes may cause a lot of problems.

In fact, as a member of the ASP.NET Web API Advisory Group I already brought that up during our meetings, not to mention it has already been submitted as an issue to the ASP.NET team on Codeplex. In the meantime, let’s tackle this problem and see how you can easily regain control over the execution order of the attributes.

The problem πŸ”—

First, let’s look at the problem. Let’s declare three ActionFilterAttributes and apply them to a ApiController action.

To be able to see in which order they are executed, I will write to the Response object in each one of them. This is obviously totally against pretty much everything ASP.NET Web API stands for, but it will allow us to monitor the execution order very easily - and that’s the only thing we are interested in.

public class CustomAttribute : ActionFilterAttribute  
{  
public override void OnActionExecuting(HttpActionContext actionContext)  
{  
HttpContext.Current.Response.Write(" filter1 ");  
}

public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)  
{  
HttpContext.Current.Response.Write(" filter1 ");  
}  
}

public class CustomAttribute2 : ActionFilterAttribute  
{  
public override void OnActionExecuting(HttpActionContext actionContext)  
{  
HttpContext.Current.Response.Write(" filter2 ");  
}

public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)  
{  
HttpContext.Current.Response.Write(" filter2 ");  
}  
}

public class CustomAttribute3 : ActionFilterAttribute  
{  
public override void OnActionExecuting(HttpActionContext actionContext)  
{  
HttpContext.Current.Response.Write(" filter3 ");  
}

public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)  
{  
HttpContext.Current.Response.Write(" filter3 ");  
}  
}  

So we have three attributes, each one of which will write an identifying string to the Response. Note that due to the nature how ActionFilterAttributes work, the first one to hit OnActionExecuting will be the last to hit OnActionExecuted.

Let’s decorate a Web API action with those three attributes:

[CustomAttribute]  
[CustomAttribute2]  
[CustomAttribute3]  
public IEnumerable<string> Get()  
{  
return new string[] { "value1", "value2&#8243; };  
}  

This, one would expect, should yield the following result:

filter1 filter2 filter3 filter3 filter2 filter1  

Right? Well, not really.

Solution πŸ”—

The solution to the problem is to introduce a parameter which determines the attribute order, and then implement a custom IFilterProvider which will respect that.

The easiest way to go about it would be to extend the System.Web.Http.Filters.FilterInfo class, because that’s the one that IFilterProvider spits out. Unfortunately, that class is sealed so there is not much we can do with it. Plan B then, is to implement a mirror class, CustomFilterInfo, order the attributes there, and then convert the IEnumerable of CustomFilterInfo to IEnumerable of FilterInfo in the CustomFilterProvider.

Let’s go about it.

To be able to sort, we need an interface first.

public interface IBaseAttribute : IFilter  
{  
int Position { get; set; }  
}  

Position will bne the property that determines the sort order. Now we need a class which will accept the Position parameter in the constructor, so that we can decorate our attributes accordingly.

public class BaseActionFilterAttribute : ActionFilterAttribute, IBaseAttribute  
{  
public int Position { get; set; }

public BaseActionFilterAttribute()  
{  
this.Position = 0;  
}  
public BaseActionFilterAttribute(int positon)  
{  
this.Position = positon;  
}  
}  

This class, inherits from ActionFilterAttribute and implements BaseAttribute. If the attribute is created without explicitly set position, it would get position = 0 (first in the queue to execute), otherwise position will be set by the developer.

Note, that in this case we are creating a base inheriting from ActionFilterAttribute. You could/should create the same base class for AuthorizationAttribute and ExceptionFilterAttribute if you plan on sorting these as well, but since it’s duplicate code I will skip them for now.

CustomFilterInfo πŸ”—

Now, we need a class that will be a mirror to the aforementioned, sealed FilterInfo. Instances of this class will also be sortable, so it will implement IComparable.

Let me reiterate the plan - we will retrieve all the relevant filters for a given action, store them in a collection of CustomFilterInfo, sort there, and convert o a collection of FilterInfo, maintaining the order.

public class CustomFilterInfo : IComparable  
{  
public CustomFilterInfo(IFilter instance, FilterScope scope)  
{  
this.Instance = instance;  
this.Scope = scope;  
}

public IFilter Instance { get; set; }  
public FilterScope Scope { get; set; }

public int CompareTo(object obj)  
{  
if (obj is CustomFilterInfo)  
{  
var item = obj as CustomFilterInfo;

if (item.Instance is IBaseAttribute)  
{  
var attr = item.Instance as IBaseAttribute;  
return (this.Instance as IBaseAttribute).Position.CompareTo(attr.Position);  
}  
else  
{  
throw new ArgumentException("Object is of wrong type");  
}  
}  
else  
{  
throw new ArgumentException("Object is of wrong type");  
}  
}

public FilterInfo ConvertToFilterInfo()  
{  
return new FilterInfo(this.Instance, this.Scope);  
}  
}  

This object has two properties (just like FilterInfo) - Instance - which holds the actual Filter and Scope, which determines whether the filter is scoped globally, on controller level or on action level.

The CompareTo method will be used for sorting and first checks whether the compared item is CustomFilterInfo, then if the Instance property contains an object implementing IBaseAttribute (because if it does, it will contain our Position property).

Finally we include a simple method to convert from FilterInfo to CustomFilterInfo.

FilterProvider πŸ”—

Now that we have all our help classes in turn, let’s focus on the heart of it, CustomFilterProvider.

Surprisingly, it is really simple:

public class CustomFilterProvider : IFilterProvider  
{  
public IEnumerable<FilterInfo> GetFilters(HttpConfiguration configuration, HttpActionDescriptor actionDescriptor)  
{  
if (configuration == null)  
{  
throw new ArgumentNullException("Configuration is null");  
}

if (actionDescriptor == null)  
{  
throw new ArgumentNullException("ActionDescriptor is null");  
}

IEnumerable<CustomFilterInfo> customActionFilters = actionDescriptor.GetFilters().Select(i => new CustomFilterInfo(i, FilterScope.Action));  
IEnumerable<CustomFilterInfo> customControllerFilters = actionDescriptor.ControllerDescriptor.GetFilters().Select(i => new CustomFilterInfo(i, FilterScope.Controller));

return customControllerFilters.Concat(customActionFilters).OrderBy(i => i).Select(i => i.ConvertToFilterInfo());  
}  
}  

What we do here, is we read all the controller-scoped filters, and create an enumerable of CustomFilterInfo. Then we do the same for the action-scoped filters.

Finally, we merge the two enumerables, order them (very easy, since we implemented IComparable) and convert back to FilterInfo).

Notice we set the scope of all filters to “Controller”. The reason is we want to be able to apply sorting not only to action filters but also to controller ones. So that if action filter has a higher “Position” than controller filter, it will be executed first. But, if you don’t like that you can keep the scopes intact.

Plugging it in πŸ”—

The final thing to do is to plug our custom provider into the pipeline, and remove the default one. That’s done in the Global.asax.

GlobalConfiguration.Configuration.Services.Add(typeof(System.Web.Http.Filters.IFilterProvider), new CustomFilterProvider());  
var providers = GlobalConfiguration.Configuration.Services.GetFilterProviders();  
var defaultprovider = providers.First(i => i is ActionDescriptorFilterProvider);  
GlobalConfiguration.Configuration.Services.Remove(typeof(System.Web.Http.Filters.IFilterProvider), defaultprovider);  

First we add our custom one, then we find the default ActionDescriptorFilterProvider and get rid of it.

Testing the functionality πŸ”—

We can now test the code from the beginning again. Let’s modify the attributes to match our current situation:

public class CustomAttribute : BaseActionFilterAttribute {}  
public class CustomAttribute2 : BaseActionFilterAttribute {}  
public class CustomAttribute3 : BaseActionFilterAttribute {}  

We need to inherit now not directly from ActionFilterAttribute, but from BaseActionFilterAttribute. That’s not going to change anything - except introduce the Position property.

[CustomAttribute(Position=1)]  
[CustomAttribute2(Position=2)]  
[CustomAttribute3(Position=3)]  
public IEnumerable<string> Get()  
{  
return new string[] { "value1", "value2" };  
}  

Now, this is the same code as before, but it will now execute in the proper order.

If you put [CustomAttribute2(Position=2)] on the controller instead, the order remains the same! That is because controller-scoped attributes and action-scoped attributes are being sorted together.

Note, these were all ActionFilterAttributes. As mentioned, you could make the same base class for Authorization and Exception attributes (these are part of the source code that is attached to this post).

Remember, not setting a position at all, will cause the element to be treated as if it had Position=0, so will be executed first.

Summary and source πŸ”—

Hopefully this will be useful in your projects - I am sure that ultimately, there will come a time when you need an order in the attributes, and this solution should provide you with an easy out - as long as your filters would inherit from the base classes as shown here.

With that said - see you next time!

About


Hi! I'm Filip W., a software architect from ZΓΌrich πŸ‡¨πŸ‡­. I like Toronto Maple Leafs πŸ‡¨πŸ‡¦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁒󠁳󠁣󠁴󠁿.

You can find me on Github, on Mastodon and on Bluesky.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP