2011-01-13 14 views
23

W ASP.NET MVC natknąłem się na kilka przypadków, w których chciałem zastosować filtr akcji dla każdej akcji z wyjątkiem jednej lub dwóch. Załóżmy na przykład, że masz AccountController. Każda akcja w nim wymaga zalogowania użytkownika, więc dodajesz [Autoryzuj] na poziomie kontrolera. Ale powiedz, że chcesz dołączyć stronę logowania do AccountController. Problem polega na tym, że użytkownicy wysłani na stronę logowania nie są autoryzowani, więc spowodowałoby to nieskończoną pętlę.Sposób na wykluczenie filtrów działania w ASP.NET MVC?

Oczywistą poprawką (inną niż przeniesienie działania Login do innego kontrolera) jest przeniesienie [Authorize] ze sterownika do wszystkich metod działania, z wyjątkiem Login. Cóż, to nie jest zabawne, szczególnie gdy masz dużo metod lub zapomnij dodać [Autoryzuj] do nowej metody.

Szyny to proste dzięki możliwości wykluczania filtrów. ASP.NET MVC nie pozwala. Postanowiłem więc umożliwić to i było to łatwiejsze niż myślałem.

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

} 

Przykład jest właśnie tutaj. Jak widać, było to całkiem proste i działa świetnie. Mam nadzieję, że przyda się każdemu?

+0

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

+0

Tak, wiem o List.RemoveAll. Problem polega na tym, że System.Web.Mvc.FilterInfo udostępnia te zbiory jako IList <>, a nie jako Listę , mimo że podstawową implementacją jest List <>. Mogłem rzucić na listę i użyć RemoveAll, ale czułem, że najlepiej jest uczcić API. Moja mała metoda pomocnicza jest trochę brzydka, tak. Zazwyczaj mam to w bibliotece pomocniczej jako metodę rozszerzenia, co czyni kod znacznie czystszym. Ale do tego chciałem go skompilować za pomocą wklejania. Co myślisz? –

+0

Innym sposobem wykluczenia istniejącego filtru jest implementacja IFilterProvider. Zobacz pełną próbkę tutaj: http://blogs.microsoft.co.il/blogs/oric/archive/2011/10/28/exclude-a-filter.aspx –

Odpowiedz

23

Preferuję rozwiązanie opisane here. Chociaż nie jest to tak ogólne rozwiązanie jak twoje, uznałem to za nieco prostsze.

W moim przypadku szukałem sposobu na włączenie CompressionFilter we wszystkim poza kilkoma elementami. Więc stworzyłem pusty atrybut tak:

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

Następnie w głównym atrybutem, sprawdzić obecność atrybutu tak:

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

Choć strona I połączone z wspomina, że ​​jest to dla MVC 3, wydaje się również działać wystarczająco dobrze w MVC 1.

EDYCJA: pokazuje niektóre wykorzystanie tutaj w odpowiedzi na komentarze. Zanim wprowadziłem powyższe zmiany, wyglądało to dokładnie tak, z wyjątkiem braku atrybutu [DisableCompression] oznaczającego metodę, którą chciałem wykluczyć. Nie ma w tym nic wspólnego z refaktoryzacją.

[CompressionFilter] 
public abstract class BaseController : Controller 
{ 
} 

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

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

Dla każdego typu atrybutu, który chcesz wyłączyć, musisz utworzyć nowy " wyłącz "atrybut, jak również zmodyfikuj oryginalny atrybut i pamiętaj, aby zastąpić wszystkie przypadki tego atrybutu w kodzie. Wydaje się, że w porównaniu do mojego rozwiązania wygląda to na dużo kłopotliwą pracę, która nie wymaga żadnego dodatkowego kodu. Jako programista, który wierzy w DRY, nie widzę, jak ktokolwiek mógłby uznać twoje rozwiązanie za lepsze. Jedyną korzyścią, jaką widzę, jest to, że jest bardziej wyraźna. No i co z tego? –

+0

Lubię wyrażać się jasno, ponieważ łatwiej jest mi je zrozumieć i innym programistom. Realistycznie rzecz biorąc, liczba filtrów akcji, których używa się w pojedynczej aplikacji internetowej, które również muszą być zastosowane do wszystkich, z wyjątkiem kilku działań, jest z pewnością bardzo niska. A 4 dodatkowe linie kodu nie wydają mi się kłopotliwe. Nie wiem, skąd bierzesz wszystkie inne kłopoty, jeśli chodzi o użycie, to działa prawie tak samo jak Ty. – Gavin

+0

Aby wyłączyć atrybut [Autoryzuj], musisz dokonać podklasy [Autoryzuj] w coś takiego jak [Wyłącz możliwośd autoryzacji], a następnie utwórz nowy o nazwie [DisableAuthorize]. W takim przypadku musisz wymienić wszystkie przypadki [Authorize] w swojej aplikacji na [DisableableAuthorize] i upewnij się, że wszyscy pamiętają [DisableableAuthorize]. Brzmi jak koszmar utrzymania, a także 2 nowe klasy, których można uniknąć. I tak jak powiedziałeś, liczba przypadków, w których musisz wyłączyć atrybuty, jest niewielka. Dlaczego więc przechodzisz przez te wszystkie kłopoty? Atrybut [ExcludeFilter] jest szybki i łatwy, jeśli jest używany tylko raz. –

0

Zakładam za lata temu, że atrybut [AllowAnnonymous] nie został dodany do ASP.NET MVC. Dzisiaj mogę mieć atrybut [Authorize] na moim kontrolerze, który stosuje się do wszystkich metod Akcji i po prostu po prostu nadpisuję to w Akcjach, które wymagają nieautoryzowanych użytkowników, dodając atrybuty [AllowAnonymous] do konkretnych działań.

Powiązane problemy