Conditional Filters in ASP.NET MVC 3
Say you want to apply an action filter to every action except one. How would you go about it? For example, suppose you want to apply an authorization filter to every action except the action that lets the user login. Seems like a pretty good idea, right?
Currently, it takes a bit of work to do this. If you add a filter to the
GlobalFilters.Filters
collection, it applies to every action, which in
the previous scenario would mean you already need to be authorized to
login. Now that is security you can trust!
You can also manually add the filter attribute to every controller and/or action method except one. This solution is a potential bug magnet since you would you need to remember to apply this attribute every time you add a new controller. Update: There’s yet another approach you can try which is to write a custom authorize attribute as described in this blog post on Securng your ASP.NET MVC 3 Application.
Fortunately, ASP.NET MVC 3 introduced a new feature called filter providers which allow you to write a class that will be used as a source of action filters. For more details about what filter providers are, I highly recommend reading Brad Wilson’s blog post on filters.
In this case, what I need to write is a conditional action filter. I actually started writing one during my ASP.NET MVC 3 presentation at this past Mix 11 but never actually finished the demo. One of the many mistakes that inspired my blog post on presentation tips.
In this blog post, I’ll finish what I started and walk through an implementation of a conditional filter provider which will let us accomplish applying filters to action methods based on any criteria we can think of.
Here’s the approach I took. First, I wrote a custom filter provider.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
public class ConditionalFilterProvider : IFilterProvider {
private readonly
IEnumerable<Func<ControllerContext, ActionDescriptor, object>> _conditions;
public ConditionalFilterProvider(
IEnumerable<Func<ControllerContext, ActionDescriptor, object>> conditions)
{
_conditions = conditions;
}
public IEnumerable<Filter> GetFilters(
ControllerContext controllerContext,
ActionDescriptor actionDescriptor) {
return from condition in _conditions
select condition(controllerContext, actionDescriptor) into filter
where filter != null
select new Filter(filter, FilterScope.Global, null);
}
}
The code here is fairly straightforward despite all the angle brackets.
We implement the IFilterProvider
interface, but only return the
filters given that meet the set of criterias represented as a Func
.
But each Func gets passed two pieces of information, the current
ControllerContext
and an ActionDescriptor
. Through the
ActionDescriptor
, we can get access to the ControllerDescriptor
.
The ActionDescriptor and ControllerDescriptor are abstractions of actions and controllers that don’t assume that the controller is a type and the action is a method. That’s why they were implemented in that way.
So now, to use this provider, I simply need to instantiate it and add it to the global filter provider collection (or register it via my Dependency Injection container like Brad described in his blog post).
Here’s an example of creating a conditional filter provider with two
conditions. The first adds an instance of MyFilter
to every controller
except HomeController
. The second adds SomeFilter
to any action that
starts with “About”. These scenarios are a bit contrived, but I bet you
can think of a lot more interesting and powerful uses for this.
IEnumerable<Func<ControllerContext, ActionDescriptor, object>> conditions =
new Func<ControllerContext, ActionDescriptor, object>[] {
(c, a) => c.Controller.GetType() != typeof(HomeController) ?
new MyFilter() : null,
(c, a) => a.ActionName.StartsWith("About") ? new SomeFilter() : null
};
var provider = new ConditionalFilterProvider(conditions);
FilterProviders.Providers.Add(provider);
Once we create the filter provider, we add it to the filter provider collection. Again, you can also do this via dependency injection instead of adding it to this static collection.
I’ve posted the conditional filter provider as a package in my personal
NuGet
repository
I use for my own little samples located at
http://nuget.haacked.com/nuget/. Feel
free to add that URL as a package source and
Install-Package ConditionalFilterProvider
in order to get the source.
Comments
24 responses