如何在简单注入器中注册实例集合

How to register collection of instances in Simple injector

在我的项目中,我有多个数据库上下文,因此我创建了一个提供程序来根据需要获取上下文对象。 我的提供商看起来像这样。

public class DbContextProvider
{
    public Func<AccountingContext> AccountingDbContextResolver { get; set; }

    public Func<ActiveDirectryContext> ActiveDirectryDbContextResolver { get; set; }

    public AccountingContext GetAccountingDbContext() =>
        this.AccountingDbContextResolver();

    public ActiveDirectryContext GetActiveDirectryDbContext() =>
        this.ActiveDirectryDbContextResolver();
}

一个 ServiceBase class 我为获取提供程序而创建

public class ServiceBase
{
    public ServiceBase(DbContextProvider contextProvider)
    {
        this.ContextProvider = contextProvider;
    }

    protected DbContextProvider ContextProvider { get; }

    public AccountingContext AccountingDbContext =>
        this.ContextProvider.GetAccountingDbContext();

    public ActiveDirectryContext ActiveDirectryDbContext =>
        this.ContextProvider.GetActiveDirectryDbContext();
}

我正在使用简单的注入器,我需要创建两个数据库上下文的实例。

为了获取实例,我创建了两个带有委托的静态方法

private static DbContextProvider CreateActiveDirectryDbContextProvider(Container container)
{
    return new DbContextProvider
    {
        ActiveDirectryDbContextResolver =
            () => container.GetInstance<ActiveDirectryContext>();
    };
}

private static DbContextProvider CreateAccountingDbContextProvider(Container container)
{
    return new DbContextProvider
    {
        AccountingDbContextResolver = () => container.GetInstance<AccountingContext>();
    };
}

对于注册,我使用了以下代码

var accountingProvider = CreateAccountingDbContextProvider(container);
container.RegisterInstance(accountingProvider);
var activeDirectryProvider = CreateAccountingDbContextProvider(container);
container.RegisterInstance(activeDirectryProvider);

如果我 运行 代码然后我收到如下错误

System.InvalidOperationException: 'Type DbContextProvider has already been registered. If your intention is to resolve a collection of DbContextProvider implementations, use the Container.Collection.Register overloads. For more information, see https://simpleinjector.org/coll1. If your intention is to replace the existing registration with this new registration, you can allow overriding the current registration by setting Container.Options.AllowOverridingRegistrations to true. For more information, see https://simpleinjector.org/ovrrd.'

但是当我只尝试一个上下文

时一切正常
var accountingProvider = CreateAccountingDbContextProvider(container);
container.RegisterInstance(accountingProvider);

我已尝试使用 container.Collection.Register 注册它,没有出现错误,但我没有在服务层中获取实例,我在那里收到空引用异常。

有人可以帮我解决这个问题吗?

您只有一种类型 (DbContextProvider) 负责构建 AccountingContextActiveDirectryContext。从这个角度来看,创建两个每个都部分初始化的 DbContextProvider 实例真的很奇怪。消费者不会期望 GetAccountingDbContext() 为 return null 或抛出 NullReferenceException。因此,您应该创建一个可用于两种情况的单一实例:

container.Register<ActiveDirectryContext>(Lifestyle.Scoped);
container.Register<AccountingContext>(Lifestyle.Scoped);

container.RegisterInstance<DbContextProvider>(new DbContextProvider
{
    ActiveDirectryDbContextResolver = () => container.GetInstance<ActiveDirectryContext>(),
    AccountingDbContextResolver = () => container.GetInstance<AccountingContext>()
});

或者更好,使 DbContextProvider 不可变:

container.RegisterInstance<DbContextProvider>(new DbContextProvider(
    activeDirectryDbContextResolver: () => container.GetInstance<ActiveDirectryContext>(),
    accountingDbContextResolver: () => container.GetInstance<AccountingContext>()));

这解决了问题,因为 DbContextProvider 没有只有一个注册。这消除了歧义,防止了可能的错误,并且是一个更简单的解决方案。

虽然这可行,但我想建议对您的设计进行一些更改。

组合优于继承

首先,你应该去掉ServiceBase基数class。尽管基础 classes 看起来不错,但当它们开始获得自己的依赖项时,它们可能会开始违反 单一职责原则 ,而它们的派生词 依赖倒置原则Open/Closed原则:

  • 具有依赖关系的基础 classes 通常成为功能的大杂烩——通常是横切关注点。基础 class 成为不断增长的 class。不断增长的 classes 表明违反了单一职责原则。
  • 当基础 class 开始包含逻辑时,导数自动依赖于该行为——导数 总是 与其基础 class 强耦合.这使得很难单独测试导数。如果您想替换或模拟基 class 的行为,则意味着它的行为是 Volatile。当 class 与 Volatile Dependency 紧密耦合时,这意味着您违反了依赖倒置原则。
  • 基础 class 的构造函数依赖项需要由派生的 class 的构造函数提供。当基础 class 需要新的依赖项时,这将导致彻底的变化,因为所有派生的构造函数也需要更新。

而不是使用基础 classes,执行以下操作:

  • 不是将派生 class 的依赖项转发到基础 class 构造函数,派生 class 应该将依赖项本身存储在私有字段中。它可以直接使用该依赖项。
  • 如果基础 class 包含代码以外的行为:
    • 如果行为是易变的,将逻辑包装在 class 中,将 class 隐藏在抽象后面并将 class(通过其抽象)注入构造函数派生 class。执行此操作时,很容易看出派生的 class 具有哪些依赖项。
    • 如果行为是稳定的,您可以使用静态帮助程序 classes 或扩展方法使基础 class 的行为可重用。
    • 如果行为涉及横切关注点,请考虑使用装饰器或动态拦截作为基础 classes 的替代方案。

当你遵循这个建议时,你最终得到的是一组(派生的)服务classes,它们依赖于一个基础class,它只不过是一个空的[=145] =].这是您可以完全删除基础 class 的时候。你现在的成就是Composition over Inheritance。组合通常会导致比继承更易于维护的系统。

闭包组合模型

正如 JoostK 提到的,您还可以将 DbContext 类型直接注入消费者。然而,您是否想要这样做取决于 type of composition model you decided to use. What this means is that, when you choose to apply the Closure Composition Model,您通常应该将 DbContext 实现直接注入您的消费者。

public class ProduceIncomeTaxService : IHandler<ProduceIncomeTax>
{
    private readonly AccountingContext context;

    // Inject stateful AccountingContext directly into constructor
    public ProduceIncomeTaxService(AccountingContext context) => this.context = context;

    public void Handle(ProduceIncomeTax command)
    {
        var record = this.context.AccountingRecords
            .Single(r => r.Id == command.Id);
        
        var tax = CalculateIncomeTax(record);
        
        FaxIncomeTax(tax);
        
        this.context.SaveChanges();
    }
    
    ...
}

这简化了您系统的注册,因为现在您只需注册 DbContext 实现即可:

container.Register<ActiveDirectryContext>(Lifestyle.Scoped);
container.Register<AccountingContext>(Lifestyle.Scoped);

// Of course don't forget to register your service classes.

接口隔离原则

您当前的 DbContextProvider 似乎是围绕两个构图模型的 Ambient Composition Model. There are advantages 设计的,您可能有意选择了环境构图模型。

然而,DbContextProvider 仍然公开了许多 (10) 个属性——每个 DbContext 一个。 类 和许多方法的抽象会导致许多与可维护性有关的问题。这源于规定狭义抽象的接口隔离原则。因此,不是注入一个广泛的提供程序实现来访问单个 DbContext 类型。实现通常只需要访问单个 DbContext 类型。如果他们需要多个,class 几乎肯定应该分成更小、更集中的 classes。

所以您可以做的是创建一个通用抽象,允许访问单个 DbContext 类型:

public interface IDbContextProvider<T> where T : DbContext
{
    T Context { get; }
}

在消费者中使用时,如下所示:

public class ProduceIncomeTaxService : IHandler<ProduceIncomeTax>
{
    private readonly IDbContextProvider<AccountingContext> provider;

    // Inject an ambient context provider into the constructor
    public ProduceIncomeTaxService(IDbContextProvider<AccountingContext> provider)
        => this.provider = provider;

    public void Handle(ProduceIncomeTax command)
    {
        var record = this.provider.Context.AccountingRecords
            .Single(r => r.Id == command.Id);
        
        var tax = CalculateIncomeTax(record);
        
        FaxIncomeTax(tax);
        
        this.provider.Context.SaveChanges();
    }
    
    ...
}

有多种实现方式IDbContextProvider<T>,但是您可以,例如,创建一个直接依赖于 Simple Injector 的实现:

public sealed class SimpleInjectorDbContextProvider<T> : IDbContextProvider<T>
    where T : DbContext
{
    private readonly InstanceProducer producer;

    public SimpleInjectorDbContextProvider(Container container)
    {
        this.producer = container.GetCurrentRegistrations()
            .FirstOrDefault(r => r.ServiceType == typeof(T))
            ?? throw new InvalidOperationException(
                $"You forgot to register {typeof(T).Name}. Please call: " +
                $"container.Register<{typeof(T).Name}>(Lifestyle.Scope);");
    }
    
    public T Context => (T)this.producer.GetInstance();
}

此 class 使用注入的 Container 为给定的 DbContext 类型提取正确的 InstanceProducer 注册。如果未注册,则会抛出异常。 InstanceProducer 然后在调用 Context 时用于获取 DbContext

因为这个 class 依赖于 Simple Injector,它应该是你的 Composition Root 的一部分。

您可以通过以下方式注册:

container.Register<ActiveDirectryContext>(Lifestyle.Scoped);
container.Register<AccountingContext>(Lifestyle.Scoped);

container.Register(
    typeof(IDbContextProvider<>),
    typeof(SimpleInjectorDbContextProvider<>),
    Lifestyle.Singleton);