In this article, we will look at keyed services – a new feature in .NET 8 that allows us to retrieve services by a given key.
Let’s dive in!
What Are Keyed Services in .NET?
Keyed services are a new addition to .NET 8 and a long-requested feature that has finally arrived. This new feature allows us to register multiple implementations of the same type by specific keys.
We can then use those keys to tell our application which implementation to use explicitly. Keyed services come in handy when we have an interface with different implementations that we want to use in our application, especially when we need to use several of those implementations in different places in our application at the same time.
What Problem Do Keyed Services Solve?
Let’s first create an interface:
public interface IEventService { string EndEvent(); string StartEvent(); }
This interface has two methods – one for starting an event and one for ending it.
Next, we implement it:
public class OnlineEventService : IEventService { public string EndEvent() => "Ending the online event. You can turn on your microphones."; public string StartEvent() => "Starting the online event. Please turn off your microphones."; }
First, we create the OnlineEventService
that will manage online events. The two methods return strings, asking attendees to turn off or on their microphones.
Then, the second implementation:
public class InPersonEventService : IEventService { public string EndEvent() => "Ending the in-person event. You can turn on your phones."; public string StartEvent() => "Starting the in-person event. Please turn off your phones."; }
The InPersonEventService
is responsible for in-person events. It asks people to turn their phones off or on when the event starts or ends.
As our application is responsible for managing different events, we would like to register both implementations in our Dependency Injection container and use them according to our needs. The problem is that the built-in DI container doesn’t have the capability to do that in a straightforward way. Until .NET 8, there were only external libraries that solved this problem.
How Do Keyed Services Solve the Problem?
Before we can see how we can use keyed services in .NET 8, we need to install a package:
dotnet add package Microsoft.Extensions.DependencyInjection --version 8.0.0-rc.1.23419.4
After we have done that, we can register our two services:
builder.Services.AddKeyedScoped<IEventService, OnlineEventService>("online"); builder.Services.AddKeyedScoped<IEventService, InPersonEventService>("in-person");
Here we use the AddKeyedScoped<TService, TImplementation>()
method to register our services in the DI container. This method takes in an object
for the service key. We use a string as a key, although we can use anything else in this context as well. We can also use other dependency lifetimes by utilizing either the AddKeyedTransient()
or AddKeyedSingleton()
methods.
Next, we can try to use them in a minimal API:
app.MapGet("/startOnlineEvent", ([FromKeyedServices("online")] IEventService eventService) => eventService.StartEvent());
We create the /startOnlineEvent
endpoint and request the service with the key online. The application will build and run but will not work properly as keyed services are still not fully supported in minimal APIs at the point of writing the article.
They are not supported in regular controllers as well:
[ApiController] [Route("[controller]")] public class OnlineEventController : ControllerBase { private readonly IEventService _eventService; public OnlineEventController([FromKeyedServices("online")] IEventService eventService) { _eventService = eventService; } [HttpGet] public string StartEvent() { return _eventService.StartEvent(); } }
We add an OnlineEventController
controller and again request the service registered with the key online. We can build and run the application but when we try to call the desired endpoint we will get an exception that the dependency could not be resolved.
To work around the limitations when it comes to controllers, we need classes to request the services we just registered:
public class OnlineEventProducer( [FromKeyedServices("online")] IEventService eventService) { public string ProduceEvent() { var sb = new StringBuilder(); sb.AppendLine(eventService.StartEvent()); sb.AppendLine("Custom online event logic goes here."); sb.AppendLine(eventService.EndEvent()); return sb.ToString(); } } public class InPersonEventProducer( [FromKeyedServices("in-person")] IEventService eventService) { public string ProduceEvent() { var sb = new StringBuilder(); sb.AppendLine(eventService.StartEvent()); sb.AppendLine("Custom in-person event logic goes here."); sb.AppendLine(eventService.EndEvent()); return sb.ToString(); } }
Using another new feature called primary constructors, we create two classes – OnlineEventProducer
and InPersonEventProducer
. Both have a single ProduceEvent()
method that returns a new string corresponding to the type of event we are creating. In the constructors of both classes, we request a parameter of IEventService
type. For the DI container to know which concrete implementation to provide, we also add the FromKeyedServices
attribute, specifying the key we used for the desired service.
This will work the same way with the regular constructors as well.
Then we register our producers:
builder.Services.AddScoped<OnlineEventProducer>(); builder.Services.AddScoped<InPersonEventProducer>();
We add both producer classes to the container as scoped services.
Finally, we create two endpoints:
app.MapGet("/attendOnlineEvent", (OnlineEventProducer eventProducer) => eventProducer.ProduceEvent()); app.MapGet("/attendInPersonEvent", (InPersonEventProducer eventProducer) => eventProducer.ProduceEvent());
Using minimal APIs for simplicity we create the /attendOnlineEvent
and /attendInPersonEvent
endpoints. Each of them requests the corresponding producer from the DI container and then calls the ProduceEvent()
method.
If we run our application and call the endpoints, we will see by the result we get that the keyed services have been properly injected according to our requests.
Conclusion
The introduction of keyed services in .NET 8 further improves the existing dependency injection functionality. It allows us to easily register and retrieve multiple interface implementations using specific keys, addressing a long-standing limitation of the built-in DI container. This new feature helps us to simplify code, and enhance maintainability, as well as offers a flexible solution for managing interface implementations.