关于c#:基于MVC4 MEF的动态加载插件

MVC4 MEF-based dynamically loaded plugins

更新:请阅读下面的文章,了解最基本的解决方案

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

对于MVC中类似小部件的插件来说,最好的解决方案似乎是可移植区域(在MVCContrib包中)。我在这里找到了基本指导:

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

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

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

这篇文章的更多内容:

如何将ASP.NET MVC区域创建为插件dll?

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

  • 不幸的是,我需要一个动态添加和发现插件的系统,而对于可移植区域则不是这样,它必须由主MVC站点项目引用。我只想上传一些东西到网站上,让它发现并使用新的组件,所以我将使用MEF来完成这个任务。

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

  • 我必须使用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-和-webapi.html

    这实际上是在"标准"MVC应用程序中添加了一个基于MEF的依赖关系解析器和控制器工厂。总之,本文中的示例引用了一个组装解决方案,而我需要部署几个不同的插件。所以我稍微修改了代码,使用指向插件文件夹的mef directorycatalog(而不是assemblycatalog),然后用类库中的单个插件创建了一个测试MVC解决方案。

    无论如何,当我尝试加载插件控制器时,框架使用空类型调用我的工厂getcontrollerinstance,因此MEF当然不能继续组合。可能我遗漏了一些明显的东西,但我对MVC4还不熟悉,欢迎任何建议或有用的(兼容MVC4)链接。谢谢!

    以下是基本代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    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;
        }
    }

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

    http://www.filedropper.com/mvcplugins

    编辑:第一个有效的最小解决方案

    以下是我的发现,希望它们能对其他新手有所帮助:我没有成功地运行上面回复中引用的框架,我想肯定有一些东西需要为VS2012和MVC4更新。不管怎样,我看了一下代码,又在谷歌上搜索了一下:

    1)首先,我感到困惑的一个原因是两个同名的不同接口:IDependencyResolver。如果我理解的很好,一个(system.web.http.dependencies.idependencyresolver)用于WebAPI,另一个(system.web.mvc.idependencyresolver)用于通用DI。这篇文章帮助了我:http://lucid nusse.co.uk/dependency-injection-web-api-and-mvc-4-rc/。

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


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

    • 博客帖子:http://blog.longle.net/2012/03/29/building-a-composite-mvc3-application-with-pluggable-areas/
    • 源代码:https://bitback.org/darincreason/mvcaplication8.web

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

    程序集信息.cs:

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

    插件eabootstrapper.cs:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    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需求。它非常简单,不需要任何框架,配置最少,允许动态加载插件,并与MVC4一起工作。

    此解决方案将程序集插入区域目录,但我相信您可以很容易地使用路由调整程序集以播放所需的内容。


    你可以在这里找到更完整的解决方案,这里还有更多的背景。我们的需求也是一个DI,所以这也会有点帮助,尽管不需要(第一个链接也为DI提供了解决方案)。祝你好运,这并不难。还请注意,这些是针对MVC3的,但很容易转换/迁移到MVC 4