基于MVC4 MEF的动态加载插件

Naf*_*tis 17 c# mef asp.net-mvc-4

更新:请阅读本文中的最低解决方案

我有一些关于带插件的MVC4解决方案的新手问题.我google了一下,找到了一些好东西,但它并不完全符合我的要求,所以我在这里要求一些建议.

似乎MVC中类似于widget的插件的最佳解决方案是可移植区域(在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

这篇文章中有更多内容:

如何创建ASP.NET MVC区域作为插件DLL?

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

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

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

  3. 我必须使用MVC4,将来我必须添加本地化和移动定制.

  4. 我必须避免复杂框架的依赖,并尽可能简化代码.

所以,每当我想在这个网站上添加一个新的数据类型进行编辑时,我只想在其插件文件夹中删除一个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

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

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

这是基本代码:

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)首先,对我来说混淆的原因是具有相同名称的2个不同接口: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解析器:

public sealed class MefHttpDependencyResolver : IDependencyResolver
{
    private readonly CompositionContainer _container;

    public MefHttpDependencyResolver(CompositionContainer container)
    {
        if (container == null) throw new ArgumentNullException("container");
        _container = container;
    }

    public object GetService(Type serviceType)
    {
        if (serviceType == null) throw new ArgumentNullException("serviceType");

        string name = AttributedModelServices.GetContractName(serviceType);

        try
        {
            return _container.GetExportedValue(name);
        }
        catch
        {
            return null;
        }
    }

    public IEnumerable GetServices(Type serviceType)
    {
        if (serviceType == null) throw new ArgumentNullException("serviceType");

        string name = AttributedModelServices.GetContractName(serviceType);

        try
        {
            return _container.GetExportedValues(name);
        }
        catch
        {
            return null;
        }
    }

    public IDependencyScope BeginScope()
    {
        return this;
    }

    public void Dispose()
    {
    }
}

然后MVC解析器,非常相似,即使在这种情况下严格不需要虚拟样本:

public class MefDependencyResolver : IDependencyResolver
{
    private readonly CompositionContainer _container;

    public MefDependencyResolver(CompositionContainer container)
    {
        if (container == null) throw new ArgumentNullException("container");
        _container = container;
    }

    public object GetService(Type type)
    {
        if (type == null) throw new ArgumentNullException("type");

        string name = AttributedModelServices.GetContractName(type);

        try
        {
            return _container.GetExportedValue(name);
        }
        catch
        {
            return null;
        }
    }

    public IEnumerable GetServices(Type type)
    {
        if (type == null) throw new ArgumentNullException("type");

        string name = AttributedModelServices.GetContractName(type);

        try
        {
            return _container.GetExportedValues(name);
        }
        catch
        {
            return null;
        }
    }
}

最后是控制器工厂:

[Export(typeof(IControllerFactory))]
public class MefControllerFactory : DefaultControllerFactory
{
    private readonly CompositionContainer _container;

    [ImportingConstructor]
    public MefControllerFactory(CompositionContainer container)
    {
        if (container == null) throw new ArgumentNullException("container");
        _container = container;
    }

    public override IController CreateController(RequestContext requestContext, string controllerName)
    {
        var controller = _container
            .GetExports()
            .Where(c => c.Metadata.Name.Equals(controllerName, StringComparison.OrdinalIgnoreCase))
            .Select(c => c.Value)
            .FirstOrDefault();

        return controller ?? base.CreateController(requestContext, controllerName);
    }
}

至于样本控制器,我将其创建为一个类库项目:

[Export(typeof(IController))]
[PartCreationPolicy(CreationPolicy.NonShared)]
[ExportMetadata("Name", "Alpha")]
public sealed class AlphaController : Controller
{
    public ActionResult Index()
    {
        ViewBag.Message = "Hello, this is the PLUGIN controller!";

        return View();
    }
}

在主项目(MVC站点)中,我有一个Plugins文件夹,我在其中复制此DLL,并在其文件夹中为此控制器的视图添加"标准"视图集.

这是最简单的场景,可能还有更多要找出并改进,但我需要从简单开始.无论如何,任何建议都是受欢迎的.

Lan*_*eyo 2

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

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

AssemblyInfo.cs:

[assembly: PreApplicationStartMethod(
  typeof(PluginAreaBootstrapper), "Init")]
Run Code Online (Sandbox Code Playgroud)

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

我认为它满足您的1、2和4需求。它非常简单,不需要任何框架,配置最少,允许动态加载插件并与 MVC 4 配合使用。

该解决方案将程序集插入到 Area 目录中,但我相信您可以轻松地使用路由将其调整为您喜欢的播放方式。