DEV Community

Xavier Abelaira Rueda
Xavier Abelaira Rueda

Posted on

Keyed Services in .NET8's Dependency Injection

๐Ÿ“ƒ Introduction

Since the beginning, AspNetCore provided a simple but effective dependency injection container by default, without the need of configuring it manually.

This amazing built-in implementation has worked like a charm in most of the common scenarios. However, not everything was covered as easy as pie by default.

As a counterexample, if we needed to deal with more than one implementation pointing to the same interface, we needed some kind of workaround to overcome this challenge.

โช Previous context

Having said that, we would be able to use a Factory method; or a custom implementation resolver among others (or directly using a different DI container provider like Autofac), but the one that I used the most until now was using a custom delegate and a simple enum with the different implementations.

With this aim in mind, this is how it looked an implementation having this kind of Service Resolver:

1. Services definition

Let's imagine that we have a couple of implementations fulfilling the same contract definition:

public interface IService 
{
    public Task<string> DoSomethingAsync();
}


public class ServiceImplementationA : IService
{
    public Task<string> DoSomethingAsync() 
        => Task.FromResult("Hello there from Service A");
}

public class ServiceImplementationB : IService
{
    public Task<string> DoSomethingAsync() 
        => Task.FromResult("Hello there from Service B");
}
Enter fullscreen mode Exit fullscreen mode

2. Custom delegate and Enum

With this scenario, we will need something in charge of resolving this within the needed part of the project (Controller, Service...) and this will have the shape of a custom delegate, considering the members of an enum that has the different implementations defined in the project:

public enum ServiceImplementation
{
    A,
    B
}

public delegate IService ServiceResolver(ServiceImplementation implementationType);
Enter fullscreen mode Exit fullscreen mode

3. DI Container configuration

Once we have this delegate defining what we need for resolving our different implementations, let's see how to properly set up everything within the DI container:

builder.Services.AddTransient<ServiceImplementationA>();
builder.Services.AddTransient<ServiceImplementationB>();

builder.Services.AddTransient<ServiceResolver>(serviceProvider => implType =>
{
    return implType switch
    {
        ServiceImplementation.A => serviceProvider.GetService<ServiceImplementationA>()!,
        ServiceImplementation.B => serviceProvider.GetService<ServiceImplementationB>()!,
        _ => throw new ArgumentException("Invalid service type")
    };
});
Enter fullscreen mode Exit fullscreen mode

โš ๏ธ Note that we are using a pattern matching implementation for resolving the different types, however this could also be done with a traditional switch case:

builder.Services.AddTransient<ServiceResolver>(serviceProvider => implType =>
{
    switch (implType)
    {
        case ServiceImplementation.A:
            return serviceProvider.GetService<ServiceImplementationA>()!;
        case ServiceImplementation.B:
            return serviceProvider.GetService<ServiceImplementationB>()!;

        default:
            throw new ArgumentException("Invalid service type");
    }
});
Enter fullscreen mode Exit fullscreen mode

4. Resolving it

Having this, in a minimal API as a quick example, this would look like the example below once we needed to resolve our different services:

app.MapGet("/test-a", async (ServiceResolver serviceResolver) =>
{
    var service = serviceResolver(ServiceImplementation.A);
    var data = await service.DoSomethingAsync();

    return Results.Ok(data);
})
.WithName("TestEndpoint-A")
.WithOpenApi();

app.MapGet("/test-b", async (ServiceResolver serviceResolver) =>
{
    var service = serviceResolver(ServiceImplementation.B);
    var data = await service.DoSomethingAsync();

    return Results.Ok(data);
})
.WithName("TestEndpoint-B")
.WithOpenApi();
Enter fullscreen mode Exit fullscreen mode

โš ๏ธ Note that we directly inject the ServiceResolver delegate, and we request the concrete service through our initially defined enum type.

5. Running it

With the API running, once we try to execute it, this is what we have back in both endpoints:

Results when invoked from HttpRepl

๐Ÿ’ก The invocation shown in the screenshot above was using HttpRepl dotnet tool, more info here.

โฉ What's new in .NET8?

Accordingly to one of the latest tweets from David Fowler, .NET 8 will introduce a new feature called Keyed Services that will allow to define keyed elements within the DI Container. This feature will be available to any kind of AspNetCore application, and the key can be anything.

This feature isn't delivered yet in any of the current .NET 8 preview releases, so it may vary on final release.

So, the example shown above, would look like this:

1. DI Container configuration

Configuring it is quite straightforward, it's required to use the concrete extension method according to the life scope that we want to specify for the given implementation, so here we have some of the "equivalent" methods to the one that we already know.

Regular Keyed
AddTransient<TService, TImplementation>() AddKeyedTransient<TService, TImplementation>(object key)
AddScoped<TService, TImplementation>() AddKeyedScoped<TService, TImplementation>(object key)
AddSingleton<TService, TImplementation>() AddKeyedSingleton<TService, TImplementation>(object key)

Following the previous example, equally registering ours as Transient ones:

builder.Services.AddKeyedTransient<IService, ServiceImplementationA>(ServiceImplementation.A);
builder.Services.AddKeyedTransient<IService, ServiceImplementationB>(ServiceImplementation.B);
Enter fullscreen mode Exit fullscreen mode

2. Resolving it

Here we have a couple of simple mechanisms for helping us to inject the concrete type. One is using the decorator [FromKeyedServices]: very useful especially in Minimal API's. The other one is using a concrete new service provider type called IKeyedServiceProvider:

Using [FromKeyedServices]:

app.MapGet("/test-a", async ([FromKeyedServices(ServiceImplementation.A)] IService serviceImplA) =>
{
    var data = await serviceImplA.DoSomethingAsync();

    return Results.Ok(data);
})
.WithName("TestEndpoint-A")
.WithOpenApi();

app.MapGet("/test-b", async ([FromKeyedServices(ServiceImplementation.B)] IService serviceImplB) =>
{
    var data = await serviceImplB.DoSomethingAsync();

    return Results.Ok(data);
})
.WithName("TestEndpoint-B")
.WithOpenApi();
Enter fullscreen mode Exit fullscreen mode

Using IKeyedServiceProvider:

public class AnotherServiceConsumingA
{
    private readonly IService _service;

    public AnotherServiceConsumingA(IKeyedServiceProvider keyedServiceProvider)
    {
        _service = keyedServiceProvider.GetRequiredKeyedService<IService>(ServiceImplementation.A);
    }
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“’ Conclusion

This new feature is a simple but effective built-in way for resolving these particular situations in our code base easily without the need of using external libraries or workarounds, and the two flavours provided for resolving it in the consumer classes provides all that we need, so looking forward to the final implementation delivered in the upcoming .NET8 previews.

โš ๏ธ As commented, this is based on a preliminar information still not available on current .NET8 previews, so the final approach could vary significantly from this post.

If you liked it, please give me a โญ and follow me ๐Ÿ˜‰.

๐Ÿ”— Resources

You can check it out the current approach code here

Useful links

Top comments (4)

Collapse
 
joseluiseiguren profile image
Jose Luis Eiguren

Nice article @xelit3
Thanks for sharing :-)

Collapse
 
xaberue profile image
Xavier Abelaira Rueda

Thank you @joseluiseiguren ! Glad to see that is useful for others, thanks for your comment!

Collapse
 
ghadzhigeorgiev profile image
Georgi Hadzhigeorgiev

Thanks for pointing out this!

Collapse
 
xaberue profile image
Xavier Abelaira Rueda

You are welcome! Thanks for reading it ๐Ÿ‘