Keyed Services in .NET8's Dependency Injection
Tags: dotnet, csharp
Published: 2023-08-01 21:29:31

## :page_with_curl: 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. 


## :rewind: 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](https://autofac.org/))_, 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:

```csharp
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");
}
```

### 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:

```csharp
public enum ServiceImplementation
{
    A,
    B
}

public delegate IService ServiceResolver(ServiceImplementation implementationType);
```

### 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:

```csharp
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")
    };
});
```
> :warning: 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:

```csharp
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");
    }
});
```

### 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:

```csharp
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();
```
> :warning: 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hkcnds7t1oi0y1gx7xai.png)

> :bulb: The invocation shown in the screenshot above was using HttpRepl dotnet tool, more info [here](https://learn.microsoft.com/en-us/aspnet/core/web-api/http-repl/?view=aspnetcore-7.0&tabs=windows).


## :fast_forward: What's new in .NET8?

Accordingly to [one of the latest tweets](https://twitter.com/davidfowl/status/1683956501570879489?s=20) 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:

```csharp
builder.Services.AddKeyedTransient<IService, ServiceImplementationA>(ServiceImplementation.A);
builder.Services.AddKeyedTransient<IService, ServiceImplementationB>(ServiceImplementation.B);
```

### 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]`:

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

Using `IKeyedServiceProvider`:
```csharp
public class AnotherServiceConsumingA
{
    private readonly IService _service;

    public AnotherServiceConsumingA(IKeyedServiceProvider keyedServiceProvider)
    {
        _service = keyedServiceProvider.GetRequiredKeyedService<IService>(ServiceImplementation.A);
    }
}
```

## :ledger: 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. 

> :warning: 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 😉.**

## :link: Resources

You can check it out the current approach code [here](https://github.com/Xelit3/Playground/tree/main/src/Xelit3.Playground.AspNetCore.DI)

### Useful links
- [David Fowler's tweet](https://twitter.com/davidfowl/status/1683956501570879489/photo/1)
- [AspNetCore Dependency Injection](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-7.0)