Dependency Injection with ASP.NET MVC Action Filters
Action Filter attributes in ASP.NET MVC are a very nice way of encapsulating logic into small reusable components that can easily be reused across multiple controllers.
However, if you need your action filters to do anything complex, injecting services into action filters using an Inversion of Control container (such as Windsor or StructureMap) can be painful.
Typically, when using an IoC container it is necessary to register all your components with the container up-front and then use the container to create instances for you. When the container creates the object, it will also create any required dependencies and pass them to the constructor (or property setters).
However, as ActionFilters are defined as attributes, they cannot be instantiated by the container. One solution to this problem is to wrap the container with a static class and then make use of constructor chaining to create the illusion of automatic dependency resolution:
public class LoggingFilter : ActionFilterAttribute { private ILogger logger; public LoggingFilter() : this(IoC.Resolve<ILogger>()) { } public LoggingFilter(ILogger logger) { this.logger = logger; } public override void OnActionExecuting(ActionExecutingContext context) { logger.log("Entering action: " + context.RouteData.GetRequiredString("action")); } }
In this example, the default constructor will call into a static 'IoC' class (which wraps the underlying container) to resolve an ILogger instance and then pass this to the second constructor. In a unit-testing scenario, you can make use of the second constructor directly in order to supply a mocked ILogger instance.
There are several things I don't like about this code:
- The filter now has a coupling to the container
- If you trigger the instantiation of the attribute without first initialising the container then this can lead to problems (eg reflecting over a method's attributes in a unit-testing scenario)
- It's just plain ugly!
Thankfully, there is a better way. Several IoC containers (including Autofac and Unity) can inject services into objects using property setters without the target object needing to be registered with the container. I'll be using Autofac for the sample code.
We can write a custom ControllerActionInvoker to make use of this feature by intercepting the action filters before they are invoked. The custom action invoker looks like this:
namespace MyApp { using System; using System.Collections.Generic; using System.Reflection; using System.Web.Mvc; using Autofac; public class MyActionInvoker : ControllerActionInvoker { private readonly IContainer container; public MyActionInvoker(IContainer container) { this.container = container; } protected override ActionExecutedContext InvokeActionMethodWithFilters(MethodInfo methodInfo, IDictionary<string, object> parameters, IList<IActionFilter> filters) { foreach (var filter in filters) { container.InjectProperties(filter); } return base.InvokeActionMethodWithFilters(methodInfo, parameters, filters); } } }
The custom action invoker takes an instance of IContainer (the autofac container) in its constructor, which it can then use to inject services into the action filter attributes before they are invoked. This is done by overriding the InvokeActionMethodWithFilters method and calling container.InjectProperties for each object in the filters collection.
In order to get Autofac working with ASP.NET MVC, you need to register an HTTP module as well as set up the AutofacControllerFactory (instructions here.)
It is also necessary to register the custom action invoker with Autofac in your global.asax:
var builder = new ContainerBuilder(); builder.Register<MyActionInvoker>().As<IActionInvoker>().FactoryScoped();
As well as the ActionInvoker, you also need to register your controllers. Autofac contains a 'module' for doing this (the AutofacControllerModule), but we need to make a slight modification in order to use our custom action invoker. The modified module looks like this:
public class ControllerModule : Module { private readonly Assembly assembly; private readonly IControllerIdentificationStrategy controllerNamingStrategy = new DefaultControllerIdentificationStrategy(); public ControllerModule(Assembly assembly) { this.assembly = assembly; } protected override void Load(ContainerBuilder builder) { var controllers = from type in assembly.GetExportedTypes() where typeof(IController).IsAssignableFrom(type) where !type.IsAbstract select type; foreach (var type in controllers) { builder.Register(type) .FactoryScoped() .As(controllerNamingStrategy.ServiceForControllerType(type)) .As(type) .OnActivating(InjectInvoker); } } private void InjectInvoker(object sender, ActivatingEventArgs e) { ((Controller)e.Instance).ActionInvoker = e.Context.Resolve<IActionInvoker>(); } }
Now when a controller is instantiated, the ActionInvoker property will be set to an instance of our custom action invoker. The module should be registered with the container builder:
builder.RegisterModule(new ControllerModule(typeof(HomeController).Assembly));
So, assuming an ILogger instance is registered with the container, the sample LoggingFilter could be written like this:
public class LoggingFilter : ActionFilterAttribute { public ILogger Logger { get; set; } public override void OnActionExecuting(ActionExecutingContext context) { Logger.log("Entering action: " + context.RouteData.GetRequiredString("action")); } }
Much better!
The same approach can also be used for other filter types, (such as AuthorizationFilters) by overriding the appropriate method.
Note that container.InjectProperties makes use of reflection, so there is a potetial performance issue here.
Edit (5 December 2008): Here is an extension method that adds the InjectProperties method to the Microkernel from Castle Windsor.
using System; using System.Reflection; using Castle.MicroKernel; using Castle.MicroKernel.ComponentActivator; public static class WindsorExtension { 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("Error setting property {0} on type {1}, See inner exception for more information.", property.Name, type.FullName); throw new ComponentActivatorException(message, ex); } } } } }