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

Aru*_*K V 2 c# asp.net-mvc dependency-injection 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();
}
Run Code Online (Sandbox Code Playgroud)

ServiceBase我为获取提供者而创建的一类

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();
}
Run Code Online (Sandbox Code Playgroud)

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

为了获取实例,我使用委托创建了两个静态方法

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>();
    };
}
Run Code Online (Sandbox Code Playgroud)

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

var accountingProvider = CreateAccountingDbContextProvider(container);
container.RegisterInstance(accountingProvider);
var activeDirectryProvider = CreateAccountingDbContextProvider(container);
container.RegisterInstance(activeDirectryProvider);
Run Code Online (Sandbox Code Playgroud)

如果我运行代码,则会收到如下错误

System.InvalidOperationException: '类型 DbContextProvider 已经注册。如果您打算解析 DbContextProvider 实现的集合,请使用 Container.Collection.Register 重载。有关更多信息,请参阅 https://simpleinjector.org/coll1。如果您打算用此新注册替换现有注册,您可以通过将 Container.Options.AllowOverridingRegistrations 设置为 true 来允许覆盖当前注册。有关更多信息,请参阅https://simpleinjector.org/ovrrd

但是当我只尝试使用一种上下文时一切正常,

var accountingProvider = CreateAccountingDbContextProvider(container);
container.RegisterInstance(accountingProvider);
Run Code Online (Sandbox Code Playgroud)

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

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

Ste*_*ven 7

您只有一种类型 ( DbContextProvider) 负责构造AccountingContextActiveDirectryContext。从这个角度来看,创建两个DbContextProvider每个都部分初始化的实例真的很奇怪。消费者不会期望GetAccountingDbContext()返回 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>()
});
Run Code Online (Sandbox Code Playgroud)

或者更好,使DbContextProvider不可变:

container.RegisterInstance<DbContextProvider>(new DbContextProvider(
    activeDirectryDbContextResolver: () => container.GetInstance<ActiveDirectryContext>(),
    accountingDbContextResolver: () => container.GetInstance<AccountingContext>()));
Run Code Online (Sandbox Code Playgroud)

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

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

组合优于继承

首先,你应该摆脱ServiceBase基类。尽管基类看起来还不错,但当它们开始获得自己的依赖项时,它们可能会开始违反单一职责原则及其派生的依赖倒置原则开闭原则

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

相反,不要使用基类,而是执行以下操作:

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

当您遵循此建议时,您最终会得到一组(派生的)服务类,它们依赖于一个只不过是一个空壳的基类。这是您可以完全删除基类的时候。您现在实现的是Composition over Inheritance。与继承相比,组合通常会导致更易于维护的系统。

闭包组合模型

正如 JoostK 提到的,您还可以将DbContext类型直接注入消费者。但是,您是否要这样做取决于您决定使用的合成模型类型。这意味着,当您选择应用闭包组合模型时,您通常应该将DbContext实现直接注入您的使用者。

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

container.RegisterInstance<DbContextProvider>(new DbContextProvider
{
    ActiveDirectryDbContextResolver = () => container.GetInstance<ActiveDirectryContext>(),
    AccountingDbContextResolver = () => container.GetInstance<AccountingContext>()
});
Run Code Online (Sandbox Code Playgroud)

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

container.RegisterInstance<DbContextProvider>(new DbContextProvider(
    activeDirectryDbContextResolver: () => container.GetInstance<ActiveDirectryContext>(),
    accountingDbContextResolver: () => container.GetInstance<AccountingContext>()));
Run Code Online (Sandbox Code Playgroud)

接口隔离原则

你的电流DbContextProvider似乎是围绕Ambient Composition Model设计的。两种合成模型各有优势,您可能特意选择了环境合成模型。

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

所以你可以做的是创建一个允许访问单一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();
    }
    
    ...
}
Run Code Online (Sandbox Code Playgroud)

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

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

// Of course don't forget to register your service classes.
Run Code Online (Sandbox Code Playgroud)

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

public interface IDbContextProvider<T> where T : DbContext
{
    T Context { get; }
}
Run Code Online (Sandbox Code Playgroud)

这个类使用注入ContainerInstanceProducer为给定DbContext类型提取正确的注册。如果未注册,则会引发异常。该InstanceProducer则用来获取DbContextContext被调用。

由于此类依赖于 Simple Injector,因此它应该是您的Composition Root 的一部分。

您可以按如下方式注册:

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();
    }
    
    ...
}
Run Code Online (Sandbox Code Playgroud)