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!

security

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.

What others have said

Requesting Gravatar... Gene Apr 25, 2011 4:11 AM
# re: Conditional Filters in ASP.NET MVC 3
So, following up:

"For example, suppose you want to apply an authorization filter to every action except the action that lets the user login."

How would you pass in an existing action filter?
Requesting Gravatar... Jeff Putz Apr 25, 2011 4:14 AM
# re: Conditional Filters in ASP.NET MVC 3
That's pretty tight. In POP Forums, I was actually checking the namespace of the controller to decide whether or not to apply a user setting filter. Not as clean, in retrospect. Good stuff!
Requesting Gravatar... Gene Apr 25, 2011 4:41 AM
# re: Conditional Filters in ASP.NET MVC 3
I ask, as, when I remove [Authorize] attribute from controllers, and add it in via a conditionalfilterprovider:

= new Func<ControllerContext, ActionDescriptor, object>[] {
(c, a) => c.Controller.GetType() != typeof(HomeController)
? new AuthorizeAttribute()
: null };

With Glimpse installed, if I attempt to navigate to a page that would require the authorize attribute, instead of being redirected to a login page, it throws an error:

System.NullReferenceException: Object reference not set to an instance of an object.
at Glimpse.Net.Plumbing.GlimpseFilterCallMetadata..ctor(String category, Guid guid, String method, Filter innerFilter, Boolean isChild)

Set Glimpse to on="false" and the login redirect works as expected. I wasn't certain if this is a Glimpse problem or if I should be passing in something instead of / in addition to a new AuthorizeAttribute().
Requesting Gravatar... Michael Murphy Apr 25, 2011 6:33 AM
# re: Conditional Filters in ASP.NET MVC 3
Thanks for the great Mix11 presentations and thanks for posting the Conditional Filter source!
Requesting Gravatar... Koistya `Navin Apr 25, 2011 7:18 AM
# re: Conditional Filters in ASP.NET MVC 3
Hey Phil, I have an idea for one your feature blog posts - Binding Aliases! Which can be used somewhat like that:

public ActionResult(..., [Alias("return")] returnUrl) { ... }

http://example.com/?return=/hello_world
Requesting Gravatar... haacked Apr 25, 2011 8:14 AM
# re: Conditional Filters in ASP.NET MVC 3
@Gene sounds like an error in Glimpse. You should log a bug with them. :)
Requesting Gravatar... Jon Apr 25, 2011 8:44 PM
# re: Conditional Filters in ASP.NET MVC 3
I like this approach Phil, thanks for the heads up. Previously I have just added the filter onto the base controller and added a property that "disabled" it for that request. So all my actions do not need to explicitly be marked as [Authorize] however on my login action I just need to decorate it with [Authorize(ByPass=true)]
Requesting Gravatar... Anthony van der Hoorn Apr 26, 2011 4:32 AM
# re: Conditional Filters in ASP.NET MVC 3
@Gene Thanks for pointing out the issue. If you can go to https://github.com/Glimpse/Glimpse and log the issue we can continue the conversation there.

Just know that we are working as hard as we can to get all cases covered, but at the end of the day we are just two guys working in our spare time. Keep you posted once this is fixed, should be very soon.
Requesting Gravatar... Nik Apr 26, 2011 10:55 AM
# re: Conditional Filters in ASP.NET MVC 3
@Gene,

I just committed a fix for this Glimpse bug to our gitHub repository. This problem should be fixed in the next version of Glimpse (0.80), expected to release in the next day or two.

Thank you for your feedback and please let us know if you run into any other issues.
Requesting Gravatar... Dmitry Antonenko Apr 26, 2011 9:30 PM
# re: Conditional Filters in ASP.NET MVC 3
Hi Phil,

I have implemented filter provider like you. Project link fluentfilters.codeplex.com and more detailed on CodeProject. What are you think about my approach?
Requesting Gravatar... csokun Apr 27, 2011 12:45 AM
# re: Conditional Filters in ASP.NET MVC 3
That reminded me how nice [SkipFilter()] from MonoRail ;)
Requesting Gravatar... haacked Apr 27, 2011 8:42 AM
# re: Conditional Filters in ASP.NET MVC 3
@csokun yeah, we should consider that. :)
Requesting Gravatar... Kazi Manzur Rashid Apr 28, 2011 4:42 AM
# re: Conditional Filters in ASP.NET MVC 3
Sorry man, too difficult to digest the code you have shown for the conditional filters, probably the framework should include a nice wrapper for filter registration. Comparing to rails though I am not an expert:

before_filter: do_this, :only => :index
or
before_filter: do_that, :except=> :show
Requesting Gravatar... Craig Apr 28, 2011 8:56 AM
# re: Conditional Filters in ASP.NET MVC 3
Thanks heaps for the code above, it's helped me add simple breadcrumb functionality to every function. Is there any way the filters can be used to exclude HttpPost actions?
Requesting Gravatar... haacked Apr 28, 2011 9:28 AM
# re: Conditional Filters in ASP.NET MVC 3
@Craig, you mean to not run on POST actions? You could look at the ControllerContext.HttpContext.Request.Method property.
Requesting Gravatar... Garry May 06, 2011 5:44 AM
# re: Conditional Filters in ASP.NET MVC 3
@Gene Your code worked like a champ!
Requesting Gravatar... Steve Potter May 16, 2011 10:16 PM
# re: Conditional Filters in ASP.NET MVC 3
Check this out: stackoverflow.com/.... Super simple and does the same thing.
Requesting Gravatar... AbdouMoumen May 21, 2011 12:58 AM
# re: Conditional Filters in ASP.NET MVC 3
Great post. I think it would be a little bit prettier to add a constructor with params
public ConditionalFilterProvider(params Func<ControllerContext, ActionDescriptor, object>[] conditions)
{
_conditions = conditions;
}
Requesting Gravatar... Terrance Smith May 30, 2011 10:54 PM
# re: Conditional Filters in ASP.NET MVC 3

Say you want to apply an action filter to very action except one.


I'm thinking you meant every there. You kick ass. That is all continue on with your lives. lol

Requesting Gravatar... haacked May 31, 2011 4:37 AM
# re: Conditional Filters in ASP.NET MVC 3
@Terrance Thanks!
Requesting Gravatar... Trey Nov 22, 2011 4:37 AM
# re: Conditional Filters in ASP.NET MVC 3
Phil,
Thank you for the great article. I have a similar situation and I am trying to avoid overkill.

To sum it up... I have 2 types of users: "Visitors" and "Users". Users have a persistent account. Visitors do not; instead they have the option to login via an OpenID provider or legacy-style with an email address and friendly name.

I am trying to find a very simple way to accomodate multiple logon URLs. Right now, web.config designates the log on url, and thus all failures with [Authorize] attributes redirect there.

Blog Controller:
* Mostly anonymous access
* Comment actions require "visitor" authentication
* [Authorize] failures should redirect to Account/Logon

BlogAdmin Controller:
* All actions require full "user" authentication
* [Authorize] failures should redurect to Account/Admin

Any suggestions on how to do this as simple as possible? My first thought was extending the AuthorizeAttribute class, but thought you might know a better way.

Thanks.
-T


Requesting Gravatar... haacked Nov 22, 2011 1:17 PM
# re: Conditional Filters in ASP.NET MVC 3
The AuthorizeAttribute is not the thing that causes the forms login redirect. It's the FormsAuthenticationModule. The authorize attribute just sets a status of being unauthorized.

You could replace the FormsAuthenticationModule with a custome one. Or perhaps even easire, just use the same login page, but have it change its behavior depending on the status of the current user.
Requesting Gravatar... Diego Mar 18, 2012 10:34 PM
# re: Conditional Filters in ASP.NET MVC 3
Great article! I used this to automatically attach a special Ajax error handling filter for all actions tagged with [AjaxOnly] ...


if (pActionDescriptor.GetCustomAttributes(_ajaxAttributeType, true).Length > 0)
{
yield return new Filter(new JsonErrorHandlerAttribute(), FilterScope.Global, null);
}

What do you have to say?

(will show your gravatar)
Please add 7 and 3 and type the answer here: