使用 IoC 容器时如何将参数传递给构造函数?

How to pass parameters into constructors when using IoC containers?

啊啊!我在这里拉头发。我一直在尝试使用 IoC 容器,在遇到一些您认为非常基本的问题(例如将参数传递给构造函数)之前,一切看起来都很好而且花花公子。

假设我有一个 class 某处混合了可以由 IoC 解析的引用 classes 和只能在运行时解析的值类型(或其他一些类型):

public NFLFeedUnitOfWork(NFLFileType fileType, object feed, IConverterMappings<NFLFileType> nflConverterMappings, IDbContext context)
    : base(fileType, feed, nflConverterMappings, context, ContextType.NFL)
{
    //new NFLContext(connstringname, setAutoDetectChanges)
}

在这个特定的示例中,我传入了枚举 (NFLFileType)、对象实例、2 个接口参数,并将一个额外的硬编码 属性 传入基本构造函数 (ContextType.NFL)

我怎么能在任何 IoC 容器中执行此操作?

问题其实有两层:

1.) 如何传入一个只有运行时才知道的对象?举例来说,此时调用代码如下所示:

protected override IFeedUnitOfWork GetUnitOfWork(NFLFileType fileType, object feed, string connectionString)
{
    return new NFLFeedUnitOfWork(fileType, feed, new NFLConverterMappings(), new NFLContext(connectionString));
}

如何将此代码转换为使用 IoC? 也许是这样的?

protected override IFeedUnitOfWork GetUnitOfWork(NFLFileType fileType, object feed, string connectionString)
{
    return IFLFeedUnitOfWork(fileType, feed);
}

哪里最后2个参数自动解析,第1个2我自己补?

2.) 如何使用 IoC 将枚举、对象、值类型传递给构造函数? (或者在此特定情况下可能避免使用它?)

无论如何,非常感谢任何帮助,尤其是在第一点上。 我目前正在使用 Unity,但任何其他 IoC 容器也可以。

我也不想在代码中传入一个IoC容器,我只想在顶层的一个地方指定它。

您有几个不同的选择,具体取决于值的来源。 (我只熟悉 Ninject,我假设 Unity 具有类似的功能)

如果数据依赖于其他服务或存储库,您可以将对象绑定到委托,以便在满足请求时解析它。例如:

Bind<NFLFileType>().ToMethod( context => context.Kernel.Get<IConfigProvider>().NFLFileType );

Ninject 还支持仅在特定情况下使用 .When() 语法解析的绑定。例如:

Bind<NFLFileType>().ToConstant( NFLFileType.MyValue ).WhenInjectedInto<NFLFeedUnitOfWork>();

Bind<NFLFileType>().ToConstant( NFLFileType.MyValue ).When(request => request.Target.Type.GetCustomAttributes(typeof(MyValueAttribute)) != null );

如果所有其他方法都失败了,您可以将注入的接口绑定到像工厂模式一样工作的 Provider 以生成对象:

Bind<IFeedUnitOfWork>().ToProvider<UnitOfWorkProvider>();

并且提供者知道如何根据上下文解析前 2 个参数。

I have a class somewhere with a mix of reference classes that can be resolved by IoC and value types (or some other types) that can only be resolved at runtime

这就是你出错的地方。编写组件时,不应将编译时依赖项与运行时数据混合在一起。您的对象图应该是静态的(最好是无状态的),运行时数据应该在构建完整的对象图后使用方法调用通过对象图传递。这可以极大地简化您的应用程序的开发,因为它允许静态验证您的对象图(使用工具、单元测试或使用纯 DI)并且它可以防止您今天遇到的麻烦。

一般来说,您有两种选择来解决这个问题:

  1. 您通过方法调用将数据传递给较低级别​​的组件。
  2. 您通过在注入的组件上调用方法来检索数据。

采取哪种解决方案取决于上下文。

如果数据特定于已处理的请求并且是您正在处理的用例的一部分,您通常会选择选项一。例如:

public interface IFeedUnitOfWorkProvider
{
    IFeedUnitOfWork GetUnitOfWork(NFLFileType fileType, object feed);
}

此处 IFeedUnitOfWorkProvider 包含一个需要运行时参数作为输入的 GetUnitOfWork 方法。实现可能如下所示:

public class FeedUnitOfWorkProvider : IFeedUnitOfWorkProvider
{
    private readonly IConverterMappings converterMappings;
    private readonly IContext context;

    public FeedUnitOfWorkProvider(IConverterMappings converterMappings,
        IContext context) {
        this.converterMappings = converterMappings;
        this.context = context;
    }

    public IFeedUnitOfWork GetUnitOfWork(NFLFileType fileType, object feed) {
        return new NFLFeedUnitOfWork(fileType, feed, this.converterMappings,
            this.context);
    }       
}

这里有几点需要注意:

  • 所有静态已知的依赖项都是通过构造函数注入的,而运行时值是通过方法调用注入的。
  • connectionString 值对于 FeedUnitOfWorkProvider 是未知的。它不是直接依赖项,提供者不必知道它的存在。
  • 假设 connectionString 是一个在运行时不会改变的配置值(通常存储在应用程序的配置文件中),它可以被注入到 NFLContext 中,方法与注入了其他依赖项。请注意,配置值不同于运行时值,因为它在应用程序的生命周期内不会更改。

当您处理上下文信息时,第二个选项特别有用。这是对实现很重要的信息,但不应像前面的示例那样传入。一个很好的例子是有关代表请求的用户的信息 运行。对此的典型抽象如下所示:

public interface IUserContext {
    string CurrentUserName { get; }
}

这个接口可以注入任何消费者。使用这种抽象,消费者可以在运行时查询用户名。将用户名与请求数据的其余部分一起传递通常会很尴尬,因为这将允许调用者更改(或忘记)用户名,从而使代码更难使用、更难测试、更容易出错。相反,我们可以使用 IUserContext:

public IFeedUnitOfWork GetUnitOfWork(NFLFileType fileType, object feed) {
    if (this.userContext.CurrentUserName == "steven") {
        return new AdminUnitOfWork(this.context);
    }
    return new NFLFeedUnitOfWork(fileType, feed, this.converterMappings,
        this.context);
}

IUserContext 实现的外观在很大程度上取决于您正在构建的应用程序类型。对于 ASP.NET 我想是这样的:

public class AspNetUserContext : IUserContext {
    string CurrentUserName {
        get { return HttpContext.Current.User.Name; }
    }
}