Ninject动态绑定到实现

5

Stack Overflow上有一些类似的问题,但并不是我要找的。我想根据运行时条件进行Ninject绑定,这在启动时是未知的。Stack Overflow上其他关于动态绑定的问题都是围绕着基于配置文件等进行绑定 - 我需要根据处理特定实体的数据时基于数据库值进行条件绑定。例如,

public class Partner
{
    public int PartnerID { get; set; }
    public string ExportImplementationAssembly { get; set; }
}

public interface IExport
{
    void ExportData(DataTable data);
}

此外,我有两个实现了IExport接口的动态链接库(dlls)。

public PartnerAExport : IExport
{
    private readonly _db;
    public PartnerAExport(PAEntities db)
    {
        _db = db;
    }
    public void ExportData(DataTable data)
    {
        // export parter A's data...
    }
}

然后针对伙伴B;
public PartnerBExport : IExport
{
    private readonly _db;
    public PartnerBExport(PAEntities db)
    {
        _db = db;
    }
    public void ExportData(DataTable data)
    {
        // export parter B's data...
    }
}

当前的Ninject绑定为:

public class NinjectWebBindingsModule : NinjectModule
{
    public override void Load()
    {
        Bind<PADBEntities>().ToSelf();
        Kernel.Bind(s => s.FromAssembliesMatching("PartnerAdapter.*.dll")
                          .SelectAllClasses()
                          .BindDefaultInterfaces()
                   );
    }
}

那么,我该如何设置绑定方式,以便我可以执行以下操作;
foreach (Partner partner in _db.Partners)
{
    // pseudocode...
    IExport exportModule = ninject.Resolve<IExport>(partner.ExportImplementationAssembly);
    exportModule.ExportData(_db.GetPartnerData(partner.PartnerID));
}

这是否可能?看起来应该是可能的,但我不知道如何处理。上面的绑定配置对于静态绑定非常有效,但我需要在运行时解析一些东西。这是否可行,还是我必须绕过Ninject并使用老式反射来加载插件?如果是这样,我如何使用该方法通过Ninject解析任何构造函数参数,就像静态绑定的对象一样?
更新:我已经使用BatteryBackupUnit的解决方案更新了我的代码,现在我有以下内容;
Bind<PADBEntities>().ToSelf().InRequestScope();
Kernel.Bind(s => s.FromAssembliesMatching("PartnerAdapter.*.dll")
                    .SelectAllClasses()
                    .BindDefaultInterfaces()
                    .Configure(c => c.InRequestScope())
            );

Kernel.Bind(s => s.FromAssembliesMatching("PartnerAdapter.Modules.*.dll")
                    .SelectAllClasses()
                    .InheritedFrom<IExportService>()
                    .BindSelection((type, baseTypes) => new[] { typeof(IExportService) })
            );
Kernel.Bind<IExportServiceDictionary>().To<ExportServiceDictionary>().InSingletonScope();
ExportServiceDictionary dictionary = KernelInstance.Get<ExportServiceDictionary>();

在2个测试模块中实例化导出实现可以正常工作并且成功实例化PADBEntites上下文。然而,我的服务层中的所有其他绑定现在都不再适用于系统的其余部分。同样,如果我将PADBEntities变量/构造函数参数更改为ISomeEntityService组件,则无法绑定导出层。似乎我缺少最后一步来配置绑定以使其正常工作。有什么想法吗?
错误:“Error activating ISomeEntityService. No matching bindings are available and the type is not self-bindable”。
更新2:通过使用BatteryBackupUnit的解决方案进行了一些试验和错误,最终将其搞定,但我对跳跃的障碍不是很满意。欢迎任何更简洁的解决方案。
我更改了原始约定绑定为:
        Kernel.Bind(s => s.FromAssembliesMatching("PartnerAdapter.*.dll")
                          .SelectAllClasses()
                          .BindDefaultInterfaces()
                   );

转而更加冗长和明确;

Bind<IActionService>().To<ActionService>().InRequestScope();
Bind<IAuditedActionService>().To<AuditedActionService>().InRequestScope();
Bind<ICallService>().To<CallService>().InRequestScope();
Bind<ICompanyService>().To<CompanyService>().InRequestScope();
//...and so on for 30+ lines

虽然这不是我最喜欢的解决方案,但它可以与显式和基于约定的绑定一起使用,但不能与两个约定一起使用。有人能看出我的绑定有什么问题吗?

更新3:忽略更新2中的绑定问题。似乎我发现了Ninject的一个bug,与在引用库中有多个绑定模块有关。即使从未通过断点命中模块A的更改也会破坏明确使用不同模块B的项目。太难以理解了。


在我看来,下面的答案都实现了某种形式的工厂,这是一个正确的答案。注入工厂,让工厂返回适当的IExport。尽管下面的一些(有见地的)评论提供了帮助,但将其放在工厂中可以使您免受Ninject特定功能的影响。 - Trevor Ash
@Atoms,这里有一些导出实现需要其构造函数中的其他服务已经通过ninject内核绑定。重复使用它们而不是实现一个特定的工厂是有道理的。因此,在这里ninect是必需的。 - DiskJunky
1
使用两部分约定,缺少哪个绑定?还要注意,使用两部分约定,您将会两次绑定每个 IExport。对于第一种约定,您应该排除所有 IExport。我建议您创建一个新的问题,询问如何形成问题。也许像codereview或程序员这样的其他SE平台会更好。建议:为所有以“Service”结尾的类型制定一个约定,并特别绑定所有其他类型。此外,如果有更具体的绑定,它们可以放入特定程序集的 NinjectModule 中。 - BatteryBackupUnit
@BatteryBackupUnit,如果我知道怎么做的话,我会的:) 我的ninject处于n00b级别。当我填充字典时,我确实看到了重复的绑定,但将它们过滤掉了。如何在原始绑定中排除IExport?至于为服务创建规则,我也有...Reader和...Writer,但我想那些也可以配置。关键是“如何”。Ninject文档不是最好的,很难弄清楚你需要解决什么问题。因此,首先提出这个问题。 - DiskJunky
@是的,我明白。但是关于IExport的上下文绑定以及如何设计传统绑定的问题应该分开。"如何设计"传统绑定不适合在SO上提问。具体问题=>如何排除某个类型是SO上的好问题。答案:您可以使用Where过滤方法来排除所有实现IExport的类型。有关约定的更多信息,请参见此处 - BatteryBackupUnit
@BatteryBackupUnit,说得好,我会提取并重新发布有关规范的内容。我已将您的回复标记为答案,因为它确实让我找到了解决方案。感谢您的所有帮助! - DiskJunky
2个回答

3
需要翻译的内容如下:

需要注意的是,虽然实际的“条件匹配”是运行时的条件,但您事实上已经在启动时(构建容器时)预先知道了可能的匹配集-这可以通过使用约定来证明。这就是条件/上下文绑定的含义(在Ninject WIKI中描述并涉及几个问题)。因此,您实际上不需要在任意运行时进行绑定,而只需要在任意时间进行解析/选择(实际上可以提前进行解析=>尽早失败)。

以下是一种可能的解决方案,其中包括:

  • 在启动时创建所有绑定
  • 尽早失败:在启动时验证绑定(通过实例化所有绑定的IExport
  • 在任意运行时选择IExport

.

internal interface IExportDictionary
{
    IExport Get(string key);
}

internal class ExportDictionary : IExportDictionary
{
    private readonly Dictionary<string, IExport> dictionary;

    public ExportDictionary(IEnumerable<IExport> exports)
    {
        dictionary = new Dictionary<string, IExport>();
        foreach (IExport export in exports)
        {
            dictionary.Add(export.GetType().Assembly.FullName, export);
        }
    }

    public IExport Get(string key)
    {
        return dictionary[key];
    }
}

组合根:

// this is just going to bind the IExports.
// If other types need to be bound, go ahead and adapt this or add other bindings.
kernel.Bind(s => s.FromAssembliesMatching("PartnerAdapter.*.dll")
        .SelectAllClasses()
        .InheritedFrom<IExport>()
        .BindSelection((type, baseTypes) => new[] { typeof(IExport) }));

kernel.Bind<IExportDictionary>().To<ExportDictionary>().InSingletonScope();

// create the dictionary immediately after the kernel is initialized.
// do this in the "composition root".
// why? creation of the dictionary will lead to creation of all `IExport`
// that means if one cannot be created because a binding is missing (or such)
// it will fail here (=> fail early).
var exportDictionary = kernel.Get<IExportDictionary>(); 

现在IExportDictionary可以注入到任何组件中,并且只需像“required”一样使用:

foreach (Partner partner in _db.Partners)
{
    // pseudocode...
    IExport exportModule = exportDictionary.Get(partner.ExportImplementationAssembly);
    exportModule.ExportData(_db.GetPartnerData(partner.PartnerID));
}

好的实现比Steve的更容易理解。然而,当我尝试将其他接口/ PADBEntities对象添加到导出实现构造函数时,绑定失败了。有什么想法吗? - DiskJunky
这(很可能)意味着某些 IExport 所需的所有类型都没有绑定。请注意,在我的答案中,我更改了 PartnerAdapter.*.dll 类型的约定绑定。您可能希望将其更改回最初的状态或进行更适当的约定/绑定。我进行了更改,因为我不建议使用这种非常广泛的约定。相反,我会让 dll 实现一些 NinjectModule 并使用 kernel.Load 加载这些模块。如果我的怀疑是错误的,请发布整个异常(类型、消息、堆栈跟踪、潜在的内部异常...)。 - BatteryBackupUnit
我意识到了一些问题,但是你在示例中选择的绑定会破坏我在 OP 中已经设置好的构造函数中的 IService 引用 - 它不再能够看到这些引用。错误信息: Error activating ISomeEntityService. No matching bindings are available and the type is not self-bindable。这个绑定在原始帖子中是有效的,但是当添加了你对 IExport 的绑定后就失效了。 - DiskJunky
我已经更新了我的问题,并提供了一个丑陋但可行的绑定解决方案。你能提出任何改进建议吗? - DiskJunky

2
我希望能够基于运行时条件进行Ninject绑定,而这些条件在启动时并不预先知道。
在构建对象图时避免做出运行时决策。这会使配置复杂化并且难以验证。理想情况下,您的对象图应该是固定的,并且在运行时不应更改其形状。
相反,通过将此移动到代理类中,在运行时做出运行时决策。这样的代理看起来取决于您的具体情况,但以下是一个示例:
public sealed class ExportProxy : IExport
{
    private readonly IExport export1;
    private readonly IExport export2;
    public ExportProxy(IExport export1, IExport export2) {
        this.export1 = export1;
        this.export2 = export2;
    }

    void IExport.ExportData(Partner partner) {
        IExport exportModule = GetExportModule(partner.ExportImplementationAssembly);
        exportModule.ExportData(partner);
    }

    private IExport GetExportModule(ImplementationAssembly assembly) {
        if (assembly.Name = "A") return this.export1;
        if (assembly.Name = "B") return this.export2;
        throw new InvalidOperationException(assembly.Name);
    }
}

也许您正在处理一组动态确定的程序集。在这种情况下,您可以提供一个导出提供程序委托给代理。例如:

public sealed class ExportProxy : IExport
{
    private readonly Func<ImplementationAssembly, IExport> exportProvider;
    public ExportProxy(Func<ImplementationAssembly, IExport> exportProvider) {
        this.exportProvider = exportProvider;
    }

    void IExport.ExportData(Partner partner) {
        IExport exportModule = this.exportProvider(partner.ExportImplementationAssembly);
        exportModule.ExportData(partner);
    }
}

通过使用一个 Func<,> 来为代理提供参数,您仍然可以在注册 ExportProxy 的地方(即组合根)做出决策,并在该位置查询系统的程序集。这样,您就可以事先在容器中注册 IExport 实现,从而提高了配置的可验证性。如果您使用一个键注册了所有的 IExport 实现,那么您可以对 ExportProxy 进行以下简单的注册。
kernel.Bind<IExport>().ToInstance(new ExportProxy(
    assembly => kernel.Get<IExport>(assembly.Name)));

有效但有问题-潜在有大量合作伙伴使用未知的最终输出实现。您的解决方案将需要一个新的构造函数参数以及每次我们想要添加新实现时都要更新“if”或“switch”。尽管如此,我没有考虑过这种方法。 - DiskJunky
@BatteryBackupUnit:您如何在.NET中反射一组程序集并将相应类型填充到字典中?当然,确切的答案取决于OP的确切要求,但解决方案将很简单。使用Ninject,您甚至可以(滥用)内核作为字典;请参见我的更新。 - Steven
@Steven,非常有趣的方法!我得试一下看看它是否有效 - 我有点不确定在lambda中对kernel.Bind()的后期调用是否会正确解决,但值得尝试实现。 - DiskJunky
@电池备份单元,这种情况下,我们可以忽略InRequestScope()——谢天谢地它不需要导出。我只是从现有的绑定实现中复制/粘贴过来的。我会将其从帖子中删除,因为它是一个不必要的复杂性。 - DiskJunky
1
@BatteryBackupUnit:对我来说,重要的是为消费者隐藏实现细节。将Func<Asm, IExport>ExportDictionary注入到消费者中会让他们意识到存在多个实现,并且消费者会得到一个额外的概念/抽象来处理。但最终,这取决于上下文,因为使用我的解决方案,OP可能需要更改IExport接口。虽然更改的抽象可能更好,但如果更改不可行,您将再次得到一个类似于工厂/提供程序的额外抽象,例如您的IExportDictionary - Steven
显示剩余6条评论

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