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

后端 未结 3 1053
遇见更好的自我
遇见更好的自我 2020-11-30 16:15

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 a

相关标签:
3条回答
  • 2020-11-30 16:58

    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.

    0 讨论(0)
  • 2020-11-30 16:59

    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

    0 讨论(0)
  • 2020-11-30 17:02

    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:

    • ASP.NET MVC Plugin Framework (using MVC 4)
    • .NET 4.0 ASP.NET MVC 3 plug-in architecture with embedded views (obviously using MVC 3 but fundamental principles might still apply)

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

    0 讨论(0)
提交回复
热议问题