处理插件时是否可以从组合根目录之外的 DI 容器进行解析

本文关键字:DI 是否 插件 根目录 组合 处理 | 更新日期: 2023-09-27 18:22:50

我正在构建一个WPF(桌面(应用程序,利用依赖注入,DI容器和Register-Resolve-Release模式。我的应用程序还在启动期间从单独的程序集加载插件,并且插件已注册到 DI 容器。我在启动时在组合根中解析了整个对象图,但是,我在解决插件时遇到了一些问题,我想知道是否可以将 DI 容器注入工厂以解析未知类型?

我的插件全部:

  • 实现通用接口,IPlugin
  • 需要瞬态(因为多个实例可以同时存在(
  • 依赖于运行时值,以便在初始化时对其进行配置
  • 可能(也可能没有(通过其构造函数注入其他依赖项

我已经开始实现一个PluginFactory,在需要时初始化插件,因为正如 Mark Seemann 所说:">任何需要运行时值来构造特定依赖项的地方,抽象工厂都是解决方案。

但是,我不能只new()工厂中的插件,因为我不知道它们的类型和依赖项。

我想到了几个解决方案:

  1. 我可以将 DI 容器注入PluginFactory的构造函数中,在运行时解析插件并调用它一天。然而,这与RRR模式背道而驰。

  2. 我可以通过PluginFactory的构造函数注入IEnumerable<IPlugin> plugins.但是,这将违反瞬态要求,因为我每个实例只有一个副本[*错误]。我可以通过在我所有的插件上实现ICloneable并在工厂中克隆它们来解决这个问题,但是,这既很难用依赖项来纠正,又会使所有插件混乱。

[*错误] 编辑:注意,根据选择的答案,这是错误的!因此,选项 #2 是最佳选择,只要您将插件的生命周期注册为瞬态。

  1. 我可以将插件类型而不是实例注入PluginFactory并使用Activator.CreateInstance<T>()来创建实例。我甚至可以为插件定义一个通用的构造函数签名,并将参数传递给激活器以使用依赖项初始化它们。但是,这违反了我的要求,即插件具有不同的依赖项,并且还导致了约束构造反模式。

  2. 最后,在 3.我可以注入插件类型并使用反射来整理插件的依赖关系。但是,这听起来像是很多工作,让我想知道我是否正在尝试从 DI 容器构建或复制解析方法。后者是我绝对不想做的事情,也不想看到这一点。

因此,在我更喜欢#1中的替代方案中,即使这与RRR模式背道而驰。

实例化我的插件的最佳方式是什么(我倾向于#1(。我是否完全错过了一些替代方案?还是我可能误判了我的其他一些选择?

处理插件时是否可以从组合根目录之外的 DI 容器进行解析

既然你已经引用了Mark Seemann,看看他的合成根文章和他的服务定位器角色与力学文章。长话短说,可以将容器引用/注入到作为组合根一部分的基础结构组件中。

因此,解决方案是定义一个IPluginFactory抽象并将其放置在应用程序的核心库中。在你的组合根中(注意:将CR视为一个层,而不是一个类(,你可以为这个抽象创建一个实现,在这里可以注入容器。

在简单注入器中,注入的IEnumerable<T>实例的行为就像流一样。这意味着每次迭代枚举对象时,都会再次要求容器提供实例。简单喷油器文档指出:

注意:简单注入器保留了返回的实例的生活方式 从注入的IEnumerable<T>实例。实际上你不应该 将注入的IEnumerable<IValidator<T>>视为 实现 - 您应该将其视为实例。简单 注入器将始终注入对同一流的引用( IEnumerable<T>本身就是一个单例(,每次迭代 IEnumerable<T> ,对于每个单独的组件,询问容器 以根据该组件的生活方式解析实例。 无论 [消费组件] 是否注册为 单例,它包装的验证器将每个都有自己特定的 生活方式。

容器将根据实例的生活方式返回实例。如果实例注册为暂时性实例,则意味着每次迭代集合时都会获得一个新实例。从某种意义上说,简单注入器注入的枚举项的行为就像工厂一样。下面是一个显示此行为的小测试:

[TestMethod]
public void EnumerablesBehaveAsStreams()
{
    // Arrange
    var container = new Container();
    container.Collection.Register<ILogger>(typeof(SqlLogger), typeof(FileLogger));
    IEnumerable<ILogger> loggers = container.GetAllInstances<ILogger>();
    // Act
    ILogger sqlLogger1 = loggers.First();
    ILogger sqlLogger2 = loggers.First();
    // Assert
    Assert.AreNotSame(sqlLogger1, sqlLogger2);
}

一些开发人员对此感到困惑,但我认为这是唯一正确的行为,因为IEnumerable<T>流的定义,而Simple Injector尊重这一点,因此为您提供了一个流。这样做有一些有趣的优势。例如,它允许将枚举注入到单例中,同时保留元素的生活方式。

如果你想注册一个插件集合,这将是这样做的方法:

List<Type> pluginTypes = LoadPluginTypes();
container.Collection.Register<IPlugin>(pluginTypes);

Collection.Register将已注册类型的解析转发回容器,因此尊重注册每个类型的生活方式。在上面的示例中,我们没有单独注册这些类型。这意味着默认情况下假定所有元素都是瞬态的。但您可以按如下方式覆盖它:

List<Type> pluginTypes = LoadPluginTypes();
container.Register(pluginTypes.First(), Lifestyle.Singleton);
container.Collection.Register<IPlugin>(pluginTypes);

现在,集合中的第一个插件被注册为单例,而其他插件仍然是瞬态的。如果您想以相同的生活方式注册它们,您可以执行以下操作:

List<Type> pluginTypes = LoadPluginTypes();
pluginTypes.ForEach(type => container.Register(type, Lifestyle.Singleton));
container.Collection.Register<IPlugin>(pluginTypes);

另一种选择是将 Registration 实例的集合传递到 Collection.Register 方法中:

List<Type> pluginTypes = LoadPluginTypes();
container.Collection.Register(typeof(IPlugin),
    from type in pluginTypes
    select Lifestyle.Singleton.CreateRegistration(
        typeof(IPlugin), type, container));

在这种情况下,以前的注册之间没有实际区别,但是当您想要执行更高级的操作(例如在多个抽象上共享同一注册实例(时,注册Registration实例变得很有趣。

但是流行为在您的工厂中非常有用,因为您只需将IEnumerable<IPlugin>注入工厂,生活方式就会保留:

public class PluginFactory : IPluginFactory
{
    private readonly IEnumerable<IPlugin> plugins;
    public PluginFactory(IEnumerable<IPlugin> plugins)
    {
        this.plugins = plugins;
    }
    public IPlugin GetPluginByName(string name)
    {
        return this.plugins.Single(plugin => plugin.Name == name);
    }
}

这里唯一需要注意的是,如果您有兴趣通过过滤集合一次返回一个插件,则迭代集合将生成许多插件实例,即使您可能只对一个插件感兴趣。在上面的例子中,Single将导致创建所有插件,而你只返回一个插件。更有效的做法是 First ,因为当插件符合标准时会停止,这会阻止创建所有以后的插件。

Simple Injector非常快,因此在这通常成为问题之前,收集可能会变得非常大,但这是需要注意的。如果您打算筛选列表并始终选择一个列表,则其他方法可能会更好。