I have written a post in 2022 on the same topic. That article was of course not about .NET 8, but it explained how the default dependency injection container can be used if you have multiple implementations of the same interface.
.NET 8 introduces a new feature to the built-in dependency injection container, namely, keyed services. This feature allows us to register and resolve multiple implementations of the same interface using different keys.
This can be useful when we need to choose between different service implementations based on some criteria, such as configuration, environment, or user input. In this post, let’s try to know more about how to use keyed services in .NET 8 dependency injection.
What options were available before .NET 8 ?
If you have seen the previous article on this topic, there I have listed down few approaches. Most of those approaches suggested to write some code. We can still continue using those approaches if we want. But now .NET 8 comes with new feature – keyed services. In this article, we will try to know more about this feature. For the sake of this discussion we are going to refer the
For demonstrating the new feature from .NET 8, we are going to extend on the same problem statement which is discussed in the previous article.
What are keyed services in .NET 8 Dependency Injection?
Keyed services are a way of registering and resolving services with the dependency injection container using an additional piece of information, called a key, along with the service type. The key can be any object, but it is usually a string or an enum value that identifies the service implementation.
For example, in our problem statement, we have one IReminderService and there are three concrete implementations of that interface. So, while registering the types in DI container, we can specify the keys. Then the same keys can be used to resolve the respective registered type. As stated above a key can be any type of object. But in most of the scenarios it makes sense to use either string or enum as key for resolving those types. So if we decide to use enum, the keys can be, let’s say, ReminderType.Email, ReminderType.Sms and ReminderType.PushNotifications.
In the coming section we are going to have a look at the syntax for achieving this.
How to register types using keyed services feature ?
Now, let’s have a look at how to use this feature. We already know that .NET’s default DI container provide some APIs to register types. The APIs are named based on what should be the lifetime scope of the resolved object – e.g. AddTransient, AddScoped, AddSingleton, etc.
To register keyed services, we need to use the new extension methods on the IServiceCollection interface that accept a key parameter. These methods are:
AddKeyedSingleton<TService>(object? key)– registers a singleton service with the given key.AddKeyedScoped<TService>(object? key)– registers a scoped service with the given key.AddKeyedTransient<TService>(object? key)– registers a transient service with the given key.
These methods have overloads that allow you to specify the implementation type or factory delegate for the service.
How to Resolve Types ?
Now assuming that we have registered the types in the dependency injection container, let’s try to understand how to resolve those types.
There are two ways to resolve the types:
- FromKeyedServices Attribute – I would generally prefer this way over the other. Here while injecting a type, we need to specify the [FromKeyedServices(key)] attribute for the constructor parameter and the framework will automatically resolve the concrete implementation which was registered with matching key.
- GetRequiredKeyedService API – This API is to get a service of specified type from the IServiceProvider implementing the concerned interface. Because we may not want to inject IServiceProvider in various classes as it can be used to resolve any dependencies. But just in case we want to use this way, we can create a wrapper around IServiceProvider to resolve only specific types. That way, we can control that we are not abusing injection of IServiceProvider.
Demo
Let’s say, we have an interface IReminderService and it has three implementations – EmailReminderService, SmsReminderService and PushNotificationsReminderService.
Let’s say we want to register them in the dependency injection container. We can use AddKeyedTransient method to register all three types. While registering each concrete implementation of IReminderService, we need to specify the key. We have created an enum and it has been used for this purpose.
Just to demonstrate that we can specify any object as a key, I have used a string while registering the email reminder service.
The code given below has three controllers. Two controllers show how the attribute FromKeyedServices can be used to specify the key and resolve the appropriate type. Note that the interface parameter has type IReminderService and then the parameter attribute is used to resolve the type.
In SecondController, we have used an API GetRequiredKeyedService to resolve the type, instead of using the attribute.
What are possible use cases of this feature ?
Keyed services can be useful in scenarios where you need to choose between different service implementations based on some criteria, such as configuration, environment, or user input. For example, you can use keyed services to:
- Switch between different logging providers (e.g., Serilog, NLog, Log4Net) based on configuration settings or environment variables.
- Switch between different email providers (e.g., SMTP, SendGrid, etc.) based on configuration settings or user preferences.
And of course, the usage is not limited to these scenarios. Wherever you need factory pattern, you can use keyed services to create appropriate concrete implementation.
I hope you enjoyed this post. Let me know your thoughts.
