SimpleInjector-依赖于另一个注册对象的值的注册对象

本文关键字:对象 注册 依赖于 另一个 SimpleInjector- | 更新日期: 2023-09-27 18:00:30

使用SimpleInjector,我试图注册一个依赖于从另一个注册实体检索到的值的实体。例如:

Settings-读取指示应用程序需要的SomeOtherService类型的设置值。

SomeOtherService-依赖于Settings中的一个值进行实例化(因此进行注册)。

一些DI容器允许在解析另一个对象之后注册一个对象。所以你可以做一些类似下面的伪代码:

    container.Register<ISettings, Settings>();
    var settings = container.Resolve<ISettings>();
    System.Type theTypeWeWantToRegister = Type.GetType(settings.GetTheISomeOtherServiceType());
    container.Register(ISomeOtherService, theTypeWeWantToRegister);

SimpleInjector不允许在解析后进行注册。SimpleInjector中是否存在允许相同体系结构的某种机制?

SimpleInjector-依赖于另一个注册对象的值的注册对象

获得此要求的一个简单方法是注册所有可能需要的可用类型,并进行配置以确保容器在运行时返回正确的类型。。。用英语解释不那么容易,让我示范一下。

您可以有一个接口的多个实现,但在运行时您需要其中一个,并且您需要的一个由文本文件中的设置(string)控制。以下是测试类。

public interface IOneOfMany { }
public class OneOfMany1 : IOneOfMany { }
public class OneOfMany2 : IOneOfMany { }
public class GoodSettings : ISettings
{
    public string IWantThisOnePlease
    {
        get { return "OneOfMany2"; }
    }
}

所以,让我们继续注册它们:

private Container ContainerFactory()
{
    var container = new Container();
    container.Register<ISettings, GoodSettings>();
    container.RegisterAll<IOneOfMany>(this.GetAllOfThem(container));
    container.Register<IOneOfMany>(() => this.GetTheOneIWant(container));
    return container;
}
private IEnumerable<Type> GetAllOfThem(Container container)
{
    var types = OpenGenericBatchRegistrationExtensions
        .GetTypesToRegister(
            container,
            typeof(IOneOfMany),
            AccessibilityOption.AllTypes,
            typeof(IOneOfMany).Assembly);
    return types;
}

魔术发生在对GetTheOneIWant的调用中——这是一个delegate,直到Container配置完成后才会被调用——以下是delegate:的逻辑

private IOneOfMany GetTheOneIWant(Container container)
{
    var settings = container.GetInstance<ISettings>();
    var result = container
        .GetAllInstances<IOneOfMany>()
        .SingleOrDefault(i => i.GetType().Name == settings.IWantThisOnePlease);
    return result;
}

一个简单的测试将确认它按预期工作:

[Test]
public void Container_RegisterAll_ReturnsTheOneSpecifiedByTheSettings()
{
    var container = this.ContainerFactory();
    var result = container.GetInstance<IOneOfMany>();
    Assert.That(result, Is.Not.Null);
}

如前所述,Simple Injector不允许混合注册和解析实例。当从容器中解析第一个类型时,容器将被锁定以进行进一步的更改。之后调用其中一个注册方法时,容器将抛出异常。选择这种设计是为了迫使用户严格地将两个阶段分开,并防止各种可能很容易出现的令人讨厌的并发问题。然而,这种锁定也允许性能优化,使Simple Injector成为该领域中速度最快的。

然而,这意味着你有时需要对注册进行一些不同的思考。然而,在大多数情况下,解决方案相当简单。

例如,在您的示例中,只需让ISomeOtherService实现具有类型为ISettings的构造函数参数即可解决问题。这将允许设置实例在解析时注入该类型:

container.Register<ISettings, Settings>();
container.Register<ISomeOtherService, SomeOtherService>();
// Example
public class SomeOtherService : ISomeOtherService {
    public SomeOtherService(ISettings settings) { ... }
}

另一种解决方案是注册代理人:

container.Register<ISettings, Settings>();
container.Register<ISomeOtherService>(() => new SomeOtherService(
    container.GetInstance<ISettings>().Value));

请注意,这里仍然调用container.GetInstance<ISettings>(),但它嵌入在已注册的Func<ISomeOtherService>委托中。这将保持注册和解析分离。

另一种选择是首先防止具有大型应用程序Settings类。我过去曾经历过,这些类往往会经常更改,并可能使代码复杂化,因为许多类都依赖于该类/抽象,但每个类都使用不同的属性。这表明违反了接口隔离原则。

相反,您也可以将配置值直接注入到需要它的类中:

var conString = ConfigurationManager.ConnectionStrings["Billing"].ConnectionString;
container.Register<IConnectionFactory>(() => new SqlConnectionFactory(conString));

在我构建的最后几个应用程序中,我仍然有一些Settings类,但这个类是我的Composition Root内部的,它本身并没有注入,只是它在注入的地方保存的配置值。它看起来是这样的:

string connString = ConfigurationManager.ConnectionStrings["App"].ConnectionString;
var settings = new AppConfigurationSettings(
    scopedLifestyle: new WcfOperationLifestyle(),
    connectionString: connString,
    sidToRoleMapping: CreateSidToRoleMapping(),
    projectDirectories: ConfigurationManager.AppSettings.GetOrThrow("ProjectDirs"),
    applicationAssemblies:
        BuildManager.GetReferencedAssemblies().OfType<Assembly>().ToArray());
var container = new Container();
var connectionFactory = new ConnectionFactory(settings.ConnectionString);
container.RegisterSingle<IConnectionFactory>(connectionFactory);
container.RegisterSingle<ITimeProvider, SystemClockTimeProvider>();
container.Register<IUserContext>(
    () => new WcfUserContext(settings.SidToRoleMapping), settings.ScopedLifestyle);

更新

关于您的更新,如果我理解正确,您希望允许注册的类型根据配置值进行更改。一个简单的方法如下:

var settings = new Settings();
container.RegisterSingle<ISettings>(settings);
Type theTypeWeWantToRegister = Type.GetType(settings.GetTheISomeOtherServiceType());
container.Register(typeof(ISomeOtherService), theTypeWeWantToRegister);

但是,请仍然考虑根本不注册Settings文件。

但也要注意,需要将类型名称放在配置文件中这样大的灵活性是非常罕见的。通常,您唯一需要的是当您有一个动态插件模型时,在该模型中,可以将插件程序集添加到应用程序中,而无需更改应用程序。

然而,在大多数情况下,您有一组固定的实现,这些实现在编译时就已经知道了。例如,在您的验收和暂存环境中使用的假IMailSender和在生产中使用的真SmptMailSender。由于这两个实现都包含在编译过程中,因此允许指定完整的完全限定类型名称,只会提供比您需要的更多的选项,这意味着会出现更多的错误。

然而,在这种情况下,您只需要一个布尔开关。类似的东西

<add key="IsProduction" value="true" />

在你的代码中,你可以这样做:

container.Register(typeof(IMailSender),
    settings.IsProduction ? typeof(SmtpMailSender) : typeof(FakeMailSender));

这允许此配置具有编译时支持(当名称更改时,配置仍然有效),并使配置文件保持简单。