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?
'Lista .RemoveAll' istnieje: http://msdn.microsoft.com/en-us/library/wdka673a.aspx –
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? –
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 –