I’ve recently been introduced to the Managed Extensibility Framework by a colleague at work, and have enjoyed picking it up so much it made me wonder how I could apply the simplicity of composition into my favourite web framework, ASP.NET MVC. There are a few obstacles to overcome initially, namely how instances are composed by default, and how the MVC system handles custom controllers. The idea behind this project, is to enable a Controller to be developed and deployed in it’s own assembly, with the ability to drop it into a project as an extension. Thus, as is the nature of MEF, this was down to exports and imports. MVC allows us to customise how controllers are created by implementing our own controller factory, so we can use this as well.

This initial problem with how MEF would work with MVC controllers, is that a controller is created when it is needed, not composed (and instantiated) straight away. In MEF preview 9, a great example project exists called DynamicInstantiation. In said project they’ve created a custom export provider and types, the PartCreator types. Using this method they can get the benefits of automatic composure with the added extra of dynamic instantiation. Basically, they can create instances of their dynamicly imported types, as and when they chose. This seemed pretty perfect for what I wanted to do with MVC.

I’ve taken that example project, fleshed out the code with comments and renamed the PartCreator types as PartFactory (that’s just my personal preference).

To get started with the project itself, I looked at the requirement for a controller. I’ve created a class library solely to house the custom controllers TestController. I’ve used [Export(typeof(IController))] to mark my controller for export, and also [ExportMetadata("ControllerName", "Test")] to mark my export with my desired metadata. This metadata will come into play later when we are instantiating an instance of our controller. I don’t need to define my own controller contract, as we can use the IController interface as our export definition.

namespace MEFLearning.MVC
{
    using System.ComponentModel.Composition;
    using System.Web.Mvc;

    /// <summary>
    /// Defines an importable controller.
    /// </summary>
    [Export(typeof(IController)), ExportMetadata("ControllerName", "Test")]
    public class TestController : Controller
    {
        #region Actions
        /// <summary>
        /// The default action for the controller.
        /// </summary>
        /// <remarks>
        /// As this is just an example, we'll redirect the user back to the homepage.
        /// </remarks>
        public ActionResult Index()
        {
            return RedirectToAction("Index", "Home");
        }
        #endregion
    }
}

Using the DynamicInstantiationExportProvider, we can create imports of PartFactory<IController, IControllerMetadata>, instead of imports of IController. This is important, as when we compose whatever type is required, we don’t want it to automatically create instances of our controller. This only need happen when a request is made. To this end, I’ve created a new IControllerFactory, which allows us to create an instance of the controller only when it is required:

namespace MEFLearning.Web
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.Composition;
    using System.Linq;
    using System.Web.Mvc;

    using MEFLearning.Web.ComponentModel;

    /// <summary>
    /// Defines a controller factory that supports importing of controllers.
    /// </summary>
    [Export]
    public class ImportControllerFactory : DefaultControllerFactory
    {
        #region Properties
        /// <summary>
        /// Gets or sets the collection of controller part factories.
        /// </summary>
        [ImportMany]
        public IEnumerable<PartFactory<IController, IControllerMetadata>> PartFactories { get; set; }
        #endregion

        #region Methods
        /// <summary>
        /// Creates an instance a controller with the specified controller name.
        /// </summary>
        /// <param name="requestContext">The context of the request.</param>
        /// <param name="controllerName">The name of the contorller requested.</param>
        /// <returns>An instance a controller with the specified controller name.</returns>
        public override IController CreateController(System.Web.Routing.RequestContext requestContext, string controllerName)
        {
            IController controller = null;

            if (PartFactories != null) {
                // Determine if a factory is available for our controller.
                var factory = PartFactories
                    .Where(f => (f.Metadata != null
                                 && string.Equals(f.Metadata.ControllerName, controllerName,
                                                  StringComparison.InvariantCultureIgnoreCase)))
                    .FirstOrDefault();

                // Create an instance of the controller.
                if (factory != null)
                    controller = factory.CreatePart();
            }

            // If no imported controller could be found, use the default factory to create one where available.
            return controller ?? base.CreateController(requestContext, controllerName);
        }
        #endregion
    }
}

When the CreateController method is called by the MVC ControllerBuilder, we need to interrogate our PartFactories enumerable to find the available imported controller. One of the nice touches with how the imports are handled, is that the metadata we decorated on our TestController ([ExportMetadata("ControllerName", "Test")]) is projected as an instance of our interface, IControllerMetadata:

namespace MEFLearning.Web
{
    /// <summary>
    /// Defines the interface for a controller metdata view.
    /// </summary>
    public interface IControllerMetadata
    {
        #region Properties
        /// <summary>
        /// Gets the controller name.
        /// </summary>
        string ControllerName { get; }
        #endregion
    }
}

In vanilla-MVC, the controller is selected based on it’s name, minus the “Controller” suffix. Unfortunately, this wouldn’t work well with this solution, as the imported part type is IController, not TestController, and we can’t do a GetType() call on the instance, as we would have to create an instance ahead of time to do that. This is the design decision around using a metadata view. If the exported controller doesn’t have a ControllerName metadata attribute, it won’t successfully be instantiated. Small sacrifice, but I think it’s worth it.

I’ve used the DefaultControllerFactory type as a base, as it will allow us to create instances of controllers hosted in the main web project. For instance, we can keep the default HomeController and AccountController types in the website itself, the above implementation will still allow us to use them.

Routing
Controllers are pretty much useless without routes, and in light of the fact that MVC2 has the concept of AreaRegistration, I thought it would be a nice idea to have our routing handled by the MEF composition also. To this end, I’ve created a new contract, the IRouteRegistrar, which has two methods, RegisterIgnoreRoutes() and RegisterRoutes(). Handling these seperately allows multiple registrars to register their routes in a seperate fashion, this may be important due to the linear nature in which routes are selected. My contract is as follows:

namespace MEFLearning.MVC
{
    using System.Web.Routing;

    /// <summary>
    /// Defines the required contract for implemting a route registrar.
    /// </summary>
    public interface IRouteRegistrar
    {
        #region Methods
        /// <summary>
        /// Registers any routes to be ignored.
        /// </summary>
        /// <param name="routes">The collection of routes to add to.</param>
        void RegisterIgnoreRoutes(RouteCollection routes);

        /// <summary>
        /// Registers any routes to be processed.
        /// </summary>
        /// <param name="routes">The collection of routes to add to.</param>
        void RegisterRoutes(RouteCollection routes);
        #endregion
    }
}

We can take advantage of this, by having our default rules be implemented as a registrar:

namespace MEFLearning.Web
{
    using System.ComponentModel.Composition;
    using System.Web.Mvc;
    using System.Web.Routing;

    using MEFLearning.MVC;

    /// <summary>
    /// Registers the default required routes.
    /// </summary>
    [Export(typeof(IRouteRegistrar))]
    public class DefaultRouteRegistrar : IRouteRegistrar
    {
        #region Methods
        /// <summary>
        /// Registers any routes to be ignored.
        /// </summary>
        /// <param name="routes">The collection of routes to add to.</param>
        public void RegisterIgnoreRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
            routes.IgnoreRoute("{resource}.aspx/{*pathInfo}");
            routes.IgnoreRoute("{resource}.ico/{*pathInfo}");
        }

        /// <summary>
        /// Registers any routes to be processed.
        /// </summary>
        /// <param name="routes">The collection of routes to add to.</param>
        public void RegisterRoutes(RouteCollection routes)
        {
            routes.MapRoute("Default", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = "" });
        }
        #endregion
    }
}

The DefaultRouteRegistrar is in the main website project, but having the ability to export our local types means it now fits nicely into our composition. For this example, I haven’t bothered creating any routes for the TestController, as the default route will handle it for now. I have created a TestRouteRegistrar stub which can be fleshed out when more complex routes are required.

Joing it all together
Originally I toyed with having a seperate host class that we could reference from our Global application class. In the end though, it’s perfectly viable to just use the MvcApplication type thats generated with our project as our composable type. Firstly, lets look at how we are composing our application:

#region Properties
/// <summary>
/// Gets or sets the collection of route registrars.
/// </summary>
[ImportMany]
public IEnumerable<IRouteRegistrar> RouteRegistrars { get; set; }

/// <summary>
/// Gets or sets the controller factory used for imported controllers.
/// </summary>
[Import]
public ImportControllerFactory ControllerFactory { get; private set; }
#endregion

We decorate two properties here. We need our route registrars composed and available straight away, as with our custom controller factory. This all fits togther with our Application_Start method:

protected void Application_Start()
{
    // Create our part catalog and export provider.
    var catalog = new AggregateCatalog(
        new DirectoryCatalog(Server.MapPath("~/bin")),
        new DirectoryCatalog(Server.MapPath("~/bin/Imports")));
    var exportProvider = new DynamicInstantiationExportProvider();

    // Create the container used to compose parts.
    var container = new CompositionContainer(catalog, exportProvider);
    exportProvider.SourceProvider = container;

    // Compose our application.
    container.ComposeParts(this);

    // Set the controller builder to use our custom controller factory.
    ControllerBuilder.Current.SetControllerFactory(ControllerFactory);

    // Register any available routes.
    RegisterRoutes();
}

The nuts and bolts of it are a single AggregateCatalog which itself is composed of two DirectCatalog instances, one for the main application types (“~/bin”), and our for our imported types (“~/bin/Imports”). Not sure if it’s just me, but I was hoping the DirectoryCatalog would handle subdirectories (have I missed that)?

We create an instance of our DynamicInstantiationExportProvider which will allow the composition of PartFactory<> instances. Once thats all be composed, we can assign our ImportControllerFactory to the ControllerBuilder and register our routes:

/// <summary>
/// Registers any required routes.
/// </summary>
public void RegisterRoutes()
{
    RouteCollection routes = RouteTable.Routes;

    // Register our ignore routes.
    foreach (var registrar in RouteRegistrars)
        registrar.RegisterIgnoreRoutes(routes);

    // Register our processed routes.
    foreach (var registrar in RouteRegistrars)
        registrar.RegisterRoutes(routes);
}

Thats pretty much it, run the project and visit the /Test url, if all goes to plan it should redirect you back to the homepage. We could have done something a bit fancier, but I think this is a good starting point to a modular MVC project.

The project is attached for your viewing, let me know what you think.

MEF-MVCControllers.rar

Digg This
Reddit This
Stumble Now!
Buzz This
Vote on DZone
Share on Facebook
Bookmark this on Delicious
Kick It on DotNetKicks.com
Shout it
Share on LinkedIn
Bookmark this on Technorati
Post on Twitter
Google Buzz (aka. Google Reader)