两种类型使用一个子依赖——如何使用不同的实现

Two types use a subdependency - how to use different implementations

假设我们注册了两个类型,RootARootB,每个类型都依赖于 ISubdependency

共享相同的子依赖实现很容易:

services.AddSingleton<ISubdependency, SubdependencyZ>();
services.AddSingleton<IRootA, RootA>();
services.AddSingleton<IRootB, RootB>();

现在的目标是让两种根类型使用子依赖的不同 实现。调用者应该能够注册实例、工厂或类型。

// Instance
services.AddRootA<IRootA, RootA>(options =>
    options.UseSubDependency(new SubdependencyZ()));

// Factory
services.AddRootB<IRootB, RootB>(options =>
    options.UseSubDependency(provider =>
        new SubDependencyY(provider.GetRequiredService<IWhatever>())));

// Type
services.AddRootB<IRootB, RootB>(options =>
    options.UseSubDependency<SubdependencyX>());

我已经设法实现了前两种情况,尽管这里解释的方法有点复杂。然而,第三种情况仍然超出了我的范围。假设如果我们能解决那个问题,我们就能解决所有问题。

所以问题是这样的:

问题

  1. 怎样才能达到预期的效果呢?最好是一种不复杂的方式!

  2. 关于这个问题是否有事实上的标准?是不是普遍认可的用例,对同一个接口的依赖使用不同的实现?或者这通常是完全避免的,迫使依赖者简单地使用相同的实现?

内置的依赖注入不支持您的方案。您正在寻找的是 "Contextual Binding" ,它允许您将名称添加到特定绑定,然后使用该名称在运行时选择您想要的绑定。许多其他包提供了这个开箱即用的功能,但 MS DI 没有。实现该功能并非易事。虽然这个答案没有 "give you an answer" 答案是,你需要自己动手,或者改用第三方库

由于 .Net Core DI 中缺少更复杂的功能,也许最简单的方法是为每个特定的子类型创建标记接口。

interface ISubdependency { }

interface ISubdependencyA : ISubdependency { }

class SubdependencyA : ISubdependencyA { }

interface IRootA {}

class RootA : IRootA
{ 
    public RootA(ISubdependency subdependency)
    {

    }
}

interface ISubdependencyB : ISubdependency { }

class SubdependencyB : ISubdependencyB { }

interface IRootB {}

class RootB : IRootB
{
    public RootB(ISubdependency subdependency)
    {

    }
}

如果可能,最直接的 DI 组合是 Root 类 取决于它们的子系统接口,但如果不可能,您可以使用工厂来注册每个 Root:

services.AddSingleton<ISubdependencyA, SubdependencyA>();
services.AddSingleton<ISubdependencyB, SubdependencyB>();
services.AddSingleton<IRootA, RootA>(provider => new RootA(provider.GetRequiredService<ISubdependencyA>()));
services.AddSingleton<IRootB, RootB>(provider => new RootB(provider.GetRequiredService<ISubdependencyB>()));

另一种可能性,是依赖IEnumerable<ISubdependency>,然后采取合适的方式来处理。

编辑: 到目前为止,我找到了一个更合适的解决方案,我已将其作为单独的答案发布。

我找到了一种(稍微复杂的)方法,无需任何第三方库即可达到预期效果。

// RootA's options object has a fluent extension method to register the subdependency
// This registration will be used ONLY for RootA
public static RootAOptions AddSubdependency<TImplementation>(this RootAOptions options)
    where TImplementation : ISubdependency
{
    // Insert the desired dependency, so that we have a way to resolve it.
    // Register it at index 0, so that potential global registrations stay leading.
    // If we ask for all registered services, we can take the first one.
    // Register it as itself rather than as the interface.
    // This makes it less likely to have a global effect.
    // Also, if RootB registered the same type, we would use either of the identical two.
    options.Services.Insert(0,
        new ServiceDescriptor(typeof(TImplementation), typeof(TImplementation), ServiceLifetime.Singleton));

    // Insert a null-resolver right after it.
    // If the user has not made any other registration, but DOES ask for an instance elsewhere...
    // ...then they will get null, as if nothing was registered, throwing if they'd required it.
    options.Services.Insert(1,
        new ServiceDescriptor(typeof(TImplementation), provider => null, ServiceLifetime.Singleton));

    // Finally, register our required ISubdependencyA, which is how RootA asks for its own variant.
    // Implement it using our little proxy, which forwards to the TImplementation.
    // The TImplementation is found by asking for all registered ones and the one we put at index 0.
    options.Services.AddSingleton<ISubdependencyA>(provider =>
        new SubdependencyAProxy(provider.GetServices<TImplementation>().First()));

    return options;
}

如果要求超出提供的容器的范围,这里有一个使用 Autofac.Extensions.DependencyInjection. Microsoft recommends 使用另一个容器的解决方案。

The built-in service container is meant to serve the needs of the framework and most consumer apps. We recommend using the built-in container unless you need a specific feature that it doesn't support.

回答这个问题的设置包括创建一些类型,仅供说明。我尽量将其保持在最低限度。

我们得到的是:

  • 两种类型都依赖于IDependency
  • IDependency
  • 的两种实现
  • 我们想将 IDependency 的不同实现注入到需要它的每种类型中。
  • IDependency 注入的两个 class 都将其暴露为 属性。这只是为了让我们可以测试该解决方案是否有效。 (我不会在 "real" 代码中这样做。这只是为了说明和测试。)
public interface INeedsDependency
{
    IDependency InjectedDependency { get; }
}

public class NeedsDependency : INeedsDependency
{
    private readonly IDependency _dependency;

    public NeedsDependency(IDependency dependency)
    {
        _dependency = dependency;
    }

    public IDependency InjectedDependency => _dependency;
}

public interface IAlsoNeedsDependency
{
    IDependency InjectedDependency { get; }
}

public class AlsoNeedsDependency : IAlsoNeedsDependency
{
    private readonly IDependency _dependency;

    public AlsoNeedsDependency(IDependency dependency)
    {
        _dependency = dependency;
    }

    public IDependency InjectedDependency => _dependency;
}

public interface IDependency { }

public class DependencyVersionOne : IDependency { }

public class DependencyVersionTwo : IDependency { }

我们如何配置它以便 NeedsDependency 获得 DependencyVersionOne 并且 AlsoNeedsDependency 获得 DependencyVersionTwo

这里是单元测试的形式。以这种方式编写它可以很容易地验证我们是否得到了我们期望的结果。

[TestClass]
public class TestNamedDependencies
{
    [TestMethod]
    public void DifferentClassesGetDifferentDependencies()
    {
        var services = new ServiceCollection();
        var serviceProvider = GetServiceProvider(services);

        var needsDependency = serviceProvider.GetService<INeedsDependency>();
        Assert.IsInstanceOfType(needsDependency.InjectedDependency, typeof(DependencyVersionOne));

        var alsoNeedsDependency = serviceProvider.GetService<IAlsoNeedsDependency>();
        Assert.IsInstanceOfType(alsoNeedsDependency.InjectedDependency, typeof(DependencyVersionTwo));
    }

    private IServiceProvider GetServiceProvider(IServiceCollection services)
    {
        /*
         * With Autofac, ContainerBuilder and Container are similar to
         * IServiceCollection and IServiceProvider.
         * We register services with the ContainerBuilder and then
         * use it to create a Container.
         */

        var builder = new ContainerBuilder();

        /*
         * This is important. If we already had services registered with the
         * IServiceCollection, they will get added to the new container.
         */
        builder.Populate(services);

        /*
         * Register two implementations of IDependency.
         * Give them names. 
         */
        builder.RegisterType<DependencyVersionOne>().As<IDependency>()
            .Named<IDependency>("VersionOne")
            .SingleInstance();
        builder.RegisterType<DependencyVersionTwo>().As<IDependency>()
            .Named<IDependency>("VersionTwo")
            .SingleInstance();

        /*
         * Register the classes that depend on IDependency.
         * Specify the name to use for each one.
         * In the future, if we want to change which implementation
         * is used, we just change the name.
         */
        builder.Register(ctx => new NeedsDependency(ctx.ResolveNamed<IDependency>("VersionOne")))
            .As<INeedsDependency>();
        builder.Register(ctx => new AlsoNeedsDependency(ctx.ResolveNamed<IDependency>("VersionTwo")))
            .As<IAlsoNeedsDependency>();

        // Build the container
        var container = builder.Build();

        /*
         * This last step uses the Container to create an AutofacServiceProvider,
         * which is an implementation of IServiceProvider. This is the IServiceProvider
         * our app will use to resolve dependencies.
         */
        return new AutofacServiceProvider(container);
    }
}

单元测试解析了这两种类型并验证我们是否注入了我们所期望的。

现在,我们如何将其放入 "real" 应用程序中?

在你的Startupclass中,改变

public void ConfigureServices(IServiceCollection services)

public IServiceProvider ConfigureServices(IServiceCollection services)

现在 ConfigureServices 将 return 和 IServiceProvider

然后,您可以将 Autofac ContainerBuilder 步骤添加到 ConfigureServices 并具有方法 return new AutofacServiceProvider(container);

如果您已经在 IServiceCollection 注册服务,那很好。保持原样。无论您需要向 Autofac ContainerBuilder 注册任何服务,请注册这些服务。

只要确保包括这一步:

builder.Populate(services);

因此,在 IServiceCollection 中注册的内容也会添加到 ContainerBuilder


这可能看起来有点令人费解,而不是仅仅让某些东西与提供的 IoC 容器一起工作。好处是一旦你克服了这个困难,你就可以利用其他容器可以做的有用的事情。您甚至可能决定使用 Autofac 来注册所有依赖项。您可以搜索不同的方法来注册和使用 Autofac 的命名或键控依赖项,所有这些选项都可供您使用。 (他们的 documentation is great.) You can also use Windsor 或其他人。

依赖注入早在 Microsoft.Extensions.DependencyInjectionIServiceCollectionIServiceProvider 之前就存在了。它有助于学习如何使用不同的工具做相同或相似的事情,这样我们就可以使用基本概念,而不仅仅是特定的实现。

这是来自 Autofac 的 some more documentation,具体介绍了如何将其与 ASP.NET Core 一起使用。

如果您确切知道要如何组合每个 class,则可以组合它们 "manually" 并将这些实例注册到容器中。如果 classes 被注册为单例,这就特别容易了,就像你的问题一样,但即使它们是瞬态的或范围内的,它也可以应用。

通常把它写成扩展。

public static class YourServiceExtensions
{
    public static IServiceCollection AddYourStuff(this IServiceCollection services)
    {
        services.AddSingleton<SubdependencyOne>();
        services.AddSingleton<SubdependencyTwo>();
        services.AddSingleton<IRootA>(provider =>
        {
            var subdependency = provider.GetService<SubdependencyOne>();
            return new RootA(subdependency);
        });
        services.AddSingleton<IRootB>(provider =>
        {
            var subdependency = provider.GetService<SubdependencyTwo>();
            return new RootB(subdependency);
        });
        return services;
    }
}

然后,在Startup

services.AddYourStuff();

即使涉及到一些更复杂的依赖关系,也没关系。您只需为每个 class 编写一次,而消费者并不关心所有内容是如何编写的——他们只是调用扩展。 (虽然您正在向容器注册组合实例,但这类似于我们所说的 pure DI。)

这比试图让 IServiceProvider 找出要解决的依赖关系以及何时解决要简单得多。

我找到了合适的解决方案。

// Startup.cs: usage

public void ConfigureServices(IServiceCollection services)
{
    // ...

    // This is what the application will use when directly asking for an ISubdependency
    services.AddSingleton<ISubdependency, SubdependencyZ>();
    // This is what we will have RootA and RootB use below
    services.AddSingleton<SubdependencyA>();
    services.AddSingleton<SubdependencyB>();

    services.AddRootA(options => options.UseSubdependency<SubdependencyA>());
    services.AddRootB(options => options.UseSubdependency<SubdependencyB>());

    // ...
}

// RootAExtensions.cs: implementation

public static IServiceCollection AddRootA(this IServiceCollection services, Action<Options> options)
{
    var optionsObject = new Options();
    options(optionsObject); // Let the user's action manipulate the options object

    // Resolve the chosen subdependency at construction time
    var subdependencyType = optionsObject.SubdependencyType;
    services.AddSingleton<IRootA>(serviceProvider =>
        new RootA(serviceProvider.GetRequiredService(subdependencyType)));

    return services;
}

public sealed class Options
{
    public IServiceCollection Services { get; }

    internal Type SubdependencyType { get; set; } = typeof(ISubdependency); // Interface by default

    public Options(IServiceCollection services)
    {
        this.Services = services;
    }
}

// Instructs the library to use the given subdependency
// By default, whatever is registered as ISubdependency is used
public static Options UseSubdependency<TSubdependency>(this Options options)
    where TSubdependency : class, ISubdependency
{
    options.SubdependencyType = typeof(TSubdependency);
    return options;
}

首先,用户注册任何与子依赖相关的东西。在这个例子中,我考虑了应用程序也直接使用子依赖的情况,直接使用调用另一个实现而不是库 RootA 的使用,而库又调用另一个实现而不是 RootB。

在注册所有这些之后(或之前 - 从技术上讲顺序无关紧要),用户注册高级依赖项 RootA 和 RootB。他们的选项允许用户指定要使用的子依赖类型。

查看实现,您可以看到我们使用 AddSingleton 基于工厂的重载 ,这让我们可以在构造时向服务提供者询问任何子依赖项时间.

该实现还将要使用的类型初始化为 typeof(ISubdependency)。如果用户要忽略 UseSubdependency 方法,将使用:

services.AddRootA(options => { }); // Will default to asking for an `ISubdependency`

如果用户未能为 ISubdependency 注册一个实现,将获得通常的异常。


请注意,我们绝不允许用户以嵌套方式注册 事物。这会令人困惑:它 看起来 好像注册只是针对包装它的东西,但由于容器是一个平面集合,它 实际上 全球注册.

相反,我们只允许用户引用他们在别处明确注册的内容。这样就不会引入混淆了。

the code is intended for use in NuGet packages, where the consuming application chooses the container

库或框架不应依赖于任何 DI 容器,甚至 Conforming Container。这也排除了 .NET 中的内置容器。

相反,design libraries so that they're friendly to any implementation of DI

如果RootA依赖于ISubdependency,只需将其作为依赖项,并使用构造函数注入来宣传它:

public RootA(ISubdependency)

如果RootB也有相同的依赖,使用相同的模式:

public RootB(ISubdependency)

图书馆的任何消费者现在都可以按照他们喜欢的方式配置 RootARootB 的实例。使用 Pure DI,它就像 new 建立对象一样简单:

var rootA = new RootA(new SubdependencyX(/* inject dependencies here if needed */));
var rootB = new RootB(subdependencyY); // subdependencyY is an existing instance...

任何客户端也可以自由选择自己选择的 DI 容器,这可能会或可能不会解决复杂的依赖项选择场景。这为所有客户提供了充分的灵活性,而不会将任何人限制在合规容器提供的最低公分母内。

就灵活性而言,您无法击败 Pure DI,因为它基于 C#(或 Visual Basic、F# 等)的全部功能,而不是API 可能会或可能不会公开您需要的功能。