Keyed dependency injection services in .NET 8

November 3rd 2023 .NET Dependency Injection

The dependency injection framework that is built into the modern .NET works well enough to satisfy the needs of most projects. However, it's not as feature-rich as the most popular 3rd party libraries. One of the features that was missing before .NET 8 was the ability to register multiple implementations of the same interface and have the ability to specify which one should be injected.

Let's say you have an interface:

public interface IDependency
{
    string SomeMethod();
}

And multiple implementations of that same interface:

public class DependencyA : IDependency
{
    public string SomeMethod() => "A";
}

public class DependencyB : IDependency
{
    public string SomeMethod() => "B";
}

There was no easy way to have a specific dependency injected in each service. Sure, you could register each dependency as its own type instead of as implementations of the common interface type:

serviceProvider = new ServiceCollection()
    .AddTransient<DependencyA>()
    .AddTransient<DependencyB>()
    .AddTransient<ServiceA>()
    .AddTransient<ServiceB>()
    .BuildServiceProvider();

This would allow a service to request a specific dependency:

public class ServiceA
{
    private readonly IDependency dependency;

    public ServiceA(DependencyA dependency)
    {
        this.dependency = dependency;
    }

    public string InvokeSomeMethod() => this.dependency.SomeMethod();
}

public class ServiceB
{
    private readonly IDependency dependency;

    public ServiceB(DependencyB dependency)
    {
        this.dependency = dependency;
    }

    public string InvokeSomeMethod() => this.dependency.SomeMethod();
}

However, this approach makes it difficult to provide a different (mocked) implementation in tests, because you can't simply mock class methods in .NET unless they are virtual. That's why you always want to mock an interface instead.

You could work around the limitation by injecting a delegate to resolve the dependency instead of the dependency directly. You could define it like this to return the correct dependency based on a string key:

public delegate IDependency DependencyResolver(string key);

You would implement the delegate to return the correct dependency based on the key value:

DependencyResolver dependencyResolver = (string key) => key switch
{
    "A" => serviceProvider.GetRequiredService<DependencyA>(),
    "B" => serviceProvider.GetRequiredService<DependencyB>(),
    _ => throw new InvalidOperationException($"No service with key '{key}'.")
};

Then you would register this resolver in addition to the individual dependencies:

serviceProvider = new ServiceCollection()
    .AddTransient<DependencyA>()
    .AddTransient<DependencyB>()
    .AddTransient<ServiceA>()
    .AddTransient<ServiceB>()
    .AddTransient(_ => dependencyResolver)
    .BuildServiceProvider();

And finally inject this delegate into the service instead of a specific dependency:

public class ServiceA
{
    private readonly IDependency dependency;

    public ServiceA(DependencyResolver dependencyResolver)
    {
        this.dependency = dependencyResolver("A");
    }

    public string InvokeSomeMethod() => this.dependency.SomeMethod();
}

public class ServiceB
{
    private readonly IDependency dependency;

    public ServiceB(DependencyResolver dependencyResolver)
    {
        this.dependency = dependencyResolver("B");
    }

    public string InvokeSomeMethod() => this.dependency.SomeMethod();
}

With this additional plumbing, you regain the ability to provide a different dependency implementation in tests: you only need to provide a different implementation for the DependencyResolver delegate, which returns the dependencies you want.

With .NET 8, you don't need such workarounds anymore. Its support for keyed dependency injection services allows you to register multiple implementations for the same interface with different keys, using the new AddKeyed* methods:

serviceProvider = new ServiceCollection()
    .AddKeyedTransient<IDependency, DependencyA>("A")
    .AddKeyedTransient<IDependency, DependencyB>("B")
    .AddTransient<ServiceA>()
    .AddTransient<ServiceB>()
    .BuildServiceProvider();

The services can then request a specific implementation using the new FromKeyedServicesAttribute:

public class ServiceA
{
    private readonly IDependency dependency;

    public ServiceA([FromKeyedServices("A")] IDependency dependency)
    {
        this.dependency = dependency;
    }

    public string InvokeSomeMethod() => this.dependency.SomeMethod();
}

public class ServiceB
{
    private readonly IDependency dependency;

    public ServiceB([FromKeyedServices("B")] IDependency dependency)
    {
        this.dependency = dependency;
    }

    public string InvokeSomeMethod() => this.dependency.SomeMethod();
}

To provide different dependencies in tests, you only need to register those instead of the production ones. The services have no awareness of different implementation types beyond their keys.

You can check all three approaches described in this post in my GitHub repository. Each approach is available as its own commit.

The new keyed services feature of dependency injection in .NET 8 allows you to easily register multiple implementations for the same interface and request a specific one to be injected. If you switched to a different dependency injection (i.e., inversion of control or IoC) framework to achieve that or implemented your own workaround, you don't need to do that anymore.

Get notified when a new blog post is published (usually every Friday):

If you're looking for online one-on-one mentorship on a related topic, you can find me on Codementor.
If you need a team of experienced software engineers to help you with a project, contact us at Razum.
Copyright
Creative Commons License