Filtry w ASP.NET MVC potrafią znaczącą polepszyć czytelność kodu, jeśli dana logika musi być wykonywana dla wielu akcji. Stanowią one tak naprawdę programowanie aspektowe, które jest całkowicie odmienne od powszechnie znanego programowania obiektowego. Ze względu na to, że programiści nie są przyzwyczajeni do tego modelu, należy uważać jak wykorzystuje się filtry.
Klasyczne zastosowanie aspektów czy filtrów to caching, logging czy autoryzacja. Problemy te nalezą do grupy tzw. cross-cutting concerns, czyli problemów, które występują w całej domenie.
W przypadku ASP.NET MVC mamy do czynienia z następującymi typami filtrów:
- IAuthorizationFilter – autoryzacja
- IActionFilter – umożliwia wstrzyknięcie logi przed i po wykonaniu danej akcji.
- IResultFilter – analogicznie do IActionFilter, ale filtr dotyczy rezultatu akcji.
- IExceptionFilter – pozwala wyłapać wyjątki.
Pierwszy filtr ma następującą sygnaturę:
public interface IAuthorizationFilter
{
void OnAuthorization(AuthorizationContext filterContext);
}
W praktyce, jak chcemy zaimplementować własną autoryzację, korzysta się z klasy bazowej AuthorizeAttribute:
namespace System.Web.Mvc
{
/// <summary>
/// Represents an attribute that is used to restrict access by callers to an action method.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class AuthorizeAttribute : FilterAttribute, IAuthorizationFilter
{
/// <summary>
/// When overridden, provides an entry point for custom authorization checks.
/// </summary>
///
/// <returns>
/// true if the user is authorized; otherwise, false.
/// </returns>
/// <param name="httpContext">The HTTP context, which encapsulates all HTTP-specific information about an individual HTTP request.</param><exception cref="T:System.ArgumentNullException">The <paramref name="httpContext"/> parameter is null.</exception>
protected virtual bool AuthorizeCore(HttpContextBase httpContext);
/// <summary>
/// Called when a process requests authorization.
/// </summary>
/// <param name="filterContext">The filter context, which encapsulates information for using <see cref="T:System.Web.Mvc.AuthorizeAttribute"/>.</param><exception cref="T:System.ArgumentNullException">The <paramref name="filterContext"/> parameter is null.</exception>
public virtual void OnAuthorization(AuthorizationContext filterContext);
/// <summary>
/// Processes HTTP requests that fail authorization.
/// </summary>
/// <param name="filterContext">Encapsulates the information for using <see cref="T:System.Web.Mvc.AuthorizeAttribute"/>. The <paramref name="filterContext"/> object contains the controller, HTTP context, request context, action result, and route data.</param>
protected virtual void HandleUnauthorizedRequest(AuthorizationContext filterContext);
/// <summary>
/// Called when the caching module requests authorization.
/// </summary>
///
/// <returns>
/// A reference to the validation status.
/// </returns>
/// <param name="httpContext">The HTTP context, which encapsulates all HTTP-specific information about an individual HTTP request.</param><exception cref="T:System.ArgumentNullException">The <paramref name="httpContext"/> parameter is null.</exception>
protected virtual HttpValidationStatus OnCacheAuthorization(HttpContextBase httpContext);
/// <summary>
/// Gets or sets the user roles.
/// </summary>
///
/// <returns>
/// The user roles.
/// </returns>
public string Roles { get; set; }
/// <summary>
/// Gets the unique identifier for this attribute.
/// </summary>
///
/// <returns>
/// The unique identifier for this attribute.
/// </returns>
public override object TypeId { get; }
/// <summary>
/// Gets or sets the authorized users.
/// </summary>
///
/// <returns>
/// The authorized users.
/// </returns>
public string Users { get; set; }
}
}
Jeśli w momencie wystąpienia wyjątku chcemy wyświetlić własną stronę, można tego dokonać za pomocą IExceptionFilter. Interfejs wygląda następująco:
namespace System.Web.Mvc
{
public interface IExceptionFilter
{
void OnException(ExceptionContext filterContext);
}
}
Przykładowa implementacja:
public class CustomExceptionAttribute : FilterAttribute, IExceptionFilter
{
public void OnException(ExceptionContext filterContext)
{
if (!filterContext.ExceptionHandled &&
filterContext.Exception is CustomApplicationException)
{
filterContext.Result = new RedirectResult("~/App/Debug");
filterContext.ExceptionHandled = true;
}
}
}
Sprawa nie jest zbyt skomplikowana. Wystarczy zaimplementować metodę OnException i zdecydować jaka ma być następna akcja. Zastosowanie filtra jest bardzo proste:
[CustomException]
public class HomeController : Controller
{
public ActionResult Index()
{
throw new CustomApplicationException();
return View();
}
}
Można go doczepić zarówno do akcji jak i kontrolera. Warto zaznaczyć, że ASP.NET MVC ma już implementacje IExceptionFilter w formie klasy HandleError. Więcej o tej klasie napiszę w następnym poście.
Proszę również zwrócić uwagę na FilterAttribute. Pisząc własny filtr, należy dziedziczyć po tej klasie albo zaimplementować samemu interfejs IMvcFilter.
Przejdźmy do IActionFilter:
public interface IActionFilter
{
void OnActionExecuting(ActionExecutingContext filterContext);
void OnActionExecuted(ActionExecutedContext filterContext);
}
Interfejs powinien być jasny – mamy dwie metody do wykonania logiki przed i po akcji. Klasycznym zastosowaniem filtra jest pomiar czasu wykonania akcji:
public class PerformanceMonitorAttribute : FilterAttribute, IActionFilter
{
private Stopwatch _timer;
public void OnActionExecuting(ActionExecutingContext filterContext)
{
_timer = Stopwatch.StartNew();
}
public void OnActionExecuted(ActionExecutedContext filterContext)
{
_timer.Stop();
filterContext.HttpContext.Response.Write(string.Format("Czas wykonania akcji: {0}",_timer.ElapsedTicks));
}
}
Przykład użycia:
public class HomeController : Controller
{
[PerformanceMonitorAttribute]
public ActionResult Index()
{
return View();
}
}
Ostatnim filtrem jest IResultFilter:
public interface IResultFilter
{
void OnResultExecuting(ResultExecutingContext filterContext);
void OnResultExecuted(ResultExecutedContext filterContext);
}
Każda akcja zwraca ActionResult. W ASP.NET MVC istnieje wiele implementacji tej klasy. Możliwe jest przekierowanie do kolejnej akcji lub zwrócenie konkretnego widoku za pomocą metody View. OnResultExecuting jest wywołany przed wygenerowaniem rezultatu ale już po wykonaniu logiki akcji. W przypadku powyższej metody Index, OnResultExecuting zostanie wywołany tuż przed wykonaniem View(). Warto chyba pokazać jak wygląda abstrakcyjna klasa ActionResult:
public abstract class ActionResult
{
/// <summary>
/// Enables processing of the result of an action method by a custom type that inherits from the <see cref="T:System.Web.Mvc.ActionResult"/> class.
/// </summary>
/// <param name="context">The context in which the result is executed. The context information includes the controller, HTTP content, request context, and route data.</param>
public abstract void ExecuteResult(ControllerContext context);
}
Innymi słowy, OnResultExecuting jest wywołany przed ActionResult.ExecuteResult a OnResultExecuted po. Analogiczna implementacja filtru sprawdzającego wydajność może wyglądać następująco:
public class PerformanceMonitorAttribute : FilterAttribute, IResultFilter
{
private Stopwatch _timer;
public void OnResultExecuting(ResultExecutingContext filterContext)
{
_timer = Stopwatch.StartNew();
}
public void OnResultExecuted(ResultExecutedContext filterContext)
{
_timer.Stop();
filterContext.HttpContext.Response.Write(string.Format("Zwrocenie wynikku akcji: {0}", _timer.ElapsedTicks));
}
}
Ze względu, że bardzo często IResultFilter i IActionFilter mają podobne zastosowanie, framework dostarcza klasę bazową implementującą oba interfejsy:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public abstract class ActionFilterAttribute : FilterAttribute, IActionFilter, IResultFilter
{
/// <summary>
/// Called by the ASP.NET MVC framework before the action method executes.
/// </summary>
/// <param name="filterContext">The filter context.</param>
public virtual void OnActionExecuting(ActionExecutingContext filterContext);
/// <summary>
/// Called by the ASP.NET MVC framework after the action method executes.
/// </summary>
/// <param name="filterContext">The filter context.</param>
public virtual void OnActionExecuted(ActionExecutedContext filterContext);
/// <summary>
/// Called by the ASP.NET MVC framework before the action result executes.
/// </summary>
/// <param name="filterContext">The filter context.</param>
public virtual void OnResultExecuting(ResultExecutingContext filterContext);
/// <summary>
/// Called by the ASP.NET MVC framework after the action result executes.
/// </summary>
/// <param name="filterContext">The filter context.</param>
public virtual void OnResultExecuted(ResultExecutedContext filterContext);
}
Jeśli chcemy zastosować filtr dla kilku kontrolerów, najlepszym sposobem jest po prostu ich implementacja w osobnych klasach jak to zostało pokazane wyżej. Czasami jednak chcemy napisać filtr tylko dla pojedynczego kontrolera. W takich przypadkach łatwiejsze może okazać się po prostu przeładowanie metody w tym konkretnym kontrolerze:
public class HomeController : Controller
{
[PerformanceMonitorAttribute]
public ActionResult Index()
{
return View();
}
protected override void OnActionExecuted(ActionExecutedContext filterContext)
{
base.OnActionExecuted(filterContext);
}
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);
}
}
W każdym kontrolerze można przeładować wszystkie wcześniej opisane metody.
Jeśli natomiast chcemy zastosować filtr do wszystkich metod, wtedy lepiej zdefiniować to globalnie:
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new HandleErrorAttribute());
}
}
Jeśli projekt został automatycznie wygenerowany to wystarczy dodać filtr w RegisterGlobalFilter. Należy jednak upewnić się, że metoda jest wywoływana przy starcie aplikacji:
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
AuthConfig.RegisterAuth();
}
}
Zwykle to nie powinno mieć znaczenia, ale istnieje możliwość wykonywania filtrów w konkretnym porządku. Służy do tego właściwość Order:
[PerformanceMonitorAttribute(Order = 1)]
[AnotherPerformanceMonitorAttribute(Order = 2)]
public ActionResult Index()
{
return View();
}