Dependency Injection using keyed services is finally in ASP.NET
.NET 8 was released lately (at least when this post was written) and it contains lots of excellent features and improvements. A minor improvement that I was very happy to finally see in ASP.NET Core, is dependency injection using keys. A feature that I have been using in other dependency injection frameworks in the past. In this post, I'll show you how to use it and discuss both advantages and disadvantages.
Let's start by setting up the stage using some example code that I already have implemented in an ASP.NET Core web app. I'm using MVC and controllers here, but the same possibilities are there for Razor pages, minimal APIs, and even outside the scope of a web app. For the demo, I'm using the Azure.Messaging.ServiceBus
package to create a couple of clients for publishing messages to several Service Bus topics. In my existing code, I create two clients in the initialization code of the website:
var serviceBusClient = new ServiceBusClient("connectionString");
var tasksClient = serviceBusClient.CreateSender("tasks");
var preprocessorClient = serviceBusClient.CreateSender("preprocessor");
In an ideal world (well, that depends but more about that later), I want to register both clients as singletons:
builder.Services.AddSingleton(tasksClient);
builder.Services.AddSingleton(preprocessorClient);
And finally, inject both clients in the controller:
private readonly ServiceBusSender taskTopic;
private readonly ServiceBusSender preprocessorTopic;
public HomeController(ServiceBusSender taskTopic, ServiceBusSender preprocessorTopic)
{
this.taskTopic = taskTopic;
this.preprocessorTopic = preprocessorTopic;
}
If you have tried something similar you know that this simply isn't possible. ASP.NET Core won't know which of the parameters that map to witch registered singleton. My solution so far has been to create a wrapper for these services:
public class Topics(ServiceBusSender taskTopic, ServiceBusSender preprocessorTopic)
{
public ServiceBusSender TaskTopic { private set; get; } = taskTopic;
public ServiceBusSender PreprocessorTopic { private set; get; } = preprocessorTopic;
}
And then register and inject that object instead:
// Program.cs
var topics = new Topics(tasksClient, preprocessorClient);
builder.Services.AddSingleton(topics);
// HomeController.cs
private readonly Topics topics;
public HomeController(Topics topics)
{
this.topics = topics;
}
We finally arrived at what this post is about: Keyed services, introduced in ASP.NET Core 8. With this new feature, we can remove the Topics
class and replace it by injecting each topic client as singletons. This is done using a new method named AddKeyedSingleton
. There are similar methods available for transient and scoped dependencies. Let's see how it works:
builder.Services.AddKeyedSingleton("taskTopic", tasksClient);
builder.Services.AddKeyedSingleton("preprocessorTopic", preprocessorClient);
Notice how each singleton includes a name. Injecting each object into the controller can now be done by including the FromKeyedServices
attribute on each parameter:
private readonly ServiceBusSender taskTopic;
private readonly ServiceBusSender preprocessorTopic;
public HomeController(
[FromKeyedServices("taskTopic")] ServiceBusSender taskTopic,
[FromKeyedServices("preprocessorTopic")] ServiceBusSender preprocessorTopic)
{
this.taskTopic = taskTopic;
this.preprocessorTopic = preprocessorTopic;
}
The parameter provided for the FromKeyedServices
attribute must match what we set up in the Program.cs
file.
This new feature definitely makes it a lot easier to inject multiple objects of the same type. I don't see it used for a lot of other scenarios, though. It relies on "magic strings" so you should probably consider specifying the service name as constants somewhere to avoid typos. Also, remember to unit-test your dependency injection code to make sure all services can be resolved. In time, someone will (hopefully) create a Roslyn analyzer or similar to make sure that these magic strings can actually be resolved.