.NET 8 brings some great improvements, making it a key milestone for the “new .NET” with cool new features and better performance. In this post, I want to share a feature I really like, and it’s minor but handy – “Keyed Dependency Injection (DI)”

Keyed DI provides registering services with some user-defined keys and consuming those services by those keys. Yes, I can hear that you say, “But I can already do this”. This is already possible with AutoFac, Unity…etc. Because of this I am calling this new feature minor. But I think it is great to have this feature within.NET platform as a built-in feature.

Before delving into the specifics of Keyed DI, let’s consider its purpose, especially for those encountering it for the first time. Picture a scenario where different service implementations or configurations are necessary due to specific business requirements, such as distinct cache provider implementations.

builder.Services.AddSingleton<ICacheProvider>(provider => new RedisCacheProvider("SomeFancyServer:1234"));
builder.Services.AddSingleton<ICacheProvider>(new UltimateCacheProvider("MoreFancyServer:5432"));

In traditional host or container services, retrieving the required service from registrations posed challenges. How could one seamlessly inject a particular cache provider into a specific API?

Keyed Dependency Injection (DI)

With the new keyed DI services feature in .NET 8, now we can define some keys to register services and then we can use those keys when consuming the services. Let’s look at this with a simple example.

Let’s have two different implementations because of some fancy business requirements for an interface as below.

Please check the GitHub link at the end of the post for full example codes.

public interface IProductService
{
    List<string> ListProducts(int top = 5);
}

public class AmazonProducts : IProductService
{
    public List<string> ListProducts(int top = 5)
    {
        return new List<string>{
            "Fancy Product from Amazon",
            "Another Fancy Product from Amazon",
            "Top Seller Product from Amazon",
            "Most Expensive Product from Amazon",
            "Cheapest Product from Amazon",
            "Some Shinny Product from Amazon",
            "A Red Product from Amazon",
            "A Blue Product from Amazon",
            "Most Tasty Cake from Amazon",
            "Most Biggest Product from Amazon",
       }.Take(top).ToList();
    }
}

public class CDONProducts : IProductService
{
    public List<string> ListProducts(int top = 5)
    {
        var ran = new Random();
        return new List<string>{
            "Fancy Product from CDON",
            "Another Fancy Product from CDON",
            "Top Seller Product from CDON",
            "Most Expensive Product from CDON",
            "Cheapest Product from CDON",
            "Some Shinny Product from CDON",
            "A Red Product from CDON",
            "A Blue Product from CDON",
            "Most Tasty Cake from CDON",
            "Most Biggest Product from CDON",
       }.OrderBy(x => ran.Next()).Take(top).ToList();
    }
}

And let’s have some web API which is exposing the following service.

public class ProductsAPI
{
    private IProductService _service;
    public ProductsAPI(IProductService service)
    {
        _service = service;
    }

    public static async Task<ActionResult<List<string>>> GetProducts()
    {
        return _service.ListProducts();
    }
}

So far, they should be very familiar to you. No rocket science. So, let’s register those 2-service implementations with some keys.

builder.Services.AddKeyedScoped<IProductService,AmazonProducts>("amazon");
builder.Services.AddKeyedScoped<IProductService,CDONProducts>("cdon");

//There are also .AddKeyedSingelton() and .AddKeyedTransient() methods as usuall

Within new service registering methods, we can register those services with some keys. In here, we are registering AmazonProducts implementation with “amazon” key and CDONProducts implementation with “cdon” key so that the implementations can be used used within keys.

And now these services can be consumed with keys. There is a new attribute in .NET 8 as FromKeyedServices(key). When injecting a service with this attribute, a defined key that is used while registering the service can be used. So, the service registered with that key will be injecting into the host service.

public ProductsAPI([FromKeyedServices("amazon")]IProductService service){
    _service = service;
}

//OR this attribute can also be used within method parameter

public static async Task<ActionResult<List<string>>> GetProducts([FromKeyedServices("amazon")]IProductService service)
{
    return service.ListProducts();
}

No more tricky workarounds to incorporate different service implementations. For instance, if a new requirement emerges, like exposing CDON products, a simple key change in the web API is all that’s needed.

This was a very simple example, but I hope it helped to get the idea with the keyed DI services in .NET 8.

Bonus!!!

While the above example is straightforward, it effectively conveys the power of keyed DI services. A similar approach can be applied to configuration APIs in .NET, although not a new feature like keyed DI services, showcasing its versatility in managing configuration bindings.

Consider a scenario where there’s a uniform configuration structure per service/component/module as below.

{
  "ProductService": {
    "Amazon": {
      "top":3
    },
    "CDON": {
      "top":10
    }
  }
}

These configurations can be bound with keys/names, allowing for seamless binding of required configuration values into a service.

builder.Services.Configure<ListOptions>("amazon", builder.Configuration.GetSection("ProductService:Amazon")); 
builder.Services.Configure<ListOptions>("cdon", builder.Configuration.GetSection("ProductService:CDON")); 

So within “amazon” key some different configuration values are set for configuration options and with “cdon” different configuration options are set.

And within IOptionsSnapshot<T>.Get() it is possible to get named configuration option as below.

It is important to use IOptionsSnapshot for named configurations. IOptions was not supported for named configurations. Maybe I can have some another post about IOptions, IOptionsSnapshot and also IOptionsMonitor

public static async Task<ActionResult<List<string>>> GetProducts([FromKeyedServices("amazon")]IProductService service, IOptionsSnapshot<ListOptions> config)
{
    var listOptions = config.Get("amazon");
    return service.ListProducts(listOptions.ListCount);
}

public static async Task<ActionResult<List<string>>> GetAlternativeProducts([FromKeyedServices("cdon")]IProductService service,IOptionsSnapshot<ListOptions> config)
{
    var listOptions = config.Get("cdon");
    return service.ListProducts(listOptions.ListCount);
}

This was short and quick post, but I hope it will help you to have some awareness for a new feature in .NET 8. Happy coding until see you in the next article.

Please check the the following GitHub link for all full code and implementations.
https://github.com/ardacetinkaya/Demo.KeyedService