30

I've run into several cases in ASP.NET MVC where I wanted to apply an action filter on every action except one or two. For example, say you have an AccountController. Every action in it requires the user be logged in, so you add [Authorize] at the controller level. But say you want to include the login page in AccountController. The problem is, users sent to the login page aren't authorized, so this would result in an infinite loop.

The obvious fix (other than moving the Login action to another controller) is to move the [Authorize] from the controller to all action methods except Login. Well that ain't fun, especially when you have a lot of methods or forget to add [Authorize] to a new method.

Rails makes this easy with an ability to exclude filters. ASP.NET MVC doesn't let you. So I decided to make it possible and it was easier than I thought.

    /// <summary>
/// This will disable any filters of the given type from being applied.  This is useful when, say, all but on action need the Authorize filter.
/// </summary>
[AttributeUsage(AttributeTargets.Method|AttributeTargets.Class, AllowMultiple=true)]
public class ExcludeFilterAttribute : ActionFilterAttribute
{

    public ExcludeFilterAttribute(Type toExclude)
    {
        FilterToExclude = toExclude;
    }

    /// <summary>
    /// The type of filter that will be ignored.
    /// </summary>
    public Type FilterToExclude
    {
        get;
        private set;
    }
}

/// <summary>
/// A subclass of ControllerActionInvoker that implements the functionality of IgnoreFilterAttribute.  To use this, just override Controller.CreateActionInvoker() and return an instance of this.
/// </summary>
public class ControllerActionInvokerWithExcludeFilter : ControllerActionInvoker
{
    protected override FilterInfo GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
    {
        //base implementation does all the hard work.  we just prune off the filters to ignore
        var filterInfo = base.GetFilters(controllerContext, actionDescriptor);           
        foreach( var toExclude in filterInfo.ActionFilters.OfType<ExcludeFilterAttribute>().Select(f=>f.FilterToExclude).ToArray() )
        {
            RemoveWhere(filterInfo.ActionFilters, filter => toExclude.IsAssignableFrom(filter.GetType()));
            RemoveWhere(filterInfo.AuthorizationFilters, filter => toExclude.IsAssignableFrom(filter.GetType()));
            RemoveWhere(filterInfo.ExceptionFilters, filter => toExclude.IsAssignableFrom(filter.GetType()));
            RemoveWhere(filterInfo.ResultFilters, filter => toExclude.IsAssignableFrom(filter.GetType()));
        }
        return filterInfo;
    }


    /// <summary>
    /// Removes all elements from the list that satisfy the condition.  Returns the list that was passed in (minus removed elements) for chaining.  Ripped from one of my helper libraries (where it was a pretty extension method).
    /// </summary>
    private static IList<T> RemoveWhere<T>(IList<T> list, Predicate<T> predicate)
    {

        if (list == null || list.Count == 0)
            return list;
        //note: didn't use foreach because an exception will be thrown when you remove items during enumeration
        for (var i = 0; i < list.Count; i++)
        {
            var item = list[i];
            if (predicate(item))
            {
                list.RemoveAt(i);
                i--;
            }
        }
        return list;
    }
}

/// <summary>
/// An example of using the ExcludeFilterAttribute.  In this case, Action1 and Action3 require authorization but not Action2.  Notice the CreateActionInvoker() override.  That's necessary for the attribute to work and is probably best to put in some base class.
/// </summary>
[Authorize]
public class ExampleController : Controller
{
    protected override IActionInvoker CreateActionInvoker()
    {
        return new ControllerActionInvokerWithExcludeFilter();
    }

    public ActionResult Action1()
    {
        return View();
    }

    [ExcludeFilter(typeof(AuthorizeAttribute))]
    public ActionResult Action2()
    {
        return View();
    }

    public ActionResult Action3()
    {
        return View();
    }

}

The example is right there. As you can see, this was pretty straightforward to do and works great. I hope it's useful to anyone?

Tunaki
  • 132,869
  • 46
  • 340
  • 423
Steve Potter
  • 1,899
  • 2
  • 22
  • 25
  • `List.RemoveAll` exists: http://msdn.microsoft.com/en-us/library/wdka673a.aspx – Ian Mercer Jan 14 '11 at 00:03
  • Yeah I know about List.RemoveAll. The problem is System.Web.Mvc.FilterInfo exposes those collections as IList<> and not List, even though the underlying implementation is List<>. I could have casted to List and used RemoveAll, but I felt it was best to honor the API. My little helper method is a bit ugly, yes. I normally have that tucked into a helper library as an extension method, which makes the code much cleaner. But for this I wanted it to compile via copy paste. What do you think? – Steve Potter Jan 14 '11 at 15:50
  • Another way to exclude an existing filter is by implementing IFilterProvider. See full sample here: http://blogs.microsoft.co.il/blogs/oric/archive/2011/10/28/exclude-a-filter.aspx – Ori Calvo Oct 29 '11 at 01:06
  • When you have an answer to your own question, it shouldn't be included in the question itself by editing it. Instead, you should answer yourself, and mark your answer as accepted. – JotaBe Dec 05 '13 at 11:55
  • I don't think this is a good solution: you need to remember to override CreateActionInvoker in all your controllers, unless you have a base controller. The right solution is to implement your own ControllerFactory and set the action invoker in it. You can do that like explained here: http://stackoverflow.com/questions/568137/adding-a-controller-factory-to-asp-mvc – JotaBe Dec 05 '13 at 12:10

2 Answers2

28

I prefer the solution outlined here. Though it's not as generic a solution as yours, I found it a bit more straightforward.

In my case, I was looking for a way to enable a CompressionFilter on everything but a few items. So I created an empty attribute like this:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class DisableCompression : Attribute { }

Then in the main attribute, check for the presence of the attribute like so:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public class CompressionFilter : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        bool disabled = filterContext.ActionDescriptor.IsDefined(typeof(DisableCompression), true) ||
                        filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(DisableCompression), true);
        if (disabled)
            return;

        // action filter logic here...
    }
}

Though the page I linked to mentions that this is for MVC 3, it seems to work well enough way back in MVC 1 as well.

EDIT: showing some usage here in response to comments. Before I made the changes above, it looked exactly like this, except without the [DisableCompression] attribute flagging the method I wanted to exclude. There's no other refactoring involved.

[CompressionFilter]
public abstract class BaseController : Controller
{
}

public class SomeController : BaseController
{
    public ActionResult WantThisActionCompressed()
    {
        // code
    }

    [DisableCompression]
    public ActionResult DontWantThisActionCompressed()
    {
        // code
    }
}
Gavin
  • 9,855
  • 7
  • 49
  • 61
  • For every attribute type you wish to disable, you need to create a new "disable" attribute as well as modify the original attribute, and be sure to replace all cases of that attribute in your code. Seems like a whole lot of cumbersome work compared to my solution, which requires NO extra code. As a developer who believes in DRY, I don't see how anyone could see your solution as a better one. The only advantage I see is that it's more explicit. But so what? – Steve Potter May 17 '11 at 13:15
  • 1
    I like explicit because it's easier for me and other developers to understand. And realistically, the number of action filters one uses in a single web app, which also need to be applied to all except a few actions, is surely very low. And 4 extra lines of code does not seem cumbersome to me. Not sure where you're getting all the other cumbersome from, as far as usage it pretty much works on the same principal as yours. – Gavin May 17 '11 at 14:15
  • So in order to disable the [Authorize] attribute, you need to subclass [Authorize] into something like [DisableableAuthorize] then create a new one called [DisableAuthorize]. THEN you need to replace all cases of [Authorize] in your app with [DisableableAuthorize] and be sure everyone remembers to use [DisableableAuthorize]. Sounds like a maintenance nightmare as well as 2 new classes that could be avoided. And like you said, the number of times you need to disable attributes is few. So why go through all that trouble? The [ExcludeFilter] attribute is quick and easy, if only used once. – Steve Potter May 18 '11 at 22:06
  • 1
    Why would I subclass my [Authorize] action filter into [DisableableAuthorize]? I just add the two lines to my existing [Authorize] action filter. They check for the presence of the [DisableAuthorize] attribute. That is ALL. 4 lines. Nothing else needs to be replaced. Or are you talking about action filters that you can't modify? If the [Authorize] filter is a .NET class that you can't modify, then fine, but in my answer it's clearly not. – Gavin May 20 '11 at 01:41
  • In your solution, disabling any type of attribute involves a subclass of that attribute as well as an accompanying 'disable' attribute. And you forget that you'll have to potentially override OnActionExecuted, OnResultExecuting, and OnResultExecuted as well as OnActionExecuting. Way more than 4 lines if you want to do it right. Anyway, I'll let the issue go. I think it's more about a fundamental disagreement. I prefer reusable code while I see you appreciate a more obvious solution. Thanks for the great debate! – Steve Potter May 31 '11 at 15:41
  • There's a way to deal with this error only removing filter adding on FilterConfig. So you can add your filter wherever you want and methods/classes which don't have it added won't be called. – gandarez Jun 24 '14 at 17:39
1

I assume for years ago that the [AllowAnnonymous] attribute hadn't been added to ASP.NET MVC. Today I can have the [Authorize] attribute on top of my controller applying to all the Action methods and I just simply override this in Actions I require unauthorized users by adding the [AllowAnonymous] attributes to the specific actions.

Dev
  • 1,146
  • 2
  • 19
  • 30