MVC4 MEF-based dynamically loaded plugins
Asked Answered
D

1

18

updated: read below in this post for a minimal solution

I have some newbie questions about an MVC4 solution with plugins. I googled around a bit and found some good stuff, but it does not exactly fit my requirements, so I'm asking here for some advice.

It seems that the best solution for widget-like plugins in MVC is portable areas (in the MvcContrib package). I found the basic guidance here:

http://lostechies.com/erichexter/2009/11/01/asp-net-mvc-portable-areas-via-mvccontrib/

and some useful tips here:

http://geekswithblogs.net/michelotti/archive/2010/04/05/mvc-portable-areas-ndash-web-application-projects.aspx

More stuff in this post:

How to create ASP.NET MVC area as a plugin DLL?

That's all cool but sadly my requirements are a bit different:

  1. unfortunately, I need a system where plugins are added and discovered dynamically, and this is not the case with portable areas, which must be referenced by the main MVC site project. I'd like to just upload something to the site and get it discover and use new components, so I'm going to use MEF for this.

  2. fortunately, my plugins will not be like widgets, which might be very complex and heterogeneous; rather, they are components which must follow a common, shared pattern. Think of them like specialized editors: for each data type I'll offer a component with editing functions: new, edit, delete. So I was thinking of plugin-controllers which implement a common interface and provide actions like New, Edit, Delete and the like.

  3. I must use MVC4 and in the future I'll have to add localization and mobile customizations.

  4. I must avoid dependencies from complex frameworks and keep the code as simple as possible.

So, whenever I want to add a new data type for editing in this website I'd just like to drop a DLL in its plugins folder for the logic stuff (controller etc), and some views in the correct locations, to get the site discover and use the new editor.

Eventually I could include the views in the DLL itself (I found this: http://razorgenerator.codeplex.com , and this tutorial: http://www.chrisvandesteeg.nl/2010/11/22/embedding-pre-compiled-razor-views-in-your-dll/, which I suppose I could use with the codeplex razorgenerator as the code it refers to is not compatible with VS2012), but probably I'll have better keep them separated (also because of the localization and mobile-awareness requirements); I was thinking of adding an upload mechanism to my site admin area, where you can upload a single zip with the DLL with controllers and folders with views, and then let the server unzip and store files where required. This would allow me to easily modify views without having to deploy again the whole add-in.

So I started looking for MEF and MVC, but most of the posts refer to MVC2 and are not compatible. I had better luck with this, which is mainly focused on web API, but looks promising and simple enough:

http://kennytordeur.blogspot.it/2012/08/mef-in-aspnet-mvc-4-and-webapi.html

This essentially adds a MEF-based dependency resolver and controller factory to the "standard" MVC application. Anyway the sample in the post refers to a single-assembly solution, while I need to deploy several different plugins. So I slightly modified the code to use a MEF DirectoryCatalog (rather than an AssemblyCatalog) pointing to my plugins folder and then created a test MVC solution, with a single plugin in a class library.

Anyway, when I try loading the plugin controller the framework calls my factory GetControllerInstance with a null type, so that of course MEF cannot proceed to composition. Probably I'm missing something obvious, but I'm new to MVC 4 and any suggestion or useful (MVC4-compliant) link are welcome. Thanks!

Here is the essential code:

public static class MefConfig
{
    public static void RegisterMef()
    {
        CompositionContainer container = ConfigureContainer();

        ControllerBuilder.Current.SetControllerFactory(new MefControllerFactory(container));

        System.Web.Http.GlobalConfiguration.Configuration.DependencyResolver =
            new MefDependencyResolver(container);
    }

    private static CompositionContainer ConfigureContainer()
    {
        //AssemblyCatalog assemblyCatalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());

        DirectoryCatalog catalog = new DirectoryCatalog(
            HostingEnvironment.MapPath("~/Plugins"));
        CompositionContainer container = new CompositionContainer(catalog);
        return container;
    }
}

public class MefDependencyResolver : IDependencyResolver
{
    private readonly CompositionContainer _container;

    public MefDependencyResolver(CompositionContainer container)
    {
        _container = container;
    }

    public IDependencyScope BeginScope()
    {
        return this;
    }

    public object GetService(Type serviceType)
    {
        var export = _container.GetExports(serviceType, null, null).SingleOrDefault();
        return (export != null ? export.Value : null);
    }

    public IEnumerable GetServices(Type serviceType)
    {
        var exports = _container.GetExports(serviceType, null, null);
        List createdObjects = new List();

        if (exports.Any())
            createdObjects.AddRange(exports.Select(export => export.Value));

        return createdObjects;
    }

    public void Dispose()
    {
    }
}

public class MefControllerFactory : DefaultControllerFactory
{
    private readonly CompositionContainer _compositionContainer;

    public MefControllerFactory(CompositionContainer compositionContainer)
    {
        _compositionContainer = compositionContainer;
    }

    protected override IController GetControllerInstance(
        System.Web.Routing.RequestContext requestContext, Type controllerType)
    {
        if (controllerType == null) throw new ArgumentNullException("controllerType");
        var export = _compositionContainer.GetExports(controllerType, null, null).SingleOrDefault();

        IController result;

        if (null != export) result = export.Value as IController;
        else
        {
            result = base.GetControllerInstance(requestContext, controllerType);
            _compositionContainer.ComposeParts(result);
        } //eelse

        return result;
    }
}

You can download the full test solution from here:

http://www.filedropper.com/mvcplugins

Edit: a first working minimal solution

Here are my findings, hope they can be useful for some other newbie starting with this stuff: I did not manage to succesfully run the framework quoted in the above reply, I suppose there must be something to be updated for VS2012 and MVC4. Anyway, I looked at the code and googled a bit more:

1) first of all, a source of confusion for me were the 2 different interfaces with the same name: IDependencyResolver. If I understand well, one (System.Web.Http.Dependencies.IDependencyResolver) is used for webapi, and another (System.Web.Mvc.IDependencyResolver) for generic DI. This post helped me here: http://lucid-nonsense.co.uk/dependency-injection-web-api-and-mvc-4-rc/.

2) also, a third component is the DefaultControllerFactory-derived controller factory, which is crucial to this post because it is the factory used for plugin-hosted controllers.

Here are my implementations for all these, slightly modified from several samples: first the HTTP resolver:

public sealed class MefHttpDependencyResolver : IDependencyResolver
{
    private readonly CompositionContainer _container;

    public MefHttpDependencyResolver(CompositionContainer container)
    {
        if (container == null) throw new ArgumentNullException("container");
        _container = container;
    }

    public object GetService(Type serviceType)
    {
        if (serviceType == null) throw new ArgumentNullException("serviceType");

        string name = AttributedModelServices.GetContractName(serviceType);

        try
        {
            return _container.GetExportedValue(name);
        }
        catch
        {
            return null;
        }
    }

    public IEnumerable GetServices(Type serviceType)
    {
        if (serviceType == null) throw new ArgumentNullException("serviceType");

        string name = AttributedModelServices.GetContractName(serviceType);

        try
        {
            return _container.GetExportedValues(name);
        }
        catch
        {
            return null;
        }
    }

    public IDependencyScope BeginScope()
    {
        return this;
    }

    public void Dispose()
    {
    }
}

Then the MVC resolver, which is very similar, even if strictly not necessary for the dummy sample in this scenario:

public class MefDependencyResolver : IDependencyResolver
{
    private readonly CompositionContainer _container;

    public MefDependencyResolver(CompositionContainer container)
    {
        if (container == null) throw new ArgumentNullException("container");
        _container = container;
    }

    public object GetService(Type type)
    {
        if (type == null) throw new ArgumentNullException("type");

        string name = AttributedModelServices.GetContractName(type);

        try
        {
            return _container.GetExportedValue(name);
        }
        catch
        {
            return null;
        }
    }

    public IEnumerable GetServices(Type type)
    {
        if (type == null) throw new ArgumentNullException("type");

        string name = AttributedModelServices.GetContractName(type);

        try
        {
            return _container.GetExportedValues(name);
        }
        catch
        {
            return null;
        }
    }
}

And finally the controller factory:

[Export(typeof(IControllerFactory))]
public class MefControllerFactory : DefaultControllerFactory
{
    private readonly CompositionContainer _container;

    [ImportingConstructor]
    public MefControllerFactory(CompositionContainer container)
    {
        if (container == null) throw new ArgumentNullException("container");
        _container = container;
    }

    public override IController CreateController(RequestContext requestContext, string controllerName)
    {
        var controller = _container
            .GetExports()
            .Where(c => c.Metadata.Name.Equals(controllerName, StringComparison.OrdinalIgnoreCase))
            .Select(c => c.Value)
            .FirstOrDefault();

        return controller ?? base.CreateController(requestContext, controllerName);
    }
}

As for the sample controller, I created it into a class library project:

[Export(typeof(IController))]
[PartCreationPolicy(CreationPolicy.NonShared)]
[ExportMetadata("Name", "Alpha")]
public sealed class AlphaController : Controller
{
    public ActionResult Index()
    {
        ViewBag.Message = "Hello, this is the PLUGIN controller!";

        return View();
    }
}

In the main project (the MVC site) I have a Plugins folder where I copy this DLL, plus a "standard" set of views in their folders for this controller's views.

This is the simplest possible scenario, and probably there is much more to find out and refine, but I needed to be simple to start with. Anyway, any suggestion is welcome.

Dolan answered 19/9, 2012 at 9:26 Comment(2)
I think you might be looking at something which I have figured out a solution. Basically I'm working on a MEFified MVC 4/web api app. Feel free to take a look at this: #16627137 I hope it helps.Consciencestricken
Community bumped your question, probably because it doesn't have an accepted answer. You should remove your solution from your question and post it as an answer instead (and then accept it).Deadhead
M
2

I'm currently working on the same issue. I've found this solution:

Basically it loads assemblies from specified location and with some name pattern on web application startup:

AssemblyInfo.cs:

[assembly: PreApplicationStartMethod(
  typeof(PluginAreaBootstrapper), "Init")]

PluginAreaBootstrapper.cs:

public class PluginAreaBootstrapper
{
    public static readonly List<Assembly> PluginAssemblies = new List<Assembly>();

    public static List<string> PluginNames()
    {
        return PluginAssemblies.Select(
            pluginAssembly => pluginAssembly.GetName().Name)
            .ToList();
    }

    public static void Init()
    {
        var fullPluginPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Areas");

        foreach (var file in Directory.EnumerateFiles(fullPluginPath, "*Plugin*.dll", SearchOption.AllDirectories))
            PluginAssemblies.Add(Assembly.LoadFile(file));

        PluginAssemblies.ForEach(BuildManager.AddReferencedAssembly);

        // Add assembly handler for strongly-typed view models
        AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolve;
    }

    private static Assembly AssemblyResolve(object sender, ResolveEventArgs resolveArgs)
    {
        var currentAssemblies = AppDomain.CurrentDomain.GetAssemblies();
        // Check we don't already have the assembly loaded
        foreach (var assembly in currentAssemblies)
        {
            if (assembly.FullName == resolveArgs.Name || assembly.GetName().Name == resolveArgs.Name)
            {
                return assembly;
            }
        }

        return null;
    }
}

But I believe you can create some directory observer which can dynamically load assemblies, so you don't even need to restart your web application.

In my opinion it meets your 1, 2 and 4 needs. It's very simple, doesn't require any frameworks, has minimal configuration, allows dynamic loading of the plugins and works with MVC 4.

This solution plugs assemblies into Area directory, but I believe you can quite easily tune it to play as you like using routing.

Mortonmortuary answered 12/2, 2015 at 19:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.