In ASP.NET MVC you may “tag” your actions with attributes derived from ActionFilterAttribute and automatically have their OnActionExecuting and OnActionExecuted methods called before and after the framework invokes your action, thus achieving a simple form of method interception by using attributes.
Pretty simple model, but it falls apart if you are using an IoC container to wire things up. Moreover, having application logic embedded in properties is sort of icky. It just so happens that we can solve both problems and end up with a pretty nice solution at the same time 🙂
First, create the interface you wish to use to denote an action filter – e.g. something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public interface IActionFilterService { void Before(IBeforeActionContext context); void After(IAfterActionContext context); } public interface IBeforeActionContext { } public interface IAfterActionContext { void ProvideViewData(IDictionary<string, object> data); } |
Note that I have used interfaces for the arguments. This allows us to decouple the action filter from the framework classes, thus making our stuff more easily testable.
In this example I have only put one method –
ProvideViewData – on
IAfterActionContext, allowing my action filters to insert additional key-value-pairs into the
ViewData. More methods should of course be added if they are needed.
Then, create the actual action filter attribute, and make it accept a type as ctor argument – e.g. like this (assuming I want to resolve dependencies through the Windsor container installed as my controller factory):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public class ServiceFilterAttribute : ActionFilterAttribute { readonly Type serviceType; readonly IWindsorContainer container = WindsorControllerFactory.Instance.Container; public ServiceFilterAttribute(Type serviceType) { this.serviceType = serviceType; } public override void OnActionExecuting(ActionExecutingContext filterContext) { ((IActionFilterService)container .Resolve(serviceType)) .Before(new BeforeActionContext(filterContext)); } public override void OnActionExecuted(ActionExecutedContext filterContext) { ((IActionFilterService)container .Resolve(serviceType)) .After(new AfterActionContext(filterContext)); } } |
And then implement the before and after argument classes – e.g. like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
class BeforeActionContext : IBeforeActionContext { readonly ActionExecutingContext context; public BeforeActionContext(ActionExecutingContext context) { this.context = context; } } class AfterActionContext : IAfterActionContext { readonly ActionExecutedContext context; public AfterActionContext(ActionExecutedContext context) { this.context = context; } public void ProvideViewData(IDictionary<string, object> data) { var viewResult = context.Result as ViewResult; if (viewResult == null) return; foreach (var pair in data) { viewResult.ViewData.Add(pair); } } } |
Now you may use the action filter like this:
1 2 3 4 5 |
[ServiceFilter(typeof(IProvideProductCount))] public ViewResult Index() { return View(); } |
where your IProvideProductCount will have all its dependencies automatically resolve by the container. That’s just sweet 🙂
In this example I made this simple implementation:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public class ProvideProductCount : IProvideProductCount { readonly IProductRepository productRepository; public ProvideProductCount(IProductRepository productRepository) { this.productRepository = productRepository; } public void Before(IBeforeActionContext context) { } public void After(IAfterActionContext context) { var count = productRepository.Count(); context .ProvideViewData(new Dictionary<string, object> { {"productCount", count} }); } } |
– thus allowing any views rendered from an action tagged with [ServiceFilter(typeof(IProvideProductCount))] to show how many products we have.
If your action filter attributes need arguments, then it’s another story. I wonder how one could get around to implement that?