基于 MVC4 MEF 的动态加载插件

Posted

技术标签:

【中文标题】基于 MVC4 MEF 的动态加载插件【英文标题】:MVC4 MEF-based dynamically loaded plugins 【发布时间】:2012-09-11 14:18:38 【问题描述】:

已更新:请阅读本文下方的最小解决方案

我有一些关于带有插件的 MVC4 解决方案的新手问题。我用谷歌搜索了一下,发现 一些好东西,但它不完全符合我的要求,所以我在这里寻求一些建议。

似乎 MVC 中类似小部件的插件的最佳解决方案是可移植区域(在 MvcContrib 包中)。我在这里找到了基本指南:

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

这里还有一些有用的提示:

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

这篇文章的更多内容:

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

这很酷,但遗憾的是我的要求有点不同:

    不幸的是,我需要一个动态添加和发现插件的系统,而便携式区域则不是这种情况,它必须由主 MVC 站点项目引用。我只想上传一些东西到网站,让它发现和使用新的组件,所以我将使用 MEF。

    幸运的是,我的插件不会像小部件那样可能非常复杂和异构;相反,它们是必须遵循共同、共享模式的组件。将它们想象成专门的编辑器:对于每种数据类型,我将提供一个具有编辑功能的组件:新建、编辑、删除。所以我在考虑实现一个通用接口并提供新建、编辑、删除等操作的插件控制器。

    我必须使用 MVC4,将来我必须添加本地化和移动自定义。

    我必须避免来自复杂框架的依赖,并保持代码尽可能简单。

所以,每当我想在这个网站上添加一个新的数据类型进行编辑时,我只想在其插件文件夹中放置一个 DLL 用于逻辑东西(控制器等),以及正确位置的一些视图,以让网站发现并使用新的编辑器。

最终我可以将视图包含在 DLL 本身中(我发现了这个:http://razorgenerator.codeplex.com,而本教程:http://www.chrisvandesteeg.nl/2010/11/22/embedding-pre-compiled-razor-views-in-your-dll/,我想我可以将它与 codeplex razorgenerator 一起使用,因为它引用的代码与VS2012),但可能我最好将它们分开(也因为本地化和移动感知要求);我正在考虑向我的站点管理区域添加一个上传机制,您可以在其中上传带有控制器和文件夹的 DLL 的单个 zip,然后让服务器解压缩并在需要的地方存储文件。这将允许我轻松修改视图,而无需再次部署整个加载项。

于是我开始寻找 MEF 和 MVC,但大多数帖子都引用 MVC2 并且不兼容。我的运气更好,它主要专注于 Web API,但看起来很有前途且足够简单:

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

这实质上是向“标准”MVC 应用程序添加了一个基于 MEF 的依赖关系解析器和控制器工厂。无论如何,帖子中的示例是指单组件解决方案,而我需要部署几个不同的插件。所以我稍微修改了代码以使用指向我的插件文件夹的 MEF DirectoryCatalog(而不是 AssemblyCatalog),然后创建了一个测试 MVC 解决方案,在类库中有一个插件。

无论如何,当我尝试加载插件控制器时,框架会使用 null 类型调用我的工厂 GetControllerInstance,因此 MEF 当然无法继续进行组合。可能我遗漏了一些明显的东西,但我是 MVC 4 的新手,欢迎任何建议或有用的(符合 MVC4 的)链接。谢谢!

这里是基本代码:

公共静态类 MefConfig 公共静态无效RegisterMef() CompositionContainer 容器 = ConfigureContainer(); ControllerBuilder.Current.SetControllerFactory(new MefControllerFactory(container)); System.Web.Http.GlobalConfiguration.Configuration.DependencyResolver = 新的 MefDependencyResolver(容器); 私有静态 CompositionContainer ConfigureContainer() //AssemblyCatalog assemblyCatalog = new AssemblyCatalog(Assembly.GetExecutingAssembly()); DirectoryCatalog 目录 = 新 DirectoryCatalog( HostingEnvironment.MapPath("~/Plugins")); 组合容器容器 = 新组合容器(目录); 返回容器; 公共类 MefDependencyResolver : IDependencyResolver 私有只读 CompositionContainer _container; 公共 MefDependencyResolver(CompositionContainer 容器) _container = 容器; 公共 IDependencyScope BeginScope() 返回这个; 公共对象 GetService(类型 serviceType) var export = _container.GetExports(serviceType, null, null).SingleOrDefault(); return (export != null ? export.Value : null); 公共 IEnumerable GetServices(类型 serviceType) var export = _container.GetExports(serviceType, null, null); 列出 createdObjects = new List(); 如果(出口。任何()) createdObjects.AddRange(exports.Select(export => export.Value)); 返回创建的对象; 公共无效处置() 公共类 MefControllerFactory : DefaultControllerFactory 私有只读 CompositionContainer _compositionContainer; 公共 MefControllerFactory(CompositionContainer compositionContainer) _compositionContainer = 合成容器; 受保护的覆盖 IController GetControllerInstance( System.Web.Routing.RequestContext requestContext,类型控制器类型) if (controllerType == null) throw new ArgumentNullException("controllerType"); var export = _compositionContainer.GetExports(controllerType, null, null).SingleOrDefault(); IController 结果; if (null != export) result = export.Value as IController; 别的 结果 = base.GetControllerInstance(requestContext, controllerType); _compositionContainer.ComposeParts(结果); //否则 返回结果;

您可以从这里下载完整的测试解决方案:

http://www.filedropper.com/mvcplugins

编辑:第一个可行的最小解决方案

这是我的发现,希望它们对其他从这些东西开始的新手有用:我没有成功运行上面回复中引用的框架,我想必须为 VS2012 和 MVC4 更新一些东西.无论如何,我查看了代码并搜索了更多内容:

1) 首先,让我感到困惑的是两个同名的不同接口:IDependencyResolver。如果我理解得很好,一个 (System.Web.Http.Dependencies.IDependencyResolver) 用于 webapi,另一个 (System.Web.Mvc.IDependencyResolver) 用于通用 DI。这篇文章在这里帮助了我:http://lucid-nonsense.co.uk/dependency-injection-web-api-and-mvc-4-rc/。

2) 另外,第三个组件是 DefaultControllerFactory 派生的控制器工厂,这对本文至关重要,因为它是用于插件托管控制器的工厂。

以下是我对所有这些的实现,对几个示例稍作修改:首先是 HTTP 解析器:

公共密封类 MefHttpDependencyResolver : IDependencyResolver 私有只读 CompositionContainer _container; 公共 MefHttpDependencyResolver(CompositionContainer 容器) if (container == null) throw new ArgumentNullException("container"); _container = 容器; 公共对象 GetService(类型 serviceType) if (serviceType == null) throw new ArgumentNullException("serviceType"); 字符串名称 = AttributedModelServices.GetContractName(serviceType); 尝试 返回 _container.GetExportedValue(name); 抓住 返回空值; 公共 IEnumerable GetServices(类型 serviceType) if (serviceType == null) throw new ArgumentNullException("serviceType"); 字符串名称 = AttributedModelServices.GetContractName(serviceType); 尝试 返回 _container.GetExportedValues(name); 抓住 返回空值; 公共 IDependencyScope BeginScope() 返回这个; 公共无效处置()

然后是 MVC 解析器,它非常相似,即使对于这种情况下的虚拟样本严格来说不是必需的:

公共类 MefDependencyResolver : IDependencyResolver 私有只读 CompositionContainer _container; 公共 MefDependencyResolver(CompositionContainer 容器) if (container == null) throw new ArgumentNullException("container"); _container = 容器; 公共对象GetService(类型类型) if (type == null) throw new ArgumentNullException("type"); 字符串名称 = AttributedModelServices.GetContractName(type); 尝试 返回 _container.GetExportedValue(name); 抓住 返回空值; 公共 IEnumerable GetServices(类型类型) if (type == null) throw new ArgumentNullException("type"); 字符串名称 = AttributedModelServices.GetContractName(type); 尝试 返回 _container.GetExportedValues(name); 抓住 返回空值;

最后是控制器工厂:

[导出(类型(IControllerFactory))] 公共类 MefControllerFactory : DefaultControllerFactory 私有只读 CompositionContainer _container; [导入构造函数] 公共 MefControllerFactory(CompositionContainer 容器) if (container == null) throw new ArgumentNullException("container"); _container = 容器; 公共覆盖 IController CreateController(RequestContext requestContext, string controllerName) var 控制器 = _container .GetExports() .Where(c => c.Metadata.Name.Equals(controllerName, StringComparison.OrdinalIgnoreCase)) .Select(c => c.Value) .FirstOrDefault(); 返回控制器 ?? base.CreateController(requestContext, controllerName);

至于示例控制器,我将其创建到类库项目中:

[导出(类型(IController))] [PartCreationPolicy(CreationPolicy.NonShared)] [导出元数据(“名称”,“Alpha”)] 公共密封类 AlphaController : 控制器 公共行动结果索引() ViewBag.Message = "你好,这是插件控制器!"; 返回视图();

在主项目(MVC 站点)中,我有一个 Plugins 文件夹,我在其中复制此 DLL,并在其文件夹中为该控制器的视图加上一组“标准”视图。

这是最简单的场景,可能还有很多东西需要找出和改进,但我需要从简单开始。无论如何,欢迎任何建议。

【问题讨论】:

我想你可能在看一些我已经找到解决方案的东西。基本上我正在开发一个 MEFified MVC 4/web api 应用程序。随意看看这个:***.com/questions/16626637/…我希望它有所帮助。 Community 碰到了你的问题,可能是因为它没有被接受的答案。您应该从您的问题中删除您的解决方案并将其作为答案发布(然后接受它)。 【参考方案1】:

我目前正在处理同样的问题。我找到了这个解决方案:

博文:http://blog.longle.net/2012/03/29/building-a-composite-mvc3-application-with-pluggable-areas/ 源代码:https://bitbucket.org/darincreason/mvcapplication8.web

基本上它从指定位置加载程序集,并在 Web 应用程序启动时使用某种名称模式:

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;
    

但我相信您可以创建一些可以动态加载程序集的目录观察器,因此您甚至不需要重新启动您的 Web 应用程序。

在我看来,它满足您的 1、2 和 4 需求。它非常简单,不需要任何框架,具有最少的配置,允许动态加载插件并适用于 MVC 4。

这个解决方案将程序集插入到 Area 目录中,但我相信您可以很容易地使用路由对其进行调整以随心所欲地播放。

【讨论】:

【参考方案2】:

您可以找到更完整的解决方案here,以及更多背景知识here。我们的要求也是 DI,所以this 也会有所帮助,尽管不需要(第一个链接也为 DI 提供解决方案。祝你好运,这并不难。另请注意,这些适用于 MVC3,但很容易转换/迁移到 MVC 4

【讨论】:

谢谢,这似乎是一段非常有趣的代码,即使我担心我必须大量研究它:)。无论如何,我首先尝试对其进行测试运行只是为了了解整个系统,所以我从帖子的第二部分下载了示例项目,打开它(使用 VS2012)并运行,但除了一些错别字(ViewModel 或 View 而不是 ViewBag)我无法让它工作:当我按下 F5 时,出现异常:“无法将类型为 'ASP._Page_Views_Home_Index_cshtml' 的对象转换为类型 'System.Web.IHttpHandler'。”。有什么提示吗? 您对错别字是正确的,但也许您的迁移失败了?它按我的计划工作!

以上是关于基于 MVC4 MEF 的动态加载插件的主要内容,如果未能解决你的问题,请参考以下文章

从 MEF 插件中读取 AppSettings

插件之dlopen/dlsym/dlclose 加载动态链接库

插件动态加载方案

插件动态加载方案

Adroid动态加载Apk-插件化技术框架(动态代理方案)

动态加载js css 插件