Wednesday, October 17, 2012

Dependency injection in ASP.NET MVC 3 action filters

So far it's not easy task to inject dependencies in MVC 3 action filters.

For example, we have action filter attribute that performs redirection to Login page if user is not authorized:
 public class AuthorizeAttribute : ActionFilterAttribute  
 {  
   public ISiteUser SiteUser { get; set; }   
   public override void OnActionExecuting( ActionExecutingContext filterContext)  
   {  
     if (!SiteUser.IsAuthenticated)  
     {  
       var urlHelper = new UrlHelper(HttpContext .Current.Request.RequestContext);  
       if (urlHelper.RequestContext.HttpContext.Request.Url != null)  
       {  
        filterContext.Result = new RedirectResult(urlHelper.Action("Login" , "Authentication" , new { ReturnUrl = urlHelper.RequestContext.HttpContext.Request.Url.PathAndQuery }));   
       }   
     }  
   }   
 }  

This action filter has dependency ISiteUser. So, how to inject concrete implementation of SiteUser there? It is good design practice to inject dependencies in constructor. But at this point we do not control moment when this attribute is created and we do not have any way
to catch this moment.

What we can to do is to write custom ActionInvoker(using Castle Windsor IoC container, for example) and to step in process of controller action invocation:
 public class WindsorActionInvoker : ControllerActionInvoker  
 {  
   private readonly IKernel _kernel;  
   public WindsorActionInvoker(IKernel kernel)  
   {  
     _kernel = kernel;  
   }  
   protected override ActionExecutedContext InvokeActionMethodWithFilters(ControllerContext controllerContext, IList<IActionFilter > filters, ActionDescriptor actionDescriptor, IDictionary<string , object > parameters)  
   {  
     foreach (IActionFilter actionFilter in filters)  
     {  
      _kernel.InjectProperties(actionFilter);  
     }  
     return base.InvokeActionMethodWithFilters(controllerContext, filters, actionDescriptor, parameters);  
   }  
 }  

As you can see, we iterate through controller action filters and call InjectProperties extension method which magically performs dependency injections.

Let's look at this method:
 public static class WindsorExtensions  
 {  
   public static void InjectProperties(this IKernel kernel, object target)  
   {  
     var type = target.GetType();  
     foreach (var property in type.GetProperties( BindingFlags.Public | BindingFlags.Instance))  
     {  
      if (property.CanWrite && kernel.HasComponent(property.PropertyType))  
      {  
        var value = kernel.Resolve(property.PropertyType);  
        try  
        {  
          property.SetValue(target, value, null);  
        }  
        catch ( Exception ex)  
        {  
          var message = string.Format("Can't set property {0} on type {1}", property.Name, type.FullName);  
          throw new InvalidOperationException(message, ex);  
        }  
      }  
     }  
   }  
 }  

Now all the magic has gone without a trace. We simply iterate through properties of Action Filter and try to resolve them using Castle Windsor kernel. Of course this dependencies should be registered before. Also we need to setup WindsorActionInvoker as default ActionInvoker in our Web Application:
 var container = new WindsorContainer();  
 DependencyResolver.SetResolver(new WindsorDependencyResolver(container.Kernel));  
 container.Register(Component.For<ISiteUser>().ImplementedBy<SiteUser>());  
 container.Register(Component.For<IActionInvoker>().ImplementedBy<WindsorActionInvoker>().DependsOn(Property.ForKey( "kernel").Eq(container.Kernel)).LifeStyle.Transient);  

One more important point here is to setup custom WindsorDependencyResolver. This class will be used to resolve dependencies instead of default MVC dependency resolver(which actually just uses Activator.CreateInstance method). Our resolver looks like this:
 public class WindsorDependencyResolver : System.Web.Mvc.IDependencyResolver  
 {  
   private readonly IKernel _kernel;  
   public WindsorDependencyResolver(IKernel kernel)  
   {  
     _kernel = kernel;  
   }  
   public object GetService(Type serviceType)  
   {  
    return _kernel.HasComponent(serviceType) ? _kernel.Resolve(serviceType) : null ;  
   }  
   public IEnumerable<object > GetServices(Type serviceType)  
   {  
     return _kernel.HasComponent(serviceType) ? _kernel.ResolveAll(serviceType).Cast<object >() : new object [] { };  
   }  
 }  

No comments:

Post a Comment