MVC Controllers as plugins with MEF and Autofac

Disclaimer

This blog posts talks about Umbraco v5 (Jupiter), however since the state of the codebase is in it’s infancy, the details about Umbraco v5 in this article may not be accurate once Umbraco v5 is released.

Plugins

In Umbraco v5 we’re introducing the concept of plugins. Currently in Umbraco, we have the notion of plugins such as Trees, Actions (used on context menus), Data types, etc… Each of these objects is found using a class called TypeFinder to search for specific types in all assemblies that are loaded in the ‘bin’ folder. Though this works, it is definitely not the most efficient approach to plugins. For Umbraco v5, we’re now using MEF as the plugin framework which is now part of .Net framework 4.  There will be many coming blog posts about the different types of plugins, how they work and how to create them using MEF, but this blog post is about overcoming a small hurdle we encountered when trying to integrate MEF, Autofac and MVC controllers as 3rd party plugins….

MVC Controllers as plugins

For Umbraco v5, we’re using Autofac as the IoC container which works really well for injecting dependencies into MVC controllers (amongst a ton of other things). Autofac also plays really nicely with MEF through it’s MEF integration library and requires all of 2 lines of code to get the MEF plugins into our IoC container:

var catalog = new DirectoryCatalog( m_HttpServer.MapPath("~/Plugins/Trees")); builder.RegisterComposablePartCatalog(catalog);

The above code tells Autofac to register all exportable MEF components that are found in all assemblies in the ~/Plugins/Trees folder. In our case, the plugins in this folder are actually MVC Controllers which will get registered in to the IoC container and since we are currently using the AutofacControllerFactory for our MVC controller factory, it ‘should’ be able to create the requested controller and inject it’s dependencies because we’ve stuck it in the container.

Problem 1

So far this is all very easy, but as soon as we want to route a request to this controller we end up with a 404 error stating that the page/controller cannot be found.  The reason for this is because the AutofacControllerFactory inherits from the DefaultControllerFactory which searches for controllers using the BuildManager which only searches for types registered in the ‘bin’ folder. I was hoping that the AutofacControllerFactory would have also searched within it’s registrations but this turns out to not be the case. (In fact, once Autofac releases it’s MVC3 implementation, it will no longer be shipped with an AutofacControllerFactory and instead be using MVC3’s new DependencyResolver).

Solution 1

In order for this to work we have to create our own ControllerFactory  (source code at the end of this article) and provide our own implementation for the methods:

IController GetControllerInstance(RequestContext context, Type controllerType) Type GetControllerType(RequestContext requestContext, string controllerName)

The GetControllerType implementation does the following:

  • Checks if the requested controllerName has already been resolved and exists in our factory’s internal cache
  • If not, try to find the controller by name from the underlying DefaultControllerFactory
  • If not found, search the IoC container’s registrations for the type,
    • if found, store the reference Type in the factory’s internal cache

The GetControllerInstance does the following:

  • Check if the controllerType has been registered in the factory’s internal cache
    • If so, get the return the resolved IController instance from the container
  • If not, try to create the controller from the underlying DefaultControllerFactory

Problem 2

The next problem we encountered was that Autofac couldn’t inject dependent objects into the controllers that weren’t registered in the container using the Autofac MEF extensions. A quick post on to the Autofac Google Group and I had my answer (read the post if you want examples)!

Solution 2

Essentially, if you want to expose objects to MEF in Autofac, you have to explicitly register them in the container with the ‘Exported’ extension method. Example:

//register the umbraco settings builder.Register<UmbracoSettings>(x => UmbracoSettings.GetSettings()) .As<IUmbracoSettings>() //ensure it's registered for MEF too .Exported(x => x.As<IUmbracoSettings>()) //only have one instance ever .SingleInstance();

Problem 3

The last problem was that by default it seems that exported MEF components are instantiated as singletons, meaning that each time you try to resolve a MEF component of a certain type it will always be the exact same instance. MVC really doesn’t like this when it comes to controllers, in fact MVC gives you a very informative error message regarding this and explains that if you are using dependency injection to create controllers to make sure the container is configured to resolve new controller objects, not the same instance.

Solution 3

All we need to do is add a PartCreationPolicyAttribute to the exported MEF type and set it to CreationPolicy.NonShared which tells the framework to create a new instance each time it is resolved. Example:

[Tree(typeof(ContentTreeController))] [PartCreationPolicy(CreationPolicy.NonShared)] public class ContentTreeController : DemoDataTreeController

UmbracoControllerFactory source

Here’s the source code for the custom controller factory, it currently inherits from AutofacControllerFactory but once we upgrade to use the new MVC 3 Autofac implementation, this will simply inherit from DefaultControllerFactory.

using System; using System.Web; using System.Web.Mvc; using System.Linq; using System.Web.Routing; using Autofac; using Autofac.Core; using Autofac.Integration.Web; using Autofac.Integration.Web.Mvc; using System.Collections.Generic; using Autofac.Features.Metadata; using UmbracoProjects.CMS.Web.Trees.Controllers; using System.Text.RegularExpressions; namespace UmbracoProjects.CMS.Web.Mvc.Controllers { public class UmbracoControllerFactory : AutofacControllerFactory { protected IContainer m_Container; private static readonly object m_Locker = new object(); /// <summary> /// Used to cache found controllers in the IoC container /// </summary> private static readonly Dictionary<string, ServiceTypeCache> m_IoCControllerCache = new Dictionary<string, ServiceTypeCache>(); private class ServiceTypeCache { public Service Service { get; set; } public Type ComponentType { get; set; } } /// <summary> /// Initializes a new instance of the <see cref="AutofacControllerFactory"/> class. /// </summary> /// <param name="containerProvider">The container provider.</param> public UmbracoControllerFactory(IContainerProvider containerProvider) : base(containerProvider) { m_Container = containerProvider.ApplicationContainer; } /// <summary> /// Creates the controller based on controller type /// </summary> /// <param name="context">The context.</param> /// <param name="controllerType">Type of the controller.</param> /// <returns>The controller.</returns> /// <remarks> /// This first checks our IoC service internal cache to see if it exists, if it does, we'll try to resolve the controller /// from IoC, otherwise we'll try to resolve the controller from the underlying factory /// </remarks> protected override IController GetControllerInstance(RequestContext context, Type controllerType) { if (context == null) throw new ArgumentNullException("context"); //first, check if this service by this type is in our cache if (m_IoCControllerCache.Where(x => x.Value.ComponentType.Equals(controllerType)).Any()) { var controllerService = m_IoCControllerCache.Where(x => x.Value.ComponentType.Equals(controllerType)).SingleOrDefault().Value.Service; object controller; if (m_Container.TryResolveService(controllerService, out controller)) { //if the controller is created by MEF, then resolve if (controller is System.ComponentModel.Composition.Primitives.Export) { return (IController)((System.ComponentModel.Composition.Primitives.Export)controller).Value; } else if (controller is IController) { return (IController)controller; } } throw new HttpException(404, string.Format("Controller type " + controllerType + " could not be resolved", controllerService, controllerType.FullName, context.HttpContext.Request.Path)); } //otherwise, try to create from the underlying factory return base.GetControllerInstance(context, controllerType); } /// <summary> /// Finds a controller type based on it's name. /// </summary> /// <param name="requestContext"></param> /// <param name="controllerName"></param> /// <returns></returns> /// <remarks> /// This searches using the DefaultControllerFactory's implementation and /// also searches the IoC container for registered types since types that are registered in the container /// may exist outside the bin folder /// </remarks> protected override Type GetControllerType(RequestContext requestContext, string controllerName) { var controllerKeyName = controllerName.ToUpper(); //first, check if this is already in our internal cache if (m_IoCControllerCache.ContainsKey(controllerKeyName)) { return m_IoCControllerCache[controllerKeyName].ComponentType; } //next, try to resolve it from the underlying factory var type = base.GetControllerType(requestContext, controllerName); //if the controller can't be resolved by name using the standard method, then we should check our container if (type == null) { //we need to search the registered components in IoC for a match foreach(var reg in m_Container.ComponentRegistry.Registrations) { foreach (var s in reg.Services) { //match namespaces/classe that then end with "controllerName + Controller" if (Regex.IsMatch(s.Description, @"(?:\w|\.)+\." + controllerName + "Controller$", RegexOptions.Compiled | RegexOptions.IgnoreCase)) { var controller = m_Container.ResolveService(s); var cType = controller.GetType(); //add to the internal cache if (!m_IoCControllerCache.ContainsKey(controllerKeyName)) { lock (m_Locker) { //double check if (!m_IoCControllerCache.ContainsKey(controllerKeyName)) { m_IoCControllerCache.Add(controllerKeyName, new ServiceTypeCache() { Service = s, ComponentType = cType }); } } } return cType; } } } } return type; } } }

Author

Administrator (1)

comments powered by Disqus