With the introduction of keyed services in C# 12, we can now request specific interface implementations in a constructor:
public class MotorController
{
private readonly IMotor _motor;
public MotorController([FromKeyedServices("AxisX")] IMotor motor)
{
_motor = motor;
}
}
This is a cool addition to the language. One useful application would be having different instances of the SAME type injected into different classes based on the service key.
Note: I’m aware there’s probably no good reason to do this in ASP.NET applications that make up the vast majority of C# projects out there. However, for the more niche applications I’m working on it would be a useful way to implement multiple instances of real word physical devices.
An obstacle to leveraging this is that there currently isn’t a way to key and map the IOptions
instances that are injected into classes; see this example (simplified as much as possible):
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
// Create a simple configuration (stand-in for JSON file, etc.)
var configBuilder = new ConfigurationBuilder();
configBuilder.AddInMemoryCollection(new Dictionary<string, string?>
{
{ "Motors:X:AxisNumber", "1"},
{ "Motors:Y:AxisNumber", "2"},
{ "Motors:Z:AxisNumber", "3"}
});
var config = configBuilder.Build();
// Create a service collection with keyed services for different instances of the same type
var serviceCollection = new ServiceCollection();
serviceCollection.AddKeyedSingleton<Motor>("X");
serviceCollection.AddKeyedSingleton<Motor>("Y");
serviceCollection.AddKeyedSingleton<Motor>("Z");
// QUESTION: How do we map different config sections to different keyed services?
serviceCollection.Configure<MotorOptions>(config.GetSection("Motors:X"));
var serviceProvider = serviceCollection.BuildServiceProvider();
var motorX = serviceProvider.GetRequiredKeyedService<Motor>("X");
var motorY = serviceProvider.GetRequiredKeyedService<Motor>("Y");
var motorZ = serviceProvider.GetRequiredKeyedService<Motor>("Z");
Console.WriteLine("Motor X Axis: " + motorX.AxisNumber);
Console.WriteLine("Motor Y Axis: " + motorY.AxisNumber);
Console.WriteLine("Motor Z Axis: " + motorZ.AxisNumber);
public class Motor
{
public Motor(IOptions<MotorOptions> options)
{
AxisNumber = options.Value.AxisNumber;
}
public int AxisNumber { get; }
}
public class MotorOptions
{
public int AxisNumber { get; set; }
}
Running this example, you get the same axis number for every instance, since they’re all sharing a single instance of IOptions<MotorOptions>
:
Motor X Axis: 1
Motor Y Axis: 1
Motor Z Axis: 1
It’s possible to make this work by manually creating and binding each instance of the options class, then passing the appropriate options instance to the constructor for each keyed service instance:
MotorOptions optionsX = new();
MotorOptions optionsY = new();
MotorOptions optionsZ = new();
config.Bind("Motors:X", optionsX);
config.Bind("Motors:Y", optionsY);
config.Bind("Motors:Z", optionsZ);
// Create a service collection with keyed services for different instances of the same type
serviceCollection.AddKeyedSingleton<Motor>("X", new Motor(Options.Create(optionsX)));
serviceCollection.AddKeyedSingleton<Motor>("Y", new Motor(Options.Create(optionsY)));
serviceCollection.AddKeyedSingleton<Motor>("Z", new Motor(Options.Create(optionsZ)));
However, this approach is pretty clunky, especially in a less trivial example where many other types are getting injected in the constructor alongside the IOptions
argument.
Any ideas on a more graceful solution to this, or is it just too far outside the intended use of keyed services?