Exclude A Filter

28 באוקטובר 2011

In my last ASP.NET MVC MSDN session I was asked a simple question: "How can we disable a filter for a specific action, assuming it is already defined at the class level?"


Of course, having this ability can really ease the maintenance of our application. Without this ability we need NOT to define the filter at the class level and then define at each action level.


Well, I did not have any simple question. However, I claimed that such an infrastructure can be easily developed using the excellent ASP.NET MVC extensibility support. Is that so? Let's try do it


Full sample code can be found here


ASP.NET MVC has a special provider named "FilterProvider" which is responsible for fetching all relevant filters from a specific controller and action.
We can define our own provider and register it instead of the default ones. Once we do this, we have a full control of the filters ASP.NET MVC is aware of and we can decide which filters to remove according to some criteria.


Suppose we have the following controller definition: 

[Authorize]
public class HomeController : Controller
{
    public ActionResult Index()
    {
        return View();
    }

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

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

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

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

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

And we would like to make the "Index" action unsecured. In other words, we would like to hide the "Authorize" filter at the "Index" action level but still keep it at the class level.
It would be nice if the following is supported: 

[Authorize]
public class HomeController : Controller
{
    [ExcludeFilter(typeof(AuthorizeAttribute))]
    public ActionResult Index()
    {
        return View();
    }

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

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

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

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

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

To achieve that, we need to define a new Attribute named "ExcludeFilter":

public class ExcludeFilterAttribute : FilterAttribute
{
    private Type filterType;

    public ExcludeFilterAttribute(Type filterType)
    {
        this.filterType = filterType;
    }

    public Type FilterType
    {
        get
        {
            return this.filterType;
        }
    }

Now, let's define a new IFilterProvider which looks for ExcludeFilter attributes and filters out the relevant action filters:

public class ExcludeFilterProvider : IFilterProvider
{
    private FilterProviderCollection filterProviders;

    public ExcludeFilterProvider(IFilterProvider[] filters)
    {
        this.filterProviders = new FilterProviderCollection(filters);
    }

    public IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
    {
        Filter[] filters = this.filterProviders.GetFilters(controllerContext, actionDescriptor).ToArray();

        IEnumerable<ExcludeFilterAttribute> excludeFilters = (from f in filters where f.Instance is ExcludeFilterAttribute select f.Instance as ExcludeFilterAttribute);

        List<Type> filterTypesToRemove = new List<Type>();
        foreach (ExcludeFilterAttribute excludeFilter in excludeFilters)
        {
            filterTypesToRemove.Add(excludeFilter.FilterType);
        }

        IEnumerable<Filter> res = (from filter in filters where !filterTypesToRemove.Contains(filter.Instance.GetType()) select filter);
        return res;
    }

In addition, we need to register our FilterProvider and remove the old ones:

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();

    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);

    IFilterProvider[] providers = FilterProviders.Providers.ToArray();
    FilterProviders.Providers.Clear();
    FilterProviders.Providers.Add(new ExcludeFilterProvider(providers));
}

That's it!!!


As you can see, it is quite simple to "twick" ASP.NET MVC to do the thing we need.


 

Add comment
facebook linkedin twitter email

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

2 comments

  1. Leniel Macaferi23 במאי 2012 ב 23:48

    Hi Ori,

    Amazing post! This worked great in my case where I wanted to exclude a PasswordExpiredAttribute filter from my AccountController. I applied the ExcludeFilterAttribute to the controller. Now my custom filter only runs once.

    I love ASP.NET MVC… so customizable. 😀

    Thanks for posting.

    All the best,

    Leniel

    Reply