2011-01-13 35 views
23

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?

+0

'Lista .RemoveAll' existe: http://msdn.microsoft.com/en-us/library/wdka673a.aspx –

+0

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? –

+0

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 –

Respuesta

23

Prefiero la solución que se describe here. Aunque no es una solución tan genérica como la tuya, me pareció un poco más directa.

En mi caso, estaba buscando una forma de habilitar un CompressionFilter en todo menos en algunos elementos. Así que creé un atributo vacío de esta manera:

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

Luego, en el atributo principal, verificar la presencia del atributo de este modo:

[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... 
    } 
} 

Aunque la página he vinculado menciona que esto es para MVC 3, parece funcionar bastante bien en MVC 1 también.

EDITAR: muestra algunos usos aquí en respuesta a los comentarios. Antes de realizar los cambios anteriores, se veía exactamente igual, excepto que el atributo [DisableCompression] marcaba el método que quería excluir. No hay otra refactorización involucrada.

[CompressionFilter] 
public abstract class BaseController : Controller 
{ 
} 

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

    [DisableCompression] 
    public ActionResult DontWantThisActionCompressed() 
    { 
     // code 
    } 
} 
+0

Para cada tipo de atributo que desee deshabilitar, debe crear uno nuevo " deshabilitar "atributo" así como modificar el atributo original, y asegúrese de reemplazar todos los casos de ese atributo en su código. Parece un montón de trabajo engorroso en comparación con mi solución, que no requiere código adicional. Como desarrollador que cree en DRY, no veo cómo alguien podría ver su solución como una mejor. La única ventaja que veo es que es más explícito. ¿Y qué? –

+0

Me gusta explícita porque es más fácil de entender para mí y para otros desarrolladores. Y, de manera realista, la cantidad de filtros de acción que uno usa en una sola aplicación web, que también debe aplicarse a todos, excepto a algunas acciones, es seguramente muy baja. Y 4 líneas adicionales de código no me parecen engorrosas. No estoy seguro de dónde está sacando todo lo demás engorroso, en cuanto al uso, funciona prácticamente con el mismo principio que el suyo. – Gavin

+0

Por lo tanto, para deshabilitar el atributo [Autorizar], debe crear una subclase [Autorizar] en algo como [Autorización inhabilitada] y luego crear una nueva llamada [Autorización de inhabilitación]. ENTONCES necesita reemplazar todos los casos de [Autorizar] en su aplicación con [Autorización inhabilitada] y asegurarse de que todos recuerden usar [Autorización inhabilitada]. Parece una pesadilla de mantenimiento, así como 2 nuevas clases que podrían evitarse. Y como dijo, la cantidad de veces que necesita deshabilitar los atributos es poca. Entonces, ¿por qué pasar por todos esos problemas? El atributo [ExcludeFilter] es rápido y fácil, si solo se usa una vez. –

0

que asumir por Hace años que el atributo [AllowAnnonymous] no había sido añadido a ASP.NET MVC. Hoy puedo tener el atributo [Authorize] en la parte superior de mi controlador que se aplica a todos los métodos de Acción y simplemente anulo esto en Acciones. Requiero usuarios no autorizados agregando los atributos [AllowAnonymous] a las acciones específicas.

Cuestiones relacionadas