MEF with MVC 4 or 5 - Pluggable Architecture (2014)
Asked Answered
C

3

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.

Curtiscurtiss answered 9/1, 2014 at 10:4 Comment(3)
Have you every got this setup to work with MVC5? I am trying to setup the same thing with MVC 5. Your help is appreciatedTrigger
Here is a compete example that has versions implementing both EF and strait ASP.net Seems complete. codeproject.com/Articles/1109475/…Burg
Why don't more applications use MEF? Everyone seems to roll their own on this one.Beliabelial
S
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.

Silhouette answered 10/1, 2014 at 5:49 Comment(15)
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.Claritaclarity
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.Silhouette
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.Curtiscurtiss
Have you got somewhere sample project for this solution?Involutional
i agree with cpoDesign. a sample project would be niceIntrovert
I also agree sample project on GitHub would be great to download :)Satirist
Do you have an Admin setting page where you can turn the plugins on and off at run time?Satirist
Can you help with this question? :) #24972106Satirist
do you have a sample project to share?Colophon
Am I right, with this example you can't change plugins at runtime?Sneed
I borrowed heavily from this answer to build a simple prototype: github.com/FNCSoftware/SharedWebComponentsMefPoundage
@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.Involutional
@IlijaDimov or anyone, could you explain how I would be able to retain the plugin's layout as well?Phthisic
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 - nuget.org/packages/NCode.Composition.DisposableParts.SignedGabie
@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}Trigger
G
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

Gabie answered 23/3, 2016 at 7:55 Comment(1)
Another reason to say DryIoc is better :)Driskill
L
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.

Laboy answered 20/10, 2015 at 14:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.