Na obsah stránky

ActionInterceptor pro ASP.NET MVC

Aleš Roubíček | | # permalink

V pátek jsem na twitteru publikoval ActionInterceptor a záhy jsem dostal reakci od Dana Kolmana, „proč ne ActionFilterAttribute?“ Sice jsem to na twitteru shrnul, ale klidně si to shrnu ještě jednou a obšírněji. Dělá mi to totiž dobře.

Action Filtry

Action filtry jsou – už od první verze ASP.NET MVC – způsobem, jak rozšiřovat akce o znovupoužitelné aspekty. Můžete tak zabalit často používané koncepty a jednoduše je aplikovat tam, kde je to třeba. Jenže action filtry mají několik zásadních omezení:

  1. Nepřijde mi úplně košer, aby atributy byly nositeli jednotky práce. Jejich úkolem je poskytovat metadata, nikoli funkcionalitu.
  2. Do verze 3 tu nebyla možnost injektovat závislosti, ve 3. verzi přibyl service lokátor (IDependencyResolver), který má k dokonalosti velmi daleko.
  3. Pokud chci přidat aspekt, musím zasahovat do rozšiřovaného kódu.

Všechny tři body jsou pro mne velmi zásadní chyby v návrhu a použitelnosti. Jak je vyřešit?

Action interceptor

Ve svých aplikacích spokojeně používám IoC kontejner Castle.Windsor, který umožňuje snadno vkládat aspekty. Děje se tak pomocí dynamicky vytvářených proxy nad dekorovanými objekty. Jak se má aspekt chovat a kde aplikovat můžeme snadno popsat v objektu, který implementuje rozhraní IInterceptor.

Ze základního popisu to nemusí být zcela zřejmé, ale aspekty můžete aplikovat pouze na virtuální metody. Proto když chcete přidávat aspekt do vašich kontrolerů musíte sáhnout k tomu, že všechny akce budou virtual (což je IMO nesmysl) nebo prostě využijete již existující extension pointy, které MVC framework nabízí. Kupodivu jsou to známé metody, které se objevují i v Action filterech. Implementace Action interceptoru bude tedy vypadat následovně:

using System;
using System.Linq;
using System.Web.Mvc;
using Castle.DynamicProxy;

public abstract class ActionInterceptor : IInterceptor, IActionFilter, IResultFilter {

 public void Intercept(IInvocation invocation) {
    switch (invocation.Method.Name) {
      case "OnActionExecuting":
        Intercept(invocation, OnActionExecuting);
        break;
      case "OnActionExecuted":
        Intercept(invocation, OnActionExecuted);
        break;
      case "OnResultExecuting":
        Intercept(invocation, OnResultExecuting);
        break;
      case "OnResultExecuted":
        Intercept(invocation, OnResultExecuted);
        break;
    }
    invocation.Proceed();
  }

  void Intercept<TContext>(IInvocation invocation, Action<TContext> action) {
    action(invocation.Arguments.First() as TContext);
  }

  public virtual void OnActionExecuting(ActionExecutingContext filterContext) {

  }

  public virtual void OnActionExecuted(ActionExecutedContext filterContext) {

  }

  public virtual void OnResultExecuting(ResultExecutingContext filterContext) {

  }

  public virtual void OnResultExecuted(ResultExecutedContext filterContext) {

  }
}

Užití je pak už pouhé podědění a přepsání patřičné metody podobně jako u Action filtrů.

Aplikace aspektu

Takovýto aspekt můžeme aplikovat několika způsoby. Nejpřímější cestou, bez nutnosti modifikovat cíl aspektu, je konfigurace kontejneru:

Component.
  For<LoggingInterceptor>().
  Lifestyle.Transient,

Component.
  For<HomeController>().
  Lifestyle.Transient.
  Interceptors<LoggingInterceptor>()

A jedem. :)

Další možností je aplikovat interceptor podobně jako action filtr – pomocí atributu:

[Authorization]
public class HomeController : System.Web.Mvc.Controller {

}

public class AuthorizationAttribute : Castle.Core.InterceptorAttribute {
  public AuthorizationAttribute()
    : base(typeof(AuthorizationInterceptor)) {
  }
}

Každý ze způsobů užití má své přednosti. Konfiguraci kontejneru používám tehdy, když potřebuji přidat dočasné nebo velice obecné aspekty. Naopak, pokud potřebuji explicitně vyjádřit nějaký koncept (autorizaci, invalidaci cache apod.), který má být při čtení controlleru zřejmý, použiji atributy.

Našli jste v článku chybu? Máte námět na reportáž? Založte mi ticket.