Conditional Filters in ASP.NET MVC 3

asp.net, asp.net mvc, code 0 comments suggest edit

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.

Tags: aspnetmvc, asp.net, filter, filter providers

Found a typo or error? Suggest an edit! If accepted, your contribution is listed automatically here.

Comments

avatar

24 responses

  1. Avatar for Gene
    Gene April 25th, 2011

    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?

  2. Avatar for Jeff Putz
    Jeff Putz April 25th, 2011

    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!

  3. Avatar for Gene
    Gene April 25th, 2011

    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().

  4. Avatar for Michael Murphy
    Michael Murphy April 25th, 2011

    Thanks for the great Mix11 presentations and thanks for posting the Conditional Filter source!

  5. Avatar for Koistya `Navin
    Koistya `Navin April 25th, 2011

    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

  6. Avatar for haacked
    haacked April 25th, 2011

    @Gene sounds like an error in Glimpse. You should log a bug with them. :)

  7. Avatar for Jon
    Jon April 25th, 2011

    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)]

  8. Avatar for Anthony van der Hoorn
    Anthony van der Hoorn April 26th, 2011

    @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.

  9. Avatar for Nik
    Nik April 26th, 2011

    @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.

  10. Avatar for Dmitry Antonenko
    Dmitry Antonenko April 26th, 2011

    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?

  11. Avatar for csokun
    csokun April 26th, 2011

    That reminded me how nice [SkipFilter()] from MonoRail ;)

  12. Avatar for haacked
    haacked April 27th, 2011

    @csokun yeah, we should consider that. :)

  13. Avatar for Kazi Manzur Rashid
    Kazi Manzur Rashid April 28th, 2011

    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

  14. Avatar for Craig
    Craig April 28th, 2011

    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?

  15. Avatar for haacked
    haacked April 28th, 2011

    @Craig, you mean to not run on POST actions? You could look at the ControllerContext.HttpContext.Request.Method property.

  16. Avatar for Garry
    Garry May 6th, 2011

    @Gene Your code worked like a champ!

  17. Avatar for Steve Potter
    Steve Potter May 16th, 2011

    Check this out: stackoverflow.com/.... Super simple and does the same thing.

  18. Avatar for AbdouMoumen
    AbdouMoumen May 20th, 2011

    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;
    }

  19. Avatar for Terrance Smith
    Terrance Smith May 30th, 2011

    <super>
    Say you want to apply an action filter to very action except one.
    </super>
    I'm thinking you meant every there. You kick ass. That is all continue on with your lives. lol

  20. Avatar for haacked
    haacked May 31st, 2011

    @Terrance Thanks!

  21. Avatar for Trey
    Trey November 22nd, 2011

    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

  22. Avatar for haacked
    haacked November 22nd, 2011

    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.

  23. Avatar for Diego
    Diego March 18th, 2012

    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);
    }

  24. Avatar for osman rahimi
    osman rahimi November 4th, 2014

    hi . thanks sir