Wednesday, October 06, 2010

Experimental ASP.NET MVC Add-ins

Updated: Please read the new post here.

Get the source code.

I’ve been thinking recently about how to provide an ‘add-in’ framework for Suteki Shop. The idea is that you could ‘drop in’ additional functionality without changing the core system. A good example of this would be payment providers, so if I wanted to have a PayPal payment provider I could simply write that as a separate assembly and then simply drop it into the bin directory of the website and it would just work. Suteki Shop is build around the Windsor IoC container, so it is already designed as a collection of components. What I needed was a way for add-in assemblies to be discovered and some mechanism to allow them to register their components in the container.

I found this excellent post by Hammett where he builds a composable website using MEF. I took this as my inspiration and tweaked it so that MEF is only used for the initial assembly discovery and installation.

Here’s a snippet of my Global.asax.cs file:

public class MvcApplication : HttpApplication, IContainerAccessor { ... protected void Application_Start() { HostingEnvironment.RegisterVirtualPathProvider(new AssemblyResourceVirtualPathProvider()); RegisterRoutes(RouteTable.Routes); InitializeWindsor(); InitializeAddins(); }

void InitializeAddins() { using (var mefContainer = new CompositionContainer(new DirectoryCatalog(HttpRuntime.BinDirectory, "*AddIn.dll"))) { var lazyInstallers = mefContainer.GetExports<IAddinInstaller>(); foreach (var lazyInstaller in lazyInstallers) { var installer = lazyInstaller.Value; Container.Install(new CommonComponentInstaller(installer.GetType().Assembly)); installer.DoRegistration(Container); } } } void InitializeWindsor() { container = new WindsorContainer() .Install(new CoreComponentsInstaller()) .Install(new AddinInstaller(Assembly.GetExecutingAssembly())); var controllerFactory = Container.Resolve<IControllerFactory>(); ControllerBuilder.Current.SetControllerFactory(controllerFactory); } static IWindsorContainer container; public IWindsorContainer Container { get { return container; } } }

In the InitializeWindsor method we create a new windsor container and do the registration for the core application, this is standard stuff that I’ve put into every MVC/Monorail based app I’ve written in the last 3 years. The InitializeAddins method is where the MEF magic happens. We create a new directory catalogue and look for any assembly in the bin directory that ends with ‘AddIn.dll’. Each add-in must include an implementation of IAddinInstaller attributed as a MEF export. We then do some common windsor registration with an AddinInstaller and allow the add-in itself to do any additional custom registration by calling its DoRegistration method. Note that we only keep the MEF CompositionContainer long enough to set things up, after that it’s disposed.

Note that we also register a custom virtual path provider ‘AssemblyResourceVirtualPathProvider’ so that we can load views that are embedded in assemblies as resource files rather than from the file system. It’s described in this Code Project article.

Here’s a typical implementation of IAddinInstaller, note the MEF export attributes:

[Export(typeof(IAddinInstaller)), PartCreationPolicy(CreationPolicy.NonShared)]
public class Installer : IAddinInstaller
{
    public void DoRegistration(IWindsorContainer container)
    {
        // do any additional registration
    }
}

The CommonComponentInstaller does registration for the components that are common for all add-ins. In this case, controllers and INavigation implementations:

public class CommonComponentInstaller : IWindsorInstaller
{
    readonly Assembly assembly;

    public CommonComponentInstaller(Assembly assembly)
    {
        this.assembly = assembly;
    }

    public void Install(IWindsorContainer container, IConfigurationStore store)
    {
        container.Register(
                AllTypes
                    .FromAssembly(assembly)
                    .BasedOn<IController>()
                    .WithService.Base()
                    .Configure(c => c.LifeStyle.Transient.Named(c.Implementation.Name.ToLower())),
                AllTypes
                    .FromAssembly(assembly)
                    .BasedOn<INavigation>()
                    .WithService.Base()
                    .Configure(c => c.LifeStyle.Transient.Named(c.Implementation.Name.ToLower()))
            );
    }
}

INavigation is just a simple way to provide an extensible menu. Any add-in that wants to add a menu item simply implements it:

public class CustomerNavigation : INavigation
{
    public string Text
    {
        get { return "Customers"; }
    }

    public string Action
    {
        get { return "Index"; }
    }

    public string Controller
    {
        get { return "Customer"; }
    }
}
 
The navigation controller simply has a dependency on all INavigation implementations, and passes them to its view:
 
public class NavigationController : Controller
{
    readonly INavigation[] navigations;

    public NavigationController(INavigation[] navigations)
    {
        this.navigations = navigations;
    }

    public ViewResult Index()
    {
        return View("Index", navigations);
    }
}

Now the ‘Customers’ menu appears as expected:

MefAreas

An add-in itself is just a regular C# library project:

mefaddin_2

Views need to be marked as Embedded Resources rather than Content, and controllers should inherit AddinControllerBase rather than the standard Controller, but other than that they are laid-out and behave just like a regular MVC project.

So far this is working nicely. I should probably also take a look at the MVCContrib portable areas as well.

The complete example is on github: http://github.com/mikehadlow/MefAreas

6 comments:

Anonymous said...

Nice. How this compares to the PortableAreas Project in MVCContrib?

Troy Goode said...

Seems similar in concept to Turbine:

http://mvcturbine.codeplex.com/

Mike Hadlow said...

Anonymous,

Portable Areas is probably most similar to this, although I haven't really looked at it yet. Note that my implementation doesn't make any use at all of MVC areas.

Troy,

Isn't turbine more a general IoC framework for ASP.NET MVC rather than something that addresses add-ins?

Frank said...

This is similar to some of what MVC Turbine does, I like this example though as its short and clear.

When I have time I need to look into what you've done with the views. I didn't know you could externalize them to a separate DLL. Is it just a matter of having the right directory structure and including them as embedded resources?

Javier G. Lozano said...

I love this type approach to MVC applications! This is how MVC Turbine was build, as a composition engine for MVC by using the "Blade" concept.

Frank brings up an excellent point that this is way simpler to grok than Turbine so people get their "a ha!" sooner.

If want to chat about composition and MVC, hit me up through twitter sometime, @jglozano.

Javier G. Lozano said...

Rats, almost forgot. Here's a screencast that @darrencauthon, a committer on Turbine, did leveraging the same embedded resource concept with the Nerd Dinner sample:

http://www.youtube.com/watch?v=9D7V65DGQWM