在插件式架构中使用Ninject

27

我正在学习DI,并最近完成了我的第一个项目。

在这个项目中,我实现了仓储模式,拥有接口和具体实现。我想知道是否可以将我的接口实现构建为“插件”,即程序会动态加载dll。

因此,该程序可以随着时间的推移而改进,而无需重新构建它,只需将dll放置在“插件”文件夹中,更改设置,然后就完成了!

这种做法可行吗?Ninject能帮助实现吗?

9个回答

26

虽然Sean Chambers的解决方案适用于您控制插件的情况,但对于可能由第三方开发插件并且您不希望他们依赖编写ninject模块的情况,它无法工作。

使用Ninject的Conventions Extension非常容易实现此目的:

public static IKernel CreateKernel()
{
    var kernel = new StandardKernel();

    kernel.Scan(scanner => {
        scanner.FromAssembliesInPath(@"Path\To\Plugins");
        scanner.AutoLoadModules();
        scanner.WhereTypeInheritsFrom<IPlugin>();
        scanner.BindWith<PluginBindingGenerator<IPlugin>>();
    });

    return kernel;
}

private class PluginBindingGenerator<TPluginInterface> : IBindingGenerator
{
    private readonly Type pluginInterfaceType = typeof (TPluginInterface);

    public void Process(Type type, Func<IContext, object> scopeCallback, IKernel kernel)
    {
        if(!pluginInterfaceType.IsAssignableFrom(type))
            return;
        if (type.IsAbstract || type.IsInterface)
            return;
        kernel.Bind(pluginInterfaceType).To(type);
    }
}

你可以使用 kernel.GetAll<IPlugin>() 来获取所有已加载的插件。

这种方法的优点是:

  1. 你的插件dll不需要知道它们正在被ninject加载
  2. ninject将解析具体的插件实例,因此它们可以拥有构造函数以注入插件主机知道如何构建的类型。

3
我在这里的一个项目中基本上做了这件事。我发现如果让Ninject负责加载程序集,如果其中一个插件dll无法加载,这将导致应用程序崩溃。为了避免这种情况,我首先将程序集加载到应用程序域中,并用try/catch包装以获得更好的行为。 - ungood
你如何使用最新版本的Ninject来完成相同的事情,因为Scan已经不存在了? - MoonKnight

12

这个问题与我在这里提供的答案有关:Can NInject load modules/assemblies on demand?

我非常确定这就是你要找的答案:

var kernel = new StandardKernel();
kernel.Load( Assembly.Load("yourpath_to_assembly.dll");

如果你使用反射器(reflector)在Ninject.dll中查看KernelBase,你会发现这个调用会递归加载所有已加载程序集中的所有模块(Load方法接受IEnumerable参数)。

public void Load(IEnumerable<Assembly> assemblies)
{
    foreach (Assembly assembly in assemblies)
    {
        this.Load(assembly.GetNinjectModules());
    }
}

我在某些场景下使用它,这些场景中我不希望直接引用会经常变化的东西,而我可以更换程序集来向应用程序提供不同的模型(当然,前提是我已经进行了适当的测试)。


1
我在想,是否有可能插件会对你的应用程序造成干扰。 - Piotr Owsiak
1
这是如何工作的?如果我的程序集包含多个类,它会搜索任何继承自NinjectModule的类吗?还是我的程序集只需要包含NinjectModules? - Pandincus
1
这段代码的作用是在你的程序集中查找任何继承自NinjectModule的类,并调用每个类的Load()方法,从而初始化你的绑定。 - Sean Chambers
2
根据 GitHub 上 Ninject wiki 的说明,您可以通过将程序集命名为 Ninject.Extensions.*.dll 来自动加载插件。然而,关于此功能的文档量少于我正在输入的段落。 - devlord

11

在 @ungood 的很好的答案基础上进行扩展,该答案基于 v.2,在使用 Ninject 的 v.3(当前为 RC3)时,可以使它变得更加容易。您不再需要任何 IPluginGenerator,只需编写:

var kernel = new StandardKernel();
kernel.Bind(scanner => scanner.FromAssembliesInPath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location))
                                   .SelectAllClasses()
                                   .InheritedFrom<IPlugin>()
                                   .BindToAllInterfaces());

请注意,我正在寻找实现IPlugin(将您的接口放在这里)的插件,它们需要与应用程序在同一路径中。


你如何使用你的解决方案进行自动加载模块?我刚从Ninject 2.2换到了3.0,但无法自动加载模块。 - Charles Ouellet

3

您可以使用普通的C#反射轻松完成,无需任何额外技术。

网络上有很多示例,例如:http://www.codeproject.com/KB/cs/c__plugin_architecture.aspx

通常在主应用程序中,您需要加载实现插件的程序集,例如:

ass = Assembly.Load(name);

然后您需要创建插件的实例。如果您知道类的名称,它应该像这样:

ObjType = ass.GetType(typename);
IPlugin plugin = (IPlugin)Activator.CreateInstance(ObjType);

然后你只需要使用它。


1
不知道为什么这个被投票否决了。它在技术上是准确的并且是一个有效的答案。 - Joseph Ferris
1
可能是因为他不想知道如何创建插件,而是想知道如何使用 DI 并在不进行编译的情况下更改它的 DI... 所以他建议使用插件... - Patrick Desjardins
我没有给你投反对票 :P 为什么你要给我投反对票... 不管怎样,很有趣。 - Patrick Desjardins

1

0

我在搜索Activator.CreateInstance + Ninject时发现了这个,想指出一些相关的事情 - 希望能激发某人在SO上提供一个真正杀手级别的答案。

如果你还没有费心自动扫描模块和类并正确地将它们注册到Ninject中,并且仍然通过Activator.CreateInstance创建你的插件,那么你可以通过post-CreateInstance注入依赖项。

IKernel k = ...
var o = Activator.CreateInstance(...);
k.Inject( o );

当然,这只是通往类似http://groups.google.com/group/ninject/browse_thread/thread/880ae2d14660b33c的东西的临时解决方案。


0

有多种方法可以实现这一点,而您已经通过预定义的接口实现了具体的实现,从而实现了主要目标。如果您的接口保持稳定,那么您应该能够在核心应用程序的基础上构建。

然而,我不确定 Ninject 的实现方式是如何工作的。您可以使用提供程序模型或反射来完成此操作 - 尽管我认为如果您不绝对需要它,则反射可能过度。

使用提供程序模型方法,您将文件放置在 /bin 文件夹或任何其他您正在探测的文件夹中,并调整 .config 文件以反映提供程序的存在。如果您有一个特定的“插件”文件夹,您可以创建一个在应用程序启动时和定期扫描新实例或删除实例并重新加载提供程序的方法。

这将适用于 C# 或 VB 下的 ASP.NET。但是,如果您正在进行某种其他应用程序,则需要考虑另一种方法。提供程序实际上只是 Microsoft 对策略模式的解释。


0

-1
问题在于如果你在模块的加载中设置的对象在程序内被使用,那么你可能需要重新编译。原因是你的程序可能没有最新版本的类库程序集。例如,如果你为接口创建了一个新的具体类,比如说你更改了插件dll文件。现在,注入器会将其加载,一切正常,但是当它在你的程序中返回(kernel.get(...))时,你的程序可能没有这个程序集,并且会抛出错误。
下面是我谈论的示例:
BaseAuto auto = kernel.Get<BaseAuto>();//Get from the NInjector kernel your object. You get your concrete objet and the object "auto" will be filled up (interface inside him) with the kernel.

//Somewhere else:

public class BaseModule : StandardModule
{
        public override void Load(){
            Bind<BaseAuto>().ToSelf();
            Bind<IEngine>().To<FourCylinder>();//Bind the interface
        }     
 }

如果您创建了一个名为SixCylinder的新FourCylinder,您的真实程序将不会引用您的新对象。因此,一旦从插件加载BaseModule.cs,您可能会遇到一些引用问题。要能够这样做,您需要使用包含Injector所需的模块的插件分发此具体实现的新dll,以便加载接口到具体类。这可以无问题完成,但您开始拥有一个完全驻留在插件加载上的整个应用程序,这可能在某些点上有问题。要注意。

但是,如果您确实想要一些插件信息,可以查看 CodeProject教程


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接