Alex Meyer-Gleaves

on autofac, mvc

FilterAttribute Property Injection in Autofac MVC 3 Integration

The current mechanism for performing property injection on FilterAttribute instances via the ExtensibleActionInvoker had to be removed recently due to a rather nasty bug. These are the notes that Nick provided outlining the problem he discovered (possibly with the help of the exciting new Whitebox profiler).

Because the filters passed from the base action invoker also include the controller, property injection happens on the controller itself several times as the filters are processed.

The filter attributes also included in the collection may also be singletons cached by MVC, and so it is quite likely that dependencies may be overwritten with those from a concurrently executing request.

In all this behaviour is probably too risky to reliably support.

Removed property injection routine. (Breaking change.)

I have replaced the old mechanism using an approach that leverages the improved dependency injection support added to MVC 3 (this will be in the next release). To make use of property injection for your filter attributes all you will need to do is call the RegisterFilterProvider method on the ContainerBuilder before building your container and providing it to the AutofacDependencyResolver.

ContainerBuilder builder = new ContainerBuilder();

builder.RegisterControllers(Assembly.GetExecutingAssembly());
builder.Register(c => new Logger()).As<ILogger>().InstancePerHttpRequest();
builder.RegisterFilterProvider();

IContainer container = builder.Build();
DependencyResolver.SetResolver(new AutofacDependencyResolver(container));

Then you can add properties to your filter attributes and any matching dependencies that are registered in the container will be injected into the properties. For example, the action filter below will have the ILogger instance that was registered above injected. Note that the attribute itself does not need to be registered in the container.

public class CustomActionFilter : ActionFilterAttribute
{
    public ILogger Logger { get; set; }

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        Logger.Log("OnActionExecuting");
    }
}

The same simple approach applies to the other filter attribute types such as authorization attributes.

public class CustomAuthorizeAttribute : AuthorizeAttribute
{
    public ILogger Logger { get; set; }

    protected override bool AuthorizeCore(HttpContextBase httpContext)
    {
        Logger.Log("AuthorizeCore");
        return true;
    }
}

After applying the attributes to your actions as required your work is done.

[CustomActionFilter]
[CustomAuthorizeAttribute]
public ActionResult Index()
{
    // ...
}

To make this work I added a custom FilterAttributeFilterProvider implementation. The custom filter provider delegates the job of collecting the filters to the base class. Once the filters have been retrieved by the base class, the ILifetimeScope for the current HTTP request is retrieved and used to perform property injection on the filters. The false passed to the base FilterAttributeProvider constructor sets the cacheAttributeInstances parameter to ensure that attribute instances are not cached. Allowing the attribute instances to be cached would result in race conditions and other unexpected behaviour.

/// <summary>
/// Defines a filter provider for filter attributes that performs property injection.
/// </summary>
public class AutofacFilterAttributeFilterProvider : FilterAttributeFilterProvider
{
    /// <summary>
    /// Initializes a new instance of the <see cref="AutofacFilterAttributeFilterProvider"/> class.
    /// </summary>
    /// <remarks>
    /// The <c>false</c> constructor parameter passed to base here ensures that attribute instances are not cached.
    /// </remarks>
    public AutofacFilterAttributeFilterProvider() : base(false)
    {
    }

    /// <summary>
    /// Aggregates the filters from all of the filter providers into one collection.
    /// </summary>
    /// <param name="controllerContext">The controller context.</param>
    /// <param name="actionDescriptor">The action descriptor.</param>
    /// <returns>
    /// The collection filters from all of the filter providers with properties injected.
    /// </returns>
    public override IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
    {
        var filters = base.GetFilters(controllerContext, actionDescriptor).ToArray();
        var lifetimeScope = AutofacDependencyResolver.Current.RequestLifetimeScope;

        if (lifetimeScope != null)
            foreach (var filter in filters)
                lifetimeScope.InjectProperties(filter.Instance);

        return filters;
    }
}

The RegisterFilterProvider method has been added to the ContainerBuilder using an extension method. This method will register the AutofacFilterAttributeFilterProvider using the IFilterProvider interface that MVC uses when asking the dependency resolver for filter providers. Following the instructions outlined in Brad Wilson’s post on the subject of dependency injection and filters, I made sure that the default FilterAttributeFilterProvider instance is removed from the static collection of providers.

/// <summary>
/// Registers the <see cref="AutofacFilterAttributeFilterProvider"/>.
/// </summary>
/// <param name="builder">The container builder.</param>
public static void RegisterFilterProvider(this ContainerBuilder builder)
{
    if (builder == null) throw new ArgumentNullException("builder");

    foreach (var provider in FilterProviders.Providers.OfType<FilterAttributeFilterProvider>().ToArray())
        FilterProviders.Providers.Remove(provider);

    builder.RegisterType<AutofacFilterAttributeFilterProvider>()
        .As<IFilterProvider>()
        .SingleInstance();
}

If you were using the old mechanism you will have breaking changes to contend with, but as you can see it should be easy to get back on track again.

profile
AUTHOR

Alex Meyer-Gleaves

I'm a Technical Architect living in Australia (that island like continent in the southern hemisphere). I love Microsoft .NET and C#. I hate early mornings, slow drivers and Lotus Notes.