Enabling Add-In functionality in ASP.NET MVC 3

August 08, 2011 | ASP.NET MVC

Update:  Part 2

I remember, back in 2006 when I wrote my first managed add-in for AutoCAD. The fact that we could extend the functionality of a very big product, using .NET was huge. Till that time, if we wanted to use .NET for add-in functionality we had to rely on RCW or else we had to write messy and error-prone VBA code. 

Today, anyone who builds applications in managed code (using .NET 4 and above) has built-in functionality for extensibility provided by the framework itself. In this post, we will be extending an ASP.NET MVC 3 application. We are going to use Unity as the Dependency Injection (DI) container and the types from the System.ComponentModel.Composition namespace (or else, MEF) for managing the composition of parts.

The host application, is the one shown below. I have selected the interesting types that I will be discussing.

The types that I will be discussing

DiscoverableControllerFactory

A MEF-specific DefaultControllerFactory derived type. It gets the exported types with the contract name, derived from an IController type. After the controller is supplied, the MVC framework will resolve the Views.

internal sealed class DiscoverableControllerFactory : DefaultControllerFactory
{
    private readonly CompositionContainer compositionContainer;

    public DiscoverableControllerFactory(
        CompositionContainer compositionContainer)
    {
        this.compositionContainer = compositionContainer;
    }

    public override IController CreateController(
        RequestContext requestContext, 
        string controllerName)
    {
        Lazy<IController> controller = this.compositionContainer
          .GetExports<IController, IDictionary<string, object>>()
          .Where(c => c.Metadata.ContainsKey("controllerName")
                   && c.Metadata["controllerName"].ToString() == controllerName)
          .First();

        return controller.Value;
    }
}

UnityControllerFactory

A Unity-specific DefaultControllerFactory  derived type. There are many implementations around. The difference from other implementations is that this one takes a delegate as a parameter in the constructor that acts as the fallback factory when the DI container can not supply a controller. This is a very important part of our architecture because here we have the chance to supply the target controller (as an add-in) using MEF.

internal sealed class UnityControllerFactory : DefaultControllerFactory
{
    private readonly UnityContainer container;
    private Func<RequestContext, string, IController> alternativeFactoryMethod;

    public UnityControllerFactory(
        UnityContainer container,
        Func<RequestContext, string, IController> alternativeFactoryMethod)
    {
        this.container = container;
        this.alternativeFactoryMethod = alternativeFactoryMethod;
    }

    protected override IController GetControllerInstance(
        RequestContext requestContext, 
        Type controllerType)
    {
        IController controller;

        if (controllerType == null)
        {
            try
            {
                string controllerName = requestContext.HttpContext
                    .Request.Path.Replace("/", "");
                return this.alternativeFactoryMethod(
                    requestContext, 
                    controllerName);
            }
            catch
            {
                throw new HttpException(404, string.Format(
                    "The controller for path '{0}' could not be found or it 
                        does not implement IController.",
                    requestContext.HttpContext.Request.Path));
            }
        }

        if (!typeof(IController).IsAssignableFrom(controllerType))
        {
            throw new ArgumentException(string.Format(
                "Type requested is not a controller: {0}", controllerType.Name),
                 "controllerType");
        }

        try
        {
            controller = container.Resolve(controllerType) as IController;
        }
        catch (Exception e)
        {
            throw new InvalidOperationException(string.Format(
                "Error resolving controller {0}", controllerType.Name), e);
        }

        return controller;
    }
}

Global.asax

Here we specify the default path for the extensions. We create a new instance of the DiscoverableControllerFactory class passing a CompositionContainer and a DirectoryCatalog. Keep in mind that the DirectoryCatalog is one of the many choices that MEF provides for discovering parts. Besides the creation of the DiscoverableControllerFactory we also create a new instance of the UnityControllerFactory class acting as the default controller factory. Any controllers that this factory can not supply will fallback to the DiscoverableControllerFactory using it's CreateController method. One last thing to note, this is the application's Composition Root. The DI container is referenced here, where the composition happens, and nowhere else in the entire application.

private static void BootstrapContainer()
{
    string extensionsPath = Path.Combine(
        AppDomain.CurrentDomain.BaseDirectory, "Extensions");

    var discoverableControllerFactory = new DiscoverableControllerFactory(
        new CompositionContainer(
            new DirectoryCatalog(extensionsPath)));

    // No direct reference on the container outside this method.
    var unityControllerFactory = new UnityControllerFactory(
        new UnityContainer()
            .Install(Registrator.ForControllers,
                     Registrator.ForServices,
                     Registrator.ForEnterpriseLibrary),
        fallbackFactoryMethod: discoverableControllerFactory.CreateController);

    ControllerBuilder.Current.SetControllerFactory(unityControllerFactory);
}

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();

    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);

    BootstrapContainer();
}

The add-in application is a regular class library and it's structure is shown below. I have selected the interesting types that I will be discussing.

The types that I will be discussing

ConceptController

This is a proof of concept Controller for this demo. It is decorated with the ExportAttribute and ExportMetadataAttribute. The later is needed in order to help the DiscoverableControllerFactory to choose the right controller among all the controllers supplied by this and other add-ins. The PartCreationPolicyAttribute is needed in order to specify that a new non-shared (transient) instance will be created for each request.

[Export(typeof(IController)), ExportMetadata("controllerName", "Concept")]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class ConceptController : Controller
{
    public ActionResult Index()
    {
        ViewBag.Name = this.GetType().Assembly.FullName;

        return View("~/Extensions/Views/Concept/Index.cshtml");
    }
}

Index.cshtml, Web.config

Nothing special to say here. The razor view is just any other (razor) view. The Web.config is needed as a hint for the MVC framework to compile the razor views at runtime.

Make sure to select all the views and set the property “Copy to Output directory” to “Copy if newer”. This is important because each time we compile the add-in library besides the .dll with the models and the controllers we also want the views to be copied there (they are also part of the add-in).

You can clone the code here. Upon build the Concepts.dll along with it's Views will be copied in the Web project's “Extensions” directory. When run, the application will automatically load the assembly the first time the “Concepts” tab is pressed.