Me he encontrado con varios casos en ASP.NET MVC donde quería aplicar un filtro de acción en cada acción excepto una o dos. Por ejemplo, supongamos que tiene un AccountController. Cada acción requiere que el usuario inicie sesión, por lo que agrega [Autorizar] en el nivel del controlador. Pero supongamos que desea incluir la página de inicio de sesión en AccountController. El problema es que los usuarios enviados a la página de inicio de sesión no están autorizados, por lo que esto daría lugar a un ciclo infinito.¿Una forma de excluir filtros de acción en ASP.NET MVC?
La solución obvia (aparte de mover la acción de inicio de sesión a otro controlador) es mover el [Autorizar] del controlador a todos los métodos de acción excepto el inicio de sesión. Bueno, eso no es divertido, especialmente cuando tienes muchos métodos u olvidas agregar [Autorizar] a un nuevo método.
Rails hace esto fácil con la capacidad de excluir filtros. ASP.NET MVC no te permite. Así que decidí hacerlo posible y fue más fácil de lo que pensaba.
/// <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();
}
}
El ejemplo está justo ahí. Como puede ver, esto fue bastante sencillo de hacer y funciona muy bien. Espero que sea útil para cualquiera?
'Lista .RemoveAll' existe: http://msdn.microsoft.com/en-us/library/wdka673a.aspx –
Sí, sé de List.RemoveAll. El problema es System.Web.Mvc.FilterInfo expone esas colecciones como IList <> y no como List, aunque la implementación subyacente es List <>. Pude haber reemplazado a List y utilicé RemoveAll, pero sentí que era mejor respetar la API. Mi pequeño método de ayuda es un poco feo, sí. Normalmente lo tengo metido en una biblioteca de ayuda como método de extensión, lo que hace que el código sea mucho más limpio. Pero para esto, quería compilar a través de copiar y pegar. ¿Qué piensas? –
Otra forma de excluir un filtro existente es mediante la implementación de IFilterProvider. Vea la muestra completa aquí: http://blogs.microsoft.co.il/blogs/oric/archive/2011/10/28/exclude-a-filter.aspx –