SimpleInjector——注册对象,它依赖于另一个注册对象的值

时间:2022-04-19 07:18:50

Using SimpleInjector, I am trying to register an entity that depends on values retrieved from another registered entity. For example:

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

Settings - Reads settings values that indicate the type of SomeOtherService the app needs.

设置——读取显示应用程序需要的其他服务类型的设置值。

SomeOtherService - Relies on a value from Settings to be instantiated (and therefore registered).

SomeOtherService——依赖于要实例化(因此要注册)的设置中的值。

Some DI containers allow registering an object after resolution of another object. So you could do something like the pseudo code below:

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

    container.Register<ISettings, Settings>();

    var settings = container.Resolve<ISettings>();

    System.Type theTypeWeWantToRegister = Type.GetType(settings.GetTheISomeOtherServiceType());

    container.Register(ISomeOtherService, theTypeWeWantToRegister);

SimpleInjector does not allow registration after resolution. Is there some mechanism in SimpleInjector that allows the same architecture?

SimpleInjector不允许在解析后注册。在SimpleInjector中是否存在允许相同架构的机制?

2 个解决方案

#1


2  

A simple way to get this requirement is to register all of the available types that may be required and have the configuration ensure that the container returns the correct type at run time ... it's not so easy to explain in English so let me demonstrate.

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

You can have multiple implementations of an interface but at runtime you want one of them, and the one you want is governed by a setting in a text file - a string. Here are the test classes.

你可以有一个接口的多个实现,但是在运行时你想要其中的一个,而你想要的那个是由一个文本文件中的一个设置控制的——一个字符串。这里是测试类。

public interface IOneOfMany { }
public class OneOfMany1 : IOneOfMany { }
public class OneOfMany2 : IOneOfMany { }

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

So let's go ahead and register them all:

我们来注册一下

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;
}

The magic happens in the call to GetTheOneIWant - this is a delegate and will not get called until after the Container configuration has completed - here's the logic for the delegate:

调用GetTheOneIWant时发生了神奇的事情——这是一个委托,在容器配置完成之后才会被调用——委托的逻辑如下:

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

    return result;
}

A simple test will confirm it works as expected:

一个简单的测试将证实它的工作原理:

[Test]
public void Container_RegisterAll_ReturnsTheOneSpecifiedByTheSettings()
{
    var container = this.ContainerFactory();

    var result = container.GetInstance<IOneOfMany>();

    Assert.That(result, Is.Not.Null);
}

#2


1  

As you already stated, Simple Injector does not allow mixing registration and resolving instances. When the first type is resolved from the container, the container is locked for further changes. When a call to one of the registration methods is made after that, the container will throw an exception. This design is chosen to force the user to strictly separate the two phases, and prevents all kinds of nasty concurrency issues that can easily come otherwise. This lock down however also allows performance optimizations that make Simple Injector the fastest in the field.

如前所述,简单的注入器不允许混合注册和解析实例。当从容器解析第一个类型时,容器被锁定以进行进一步的更改。当调用其中一个注册方法之后,容器将抛出异常。选择这个设计是为了强制用户严格区分两个阶段,并防止所有可能出现的恶意并发问题。然而,这种锁定也允许性能优化,使简单的注入器成为领域中最快的。

This does however mean that you sometimes need to think a little bit different about doing your registrations. In most cases however, the solution is rather simple.

但是,这确实意味着您有时需要对注册有一些不同的想法。然而,在大多数情况下,解决方案相当简单。

In your example for instance, the problem would simply be solved by letting the ISomeOtherService implementation have a constructor argument of type ISettings. This would allow the settings instance to be injected into that type when it is resolved:

例如,在您的示例中,问题将通过让ISomeOtherService实现具有类型ISettings的构造函数参数来解决。这将允许在解决时将设置实例注入到该类型中:

container.Register<ISettings, Settings>();
container.Register<ISomeOtherService, SomeOtherService>();

// Example
public class SomeOtherService : ISomeOtherService {
    public SomeOtherService(ISettings settings) { ... }
}

Another solution is to register a delegate:

另一个解决方案是注册一个委托:

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

Notice how container.GetInstance<ISettings>() is still called here, but it is embedded in the registered Func<ISomeOtherService> delegate. This will keep the registration and resolving separated.

注意,这里仍然调用container.GetInstance (),但它嵌入到注册的Func 委托中。这将使注册和解析保持分离。

Another option is to prevent having a large application Settings class in the first place. I experienced in the past that those classes tend to change quite often and can complicate your code because many classes will depend on that class/abstraction, but every class uses different properties. This is an indication of a Interface Segregation Principle violation.

另一种选择是首先避免使用大型应用程序设置类。我过去有过这样的经历:这些类往往会经常更改,并且会使代码变得复杂,因为许多类都依赖于这个类/抽象,但是每个类都使用不同的属性。这表明违反了接口隔离原则。

Instead, you can also inject configuration values directly into classes that require it:

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

var conString = ConfigurationManager.ConnectionStrings["Billing"].ConnectionString;

container.Register<IConnectionFactory>(() => new SqlConnectionFactory(conString));

In the last few application's I built, I still had some sort of Settings class, but this class was internal to my Composition Root and was not injected itself, but only the configuration values it held where injected. It looked like this:

在我构建的最后几个应用程序中,我仍然有一些设置类,但是这个类是在我的组合根内部的,并没有注入自己,而是只注入了它所保存的配置值。它看起来像这样:

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);

UPDATE

更新

About your update, if I understand correctly, you want to allow the registered type to change based on a configuration value. A simple way to do this is as follows:

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

var settings = new Settings();

container.RegisterSingle<ISettings>(settings);

Type theTypeWeWantToRegister = Type.GetType(settings.GetTheISomeOtherServiceType());

container.Register(typeof(ISomeOtherService), theTypeWeWantToRegister);

But please still consider not registering the Settings file at all.

但是请仍然考虑不注册设置文件。

Also note though that it's highly unusual to need that much flexibility that the type name must be placed in the configuration file. Usually the only time you need this is when you have a dynamic plugin model where a plugin assembly can be added to the application, without the application to change.

还需要注意的是,非常不寻常的是,需要如此大的灵活性才能将类型名放置到配置文件中。通常情况下,只有当你有一个动态的插件模型时,插件程序集可以被添加到应用程序中,而不需要改变应用程序。

In most cases however, you have a fixed set of implementations that are already known at compile time. Take for instance a fake IMailSender that is used in your acceptance and staging environment and the real SmptMailSender that is used in production. Since both implementations are included during compilation, allowing to specify the complete fully qualified type name, just gives more options than you need, and means that there are more errors to make.

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

What you just need in that case however, is a boolean switch. Something like

在这种情况下,你只需要一个布尔开关。类似的

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

And in your code, you can do this:

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

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

This allows this configuration to have compile-time support (when the names change, the configuration still works) and it keeps the configuration file simple.

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

#1


2  

A simple way to get this requirement is to register all of the available types that may be required and have the configuration ensure that the container returns the correct type at run time ... it's not so easy to explain in English so let me demonstrate.

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

You can have multiple implementations of an interface but at runtime you want one of them, and the one you want is governed by a setting in a text file - a string. Here are the test classes.

你可以有一个接口的多个实现,但是在运行时你想要其中的一个,而你想要的那个是由一个文本文件中的一个设置控制的——一个字符串。这里是测试类。

public interface IOneOfMany { }
public class OneOfMany1 : IOneOfMany { }
public class OneOfMany2 : IOneOfMany { }

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

So let's go ahead and register them all:

我们来注册一下

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;
}

The magic happens in the call to GetTheOneIWant - this is a delegate and will not get called until after the Container configuration has completed - here's the logic for the delegate:

调用GetTheOneIWant时发生了神奇的事情——这是一个委托,在容器配置完成之后才会被调用——委托的逻辑如下:

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

    return result;
}

A simple test will confirm it works as expected:

一个简单的测试将证实它的工作原理:

[Test]
public void Container_RegisterAll_ReturnsTheOneSpecifiedByTheSettings()
{
    var container = this.ContainerFactory();

    var result = container.GetInstance<IOneOfMany>();

    Assert.That(result, Is.Not.Null);
}

#2


1  

As you already stated, Simple Injector does not allow mixing registration and resolving instances. When the first type is resolved from the container, the container is locked for further changes. When a call to one of the registration methods is made after that, the container will throw an exception. This design is chosen to force the user to strictly separate the two phases, and prevents all kinds of nasty concurrency issues that can easily come otherwise. This lock down however also allows performance optimizations that make Simple Injector the fastest in the field.

如前所述,简单的注入器不允许混合注册和解析实例。当从容器解析第一个类型时,容器被锁定以进行进一步的更改。当调用其中一个注册方法之后,容器将抛出异常。选择这个设计是为了强制用户严格区分两个阶段,并防止所有可能出现的恶意并发问题。然而,这种锁定也允许性能优化,使简单的注入器成为领域中最快的。

This does however mean that you sometimes need to think a little bit different about doing your registrations. In most cases however, the solution is rather simple.

但是,这确实意味着您有时需要对注册有一些不同的想法。然而,在大多数情况下,解决方案相当简单。

In your example for instance, the problem would simply be solved by letting the ISomeOtherService implementation have a constructor argument of type ISettings. This would allow the settings instance to be injected into that type when it is resolved:

例如,在您的示例中,问题将通过让ISomeOtherService实现具有类型ISettings的构造函数参数来解决。这将允许在解决时将设置实例注入到该类型中:

container.Register<ISettings, Settings>();
container.Register<ISomeOtherService, SomeOtherService>();

// Example
public class SomeOtherService : ISomeOtherService {
    public SomeOtherService(ISettings settings) { ... }
}

Another solution is to register a delegate:

另一个解决方案是注册一个委托:

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

Notice how container.GetInstance<ISettings>() is still called here, but it is embedded in the registered Func<ISomeOtherService> delegate. This will keep the registration and resolving separated.

注意,这里仍然调用container.GetInstance (),但它嵌入到注册的Func 委托中。这将使注册和解析保持分离。

Another option is to prevent having a large application Settings class in the first place. I experienced in the past that those classes tend to change quite often and can complicate your code because many classes will depend on that class/abstraction, but every class uses different properties. This is an indication of a Interface Segregation Principle violation.

另一种选择是首先避免使用大型应用程序设置类。我过去有过这样的经历:这些类往往会经常更改,并且会使代码变得复杂,因为许多类都依赖于这个类/抽象,但是每个类都使用不同的属性。这表明违反了接口隔离原则。

Instead, you can also inject configuration values directly into classes that require it:

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

var conString = ConfigurationManager.ConnectionStrings["Billing"].ConnectionString;

container.Register<IConnectionFactory>(() => new SqlConnectionFactory(conString));

In the last few application's I built, I still had some sort of Settings class, but this class was internal to my Composition Root and was not injected itself, but only the configuration values it held where injected. It looked like this:

在我构建的最后几个应用程序中,我仍然有一些设置类,但是这个类是在我的组合根内部的,并没有注入自己,而是只注入了它所保存的配置值。它看起来像这样:

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);

UPDATE

更新

About your update, if I understand correctly, you want to allow the registered type to change based on a configuration value. A simple way to do this is as follows:

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

var settings = new Settings();

container.RegisterSingle<ISettings>(settings);

Type theTypeWeWantToRegister = Type.GetType(settings.GetTheISomeOtherServiceType());

container.Register(typeof(ISomeOtherService), theTypeWeWantToRegister);

But please still consider not registering the Settings file at all.

但是请仍然考虑不注册设置文件。

Also note though that it's highly unusual to need that much flexibility that the type name must be placed in the configuration file. Usually the only time you need this is when you have a dynamic plugin model where a plugin assembly can be added to the application, without the application to change.

还需要注意的是,非常不寻常的是,需要如此大的灵活性才能将类型名放置到配置文件中。通常情况下,只有当你有一个动态的插件模型时,插件程序集可以被添加到应用程序中,而不需要改变应用程序。

In most cases however, you have a fixed set of implementations that are already known at compile time. Take for instance a fake IMailSender that is used in your acceptance and staging environment and the real SmptMailSender that is used in production. Since both implementations are included during compilation, allowing to specify the complete fully qualified type name, just gives more options than you need, and means that there are more errors to make.

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

What you just need in that case however, is a boolean switch. Something like

在这种情况下,你只需要一个布尔开关。类似的

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

And in your code, you can do this:

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

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

This allows this configuration to have compile-time support (when the names change, the configuration still works) and it keeps the configuration file simple.

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