With the introduction of keyed services in .NET 8, 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?
Options pattern has Named options support using IConfigureNamedOptions
which you can combine with the ServiceKeyAttribute
to get the key of the registered instance.
Sample code:
// register named options
services.Configure<MyOpts>("X", ...);
services.Configure<MyOpts>("Y", ...);
services.AddKeyedTransient<MyService>("X");
services.AddKeyedTransient<MyService>("Y");
public class MyService
{
public MyOpts MyOpts { get; }
public MyService(IOptionsFactory<MyOpts> opts, [ServiceKey] string servKey)
{
MyOpts = opts.Create(servKey); // use the service key to get named options
}
}