Two types use a subdependency - how to use different implementations

664 views Asked by At

Say we register two types, RootA and RootB, that each have a dependency on ISubdependency.

Sharing the same subdependency implementation is easy:

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

Now the goal is for the two root types to use different implementations of the subdependency. The caller should be able to register either an instance, a factory, or a type.

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

I have managed to achieve the first two scenarios, although the approach is a bit complex to explain here. The third scenario, however, is still beyond me. Let's assume that if we can solve that one, we can solve them all.

So the problem is this:

  • RootA and RootB depend on ISubdependency.
  • Other types might depend on ISubdependency as well.
  • If we register a particular implementation, e.g. services.AddSingleton<ISubdependency, SubdependencyZ>(), then that registration is global (to the container), and it overwrites any previous registrations for ISubdependency. As a result, the last registration ends up being used for all dependants!
  • Particularly the type-based registration (scenario 3 above) is challenging, because we only have the type, and no easy way to resolve an instance. That means we have to resort to having the container resolve the registered type, which makes it even harder to work around the previous bullet point.
  • We must stick to .NET Core's IOC extensions. We are not permitted to depend on a particular third party container. Edit: This is because the code is intended for use in NuGet packages, where the consuming application chooses the container.

Questions

  1. How can we achieve the desired outcome? Preferably a non-convoluted way!

  2. Is there a de facto standard with regards to this problem? Is it a use case that is generally recognized, using different implementations for dependants on the same interface? Or is this generally avoided altogether, forcing dependants to simply use the same implementation?

7

There are 7 answers

0
Timo On BEST ANSWER

I have found a proper solution.

// 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;
}

First, the user registers anything related to the subdependency. In this example, I have considered the case where the application also uses the subdependency directly, and the direct usage calls for another implementation than the usage by library RootA, which in turn calls for another implementation than RootB.

After registering all this (or before - technically the order doesn't matter), the user registers the high-level dependencies, RootA and RootB. Their options allow the user to specify the subdependency type to use.

Looking at the implementation, you can see that we use the factory-based overload of AddSingleton, which lets us ask the service provider for any subdependencies at construction time.

The implementation also initializes the type to use to typeof(ISubdependency). If the user were to ignore the UseSubdependency method, that would be used:

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

If the user fails to register an implementation for ISubdependency, the get the usual exception for that.


Note that we never allow the user to register a thing in a nested fashion. That would be confusing: it would look like the registration is only for the thing that wraps it, but since the container is a flat collection, it is actually a global registration.

Instead, we only allow the user to refer to something that they explicitly register elsewhere. This way, no confusion is introduced.

3
Robert Perry On

The built in dependency injection doesnt support your scenario. What you're looking for is "Contextual Binding" which allows you to add a name to a particular binding and then use that name to choose which binding you want at runtime. Many other packages provide this feature out of the box, but MS DI does not. To implement the feature is not trivial. Whilst this answer does not "give you an answer" the answer is, you need to roll your own, or use a third party library instead

5
Darjan Bogdan On

Due to lack of more complex features in .Net Core DI, maybe it is easiest for you to create marker interfaces for each specific sub type.

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)
    {

    }
}

If possible, the most straightforward DI composition would be if Root classes depend upon their subsystem interface, but if not possible you can use factory to register each 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>()));

The other possibility, is to depend upon IEnumerable<ISubdependency> and then take appropriate one to work with.

2
Timo On

Edit: By now I have found a more appropriate solution, which I have posted as a separate answer.

I have found a (slightly convoluted) way to achieve the desired result without any third party libraries.

// 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;
}
0
Scott Hannen On

Here's a solution using Autofac.Extensions.DependencyInjection. Microsoft recommends using another container if the requirements exceed what the provided container does.

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.

The setup for answering this includes creating a few types just for illustration. I've tried to keep this as minimal as possible.

What we've got are:

  • Two types that both depend on IDependency
  • Two implementations of IDependency
  • We want to inject a different implementation of IDependency into each type that needs it.
  • The two classes that get IDependency injected both expose it as a property. That's just so we can test that the solution works. (I wouldn't do that in "real" code. This is just for the purpose of illustrating and testing.)
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 { }

How do we configure this so that NeedsDependency gets DependencyVersionOne and AlsoNeedsDependency gets DependencyVersionTwo?

Here it is in the form of a unit test. Writing it this way make it easy to verify that we're getting the result we expect.

[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);
    }
}

The unit test resolves both types and verifies that we've injected what we expect.

Now, how do we take this and put it in a "real" application?

In your Startup class, change

public void ConfigureServices(IServiceCollection services)

to

public IServiceProvider ConfigureServices(IServiceCollection services)

Now ConfigureServices will return an IServiceProvider.

Then, you can add the Autofac ContainerBuilder steps to ConfigureServices and have the method return new AutofacServiceProvider(container);

If you're already registering services with the IServiceCollection, that's fine. Leave that as it is. Whatever services you need to register with the Autofac ContainerBuilder, register those.

Just make sure you include this step:

builder.Populate(services);

So that whatever was registered with the IServiceCollection also gets added to the ContainerBuilder.


This might seem a tiny bit convoluted as opposed to just making something work with the provided IoC container. The advantage is that once you get over that hump, you can leverage the helpful things that other containers can do. You might even decide to use Autofac to register all of your dependencies. You can search for different ways to register and use named or keyed dependencies with Autofac, and all of those options are available to you. (Their documentation is great.) You can also use Windsor or others.

Dependency injection was around long before Microsoft.Extensions.DependencyInjection, IServiceCollection, and IServiceProvider. It helps to learn how to do the same or similar things with different tools so that we're working with the underlying concepts, not just a specific implementation.

Here is some more documentation from Autofac specific to using it with ASP.NET Core.

5
Scott Hannen On

If you know exactly how you want to compose each class you can compose them "manually" and register those instances with the container. That's especially easy if the classes are registered as singletons, as in your question, but it can apply even if they are transient or scoped.

It's common to write this as an extension.

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;
    }
}

Then, in Startup,

services.AddYourStuff();

Even if there are some more complex dependencies involved, that's okay. You only have to compose each class once and the consumer doesn't care how everything is composed - they just call the extension. (Although you're registering the composed instances with the container, this is similar to what we call pure DI.)

That makes this a much simpler approach than trying to get the IServiceProvider to figure out which dependency to resolve and when.

2
Mark Seemann On

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

A library or framework shouldn't depend on any DI Container, not even a Conforming Container. That also rules out the built-in container in .NET.

Instead, design libraries so that they're friendly to any implementation of DI.

If RootA depends on ISubdependency, just make it a dependency, and use Constructor Injection to advertise it:

public RootA(ISubdependency)

If RootB also has the same dependency, use the same pattern:

public RootB(ISubdependency)

Any consumer of the library can now configure instances of RootA and RootB exactly how they prefer to do so. Using Pure DI, it's a simply as newing up the objects:

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

Any client is also free to pick a DI Container of its own choice, which may or may not be able to address complex dependency selection scenarios. This provides full flexibility for all clients, without constraining anyone to the lowest common denominator provided by a Conforming Container.

You can't beat Pure DI when it comes to flexibility, since it's based on the full power of C# (or Visual Basic, or F#, etc.) instead of an API that may or may not expose the functionality you need.