80

I am trying to build a MVC4/MVC5 application with a pluggable architecture like Orchard CMS. So I have a MVC application which will be the startup project and take care of auth, navigation etc. Then there will be multiple modules built separately as asp.net class libraries or stripped down mvc projects and have controllers, views, data repos etc.

I have spent all day going through tutorials on the web and downloading samples etc and found that Kenny has the best example around - http://kennytordeur.blogspot.in/2012/08/mef-in-aspnet-mvc-4-and-webapi.html

I am able to import the controllers from the modules(separate DLLs) if I add reference to those DLLs. But the reason behind using MEF is being able to add modules at runtime. I want the DLLs along with views to be copied to a ~/Modules// directory in the startup project (I have managed to do this) and MEF would just pick them up. Struggling to make MEF load these libraries.

There is also MefContrib as explained in this answer ASP.NET MVC 4.0 Controllers and MEF, how to bring these two together? which is the next thing I am about to try. But I'm surprised that MEF doesnt work out of the box with MVC.

Has anyone got a similar architecture working (with or without MefContrib)? Initially I even thought of stripping Orchard CMS and using it as a framework but it is too complex. Also would be nice to develop the app in MVC5 to take advantage of WebAPI2.

Community
  • 1
  • 1
Yashvit
  • 2,337
  • 3
  • 25
  • 32
  • 1
    Have you every got this setup to work with MVC5? I am trying to setup the same thing with MVC 5. Your help is appreciated – Junior May 28 '16 at 17:47
  • 1
    Here is a compete example that has versions implementing both EF and strait ASP.net Seems complete. http://www.codeproject.com/Articles/1109475/Modular-Web-Application-with-ASP-NET-Core – BrownPony Nov 03 '16 at 12:42
  • Why don't more applications use MEF? Everyone seems to roll their own on this one. – johnny May 05 '17 at 19:47

3 Answers3

106

I have worked on a project that had similar pluggable architecture like the one you described and it used the same technologies ASP.NET MVC and MEF. We had a host ASP.NET MVC application that handled the authentication, authorization and all requests. Our plugins(modules) were copied to a sub-folder of it. The plugins also were ASP.NET MVC applications that had its own models, controllers, views, css and js files. These are the steps that we followed to make it work:

Setting up MEF

We created engine based on MEF that discovers all composable parts at application start and creates a catalog of the composable parts. This is a task that is performed only once at application start. The engine needs to discover all pluggable parts, that in our case were located either in the bin folder of the host application or in the Modules(Plugins) folder.

public class Bootstrapper
{
    private static CompositionContainer CompositionContainer;
    private static bool IsLoaded = false;

    public static void Compose(List<string> pluginFolders)
    {
        if (IsLoaded) return;

        var catalog = new AggregateCatalog();

        catalog.Catalogs.Add(new DirectoryCatalog(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "bin")));

        foreach (var plugin in pluginFolders)
        {
            var directoryCatalog = new DirectoryCatalog(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Modules", plugin));
            catalog.Catalogs.Add(directoryCatalog);

        }
        CompositionContainer = new CompositionContainer(catalog);

        CompositionContainer.ComposeParts();
        IsLoaded = true;
    }

    public static T GetInstance<T>(string contractName = null)
    {
        var type = default(T);
        if (CompositionContainer == null) return type;

        if (!string.IsNullOrWhiteSpace(contractName))
            type = CompositionContainer.GetExportedValue<T>(contractName);
        else
            type = CompositionContainer.GetExportedValue<T>();

        return type;
    }
}

This is the sample code of the class that performs discovery of all MEF parts. The Compose method of the class is called from the Application_Start method in the Global.asax.cs file. The code is reduced for the sake of simplicity.

public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        var pluginFolders = new List<string>();

        var plugins = Directory.GetDirectories(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Modules")).ToList();

        plugins.ForEach(s =>
        {
            var di = new DirectoryInfo(s);
            pluginFolders.Add(di.Name);
        });

        AreaRegistration.RegisterAllAreas();
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        Bootstrapper.Compose(pluginFolders);
        ControllerBuilder.Current.SetControllerFactory(new CustomControllerFactory());
        ViewEngines.Engines.Add(new CustomViewEngine(pluginFolders));
    }
}

It is assumed that all plugins are copied in a separate sub-folder of the Modules folder that is located in the root of the host application. Each plugin subfolder contains Views sub-folder and the DLL from each plugin. In the Application_Start method above are also initialized the custom controller factory and the custom view engine which I will define below.

Creating controller factory that reads from MEF

Here is the code for defining custom controller factory which will discover the controller that needs to handle the request:

public class CustomControllerFactory : IControllerFactory
{
    private readonly DefaultControllerFactory _defaultControllerFactory;

    public CustomControllerFactory()
    {
        _defaultControllerFactory = new DefaultControllerFactory();
    }

    public IController CreateController(RequestContext requestContext, string controllerName)
    {
        var controller = Bootstrapper.GetInstance<IController>(controllerName);

        if (controller == null)
            throw new Exception("Controller not found!");

        return controller;
    }

    public SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName)
    {
        return SessionStateBehavior.Default;
    }

    public void ReleaseController(IController controller)
    {
        var disposableController = controller as IDisposable;

        if (disposableController != null)
        {
            disposableController.Dispose();
        }
    }
}

Additionally each controller must be marked with Export attribute:

[Export("Plugin1", typeof(IController))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class Plugin1Controller : Controller
{
    //
    // GET: /Plugin1/
    public ActionResult Index()
    {
        return View();
    }
}

The first parameter of the Export attribute constructor must be unique because it specifies the contract name and uniquely identifies each controller. The PartCreationPolicy must be set to NonShared because controllers cannot be reused for multiple requests.

Creating View Engine that knows to find the views from the plugins

Creation of custom view engine is needed because the view engine by convention looks for views only in the Views folder of the host application. Since the plugins are located in separate Modules folder, we need to tell to the view engine to look there also.

public class CustomViewEngine : RazorViewEngine
{
    private List<string> _plugins = new List<string>();

    public CustomViewEngine(List<string> pluginFolders)
    {
        _plugins = pluginFolders;

        ViewLocationFormats = GetViewLocations();
        MasterLocationFormats = GetMasterLocations();
        PartialViewLocationFormats = GetViewLocations();
    }

    public string[] GetViewLocations()
    {
        var views = new List<string>();
        views.Add("~/Views/{1}/{0}.cshtml");

        _plugins.ForEach(plugin =>
            views.Add("~/Modules/" + plugin + "/Views/{1}/{0}.cshtml")
        );
        return views.ToArray();
    }

    public string[] GetMasterLocations()
    {
        var masterPages = new List<string>();

        masterPages.Add("~/Views/Shared/{0}.cshtml");

        _plugins.ForEach(plugin =>
            masterPages.Add("~/Modules/" + plugin + "/Views/Shared/{0}.cshtml")
        );

        return masterPages.ToArray();
    }
}

Solve the problem with strongly typed views in the plugins

By using only the above code, we couldn't use strongly typed views in our plugins(modules), because models existed outside of the bin folder. To solve this problem follow the following link.

Heretic Monkey
  • 11,687
  • 7
  • 53
  • 122
Ilija Dimov
  • 5,221
  • 7
  • 35
  • 42
  • 1
    how about custom route for each individual module? i think each moduleneed to get ref of routetable and global asax should have route interface in which the route interface will look both in module folder and core. – sharif y Jan 10 '14 at 05:57
  • 3
    We solved that by defining separate area for each plugin. In each plugin we created a class that inherits from AreaRegistration and by overriding the RegisterArea method we were able to define routes that we wanted to use in the plugins. – Ilija Dimov Jan 10 '14 at 06:06
  • Thanks Dimov.. However we decided to go with OrchardCMS as the core for our application. It has dynamic compilation out of the box.. The nightly build is on MVC5 and WepAPI2. Also basic features like ACL, Data Repos, Localization, Caching, Scheduling. Its also one of the most amazing piece of ASP.net application I've seen around with a large community support. – Yashvit Jan 11 '14 at 13:23
  • 10
    Have you got somewhere sample project for this solution? – cpoDesign Mar 04 '14 at 20:25
  • 2
    i agree with cpoDesign. a sample project would be nice – chris vietor Aug 08 '14 at 09:15
  • 2
    I also agree sample project on GitHub would be great to download :) – Kbdavis07 Sep 30 '14 at 10:16
  • Do you have an Admin setting page where you can turn the plugins on and off at run time? – Kbdavis07 Sep 30 '14 at 10:16
  • Can you help with this question? :) http://stackoverflow.com/questions/24971606/asp-net-mvc-5-mef-how-to-programmically-import-and-export-parts – Kbdavis07 Sep 30 '14 at 10:19
  • 1
    do you have a sample project to share? – KKS Dec 01 '14 at 01:03
  • Am I right, with this example you can't change plugins at runtime? – greenhoorn Feb 02 '15 at 08:04
  • I borrowed heavily from this answer to build a simple prototype: https://github.com/FNCSoftware/SharedWebComponentsMef – kmkemp Mar 18 '15 at 15:39
  • @IlijaDimov is there a way how to disable this behavior for specific controllers? ( I have a signalr implementation that does not get resolved as is in dll) ?? i thought Add it as ignore route should work, but It needs to be processed which does not happen. – cpoDesign Aug 18 '15 at 21:10
  • @IlijaDimov or anyone, could you explain how I would be able to retain the plugin's layout as well? – thestephenstanton Sep 25 '15 at 15:05
  • 1
    Just be aware that MEF's container has a "nice feature" that keeps references to any IDisposable object it creates, and will lead to huge memory leak. Allegedly the memory leak can be addressed with this nuget - https://www.nuget.org/packages/NCode.Composition.DisposableParts.Signed/ – Aleksandar Mar 23 '16 at 07:02
  • @IlijaDimov would you mind showing some code on how you coded you RegisterArea to convert to different url? for now, I have to use the attribute name for the controller in my URL to get to the controller. I would like to be able to do URL/{pluggingName}/{controller}/{action}/{id} instead of URL/{exportName} – Junior May 28 '16 at 22:31
4

Just be aware that MEF's container has a "nice feature" that keeps references to any IDisposable object it creates, and will lead to huge memory leak. Allegedly the memory leak can be addressed with this nuget - http://nuget.org/packages/NCode.Composition.DisposableParts.Signed

Aleksandar
  • 1,341
  • 1
  • 12
  • 17
3

There are projects out there that implement a plugin architecture. You might want to use one of these or to have a look at their source code to see how they accomplish these things:

Also, 404 on Controllers in External Assemblies is taking an interesting approach. I learned a lot by just reading the question.

Community
  • 1
  • 1
Dejan
  • 9,150
  • 8
  • 69
  • 117