0

假设我们注册了两种类型,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>());

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

所以问题是这样的:

  • RootARootB依赖ISubdependency.
  • 其他类型也可能取决于 ISubdependency。
  • 如果我们注册一个特定的实现,例如services.AddSingleton<ISubdependency, SubdependencyZ>(),那么该注册是全局的(对于容器),它会覆盖任何以前的注册ISubdependency。结果,最后一次注册最终被用于所有家属!
  • 特别是基于类型的注册(上面的场景 3)具有挑战性,因为我们只有类型,没有简单的方法来解析实例。这意味着我们必须求助于让容器解析注册的类型,这使得解决前面的要点变得更加困难。
  • 我们必须坚持使用 .NET Core 的 IOC 扩展。我们不允许依赖特定的第三方容器。编辑:这是因为代码旨在用于 NuGet 包,其中消费应用程序选择容器。

问题

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

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

4

7 回答 7

2

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

// 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 的使用,而库 RootA 又调用了另一个实现而不是 RootB。

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

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

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

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

如果用户未能为 注册实现ISubdependency,则获取通常的异常。


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

相反,我们只允许用户引用他们在其他地方明确注册的东西。这样,就不会引入混乱。

于 2019-08-26T13:50:19.533 回答
2

内置的依赖注入不支持您的方案。您正在寻找的是“上下文绑定”,它允许您向特定绑定添加名称,然后使用该名称在运行时选择您想要的绑定。许多其他软件包开箱即用地提供此功能,但 MS DI 没有。实现该功能并非易事。虽然这个答案没有“给你答案”,但你需要自己动手,或者改用第三方库

于 2019-08-14T12:02:12.490 回答
1

由于 .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>然后采取适当的方法来使用。

于 2019-08-14T12:14:16.557 回答
0

这是使用Autofac.Extensions.DependencyInjection的解决方案。如果要求超出提供的容器的功能,Microsoft 建议使用另一个容器。

内置服务容器旨在满足框架和大多数消费者应用程序的需求。我们建议使用内置容器,除非您需要它不支持的特定功能。

回答这个问题的设置包括创建一些仅用于说明的类型。我试图将其保持在最低限度。

我们拥有的是:

  • 两种都依赖的类型IDependency
  • 的两种实现IDependency
  • 我们希望将不同的实现注入IDependency到需要它的每种类型中。
  • 注入的两个类IDependency都将其作为属性公开。这样我们就可以测试该解决方案是否有效。(我不会在“真实”代码中这样做。这只是为了说明和测试。)
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获取DependencyVersionOneAlsoNeedsDependency获取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);
    }
}

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

现在,我们如何将其放入“真实”应用程序中?

在你的Startup课堂上,改变

public void ConfigureServices(IServiceCollection services)

public IServiceProvider ConfigureServices(IServiceCollection services)

现在ConfigureServices将返回一个IServiceProvider.

然后,您可以将 AutofacContainerBuilder步骤添加到ConfigureServices并让方法返回new AutofacServiceProvider(container);

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

只需确保包含此步骤:

builder.Populate(services);

这样在 . 注册的任何内容IServiceCollection也会添加到ContainerBuilder.


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

依赖注入早在Microsoft.Extensions.DependencyInjection,IServiceCollectionIServiceProvider. 它有助于学习如何使用不同的工具做相同或相似的事情,以便我们使用底层概念,而不仅仅是特定的实现。

这是Autofac的更多文档,专门用于将其与 ASP.NET Core 一起使用。

于 2019-08-14T14:57:50.177 回答
0

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

我找到了一种(稍微复杂的)方法来实现所需的结果,而无需任何第三方库。

// 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;
}
于 2019-08-14T13:15:54.843 回答
0

该代码旨在用于 NuGet 包,其中消费应用程序选择容器

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

相反,设计库,使它们对 DI 的任何实现都友好

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

public RootA(ISubdependency)

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

public RootB(ISubdependency)

该库的任何使用者现在都可以配置实例RootA以及RootB他们更喜欢这样做的方式。使用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。

于 2019-08-31T10:00:05.920 回答
0

如果您确切知道要如何组合每个类,则可以“手动”组合它们并将这些实例注册到容器中。如果类被注册为单例,这尤其容易,就像您的问题一样,但即使它们是瞬态的或范围的,它也可以应用。

把它写成扩展是很常见的。

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

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

这使得这比试图IServiceProvider弄清楚要解决哪个依赖项以及何时解决要简单得多。

于 2019-08-15T13:08:20.343 回答