С появлением сервисов с ключами в .NET 8 мы теперь можем запрашивать конкретные реализации интерфейса в конструкторе:
public class MotorController
{
private readonly IMotor _motor;
public MotorController([FromKeyedServices("AxisX")] IMotor motor)
{
_motor = motor;
}
}
Это классное дополнение к языку. Одним из полезных приложений может быть внедрение разных экземпляров одного и того же типа в разные классы на основе служебного ключа.
Примечание. Я понимаю, что, вероятно, нет веской причины делать это в приложениях ASP.NET, которые составляют подавляющее большинство проектов C#. Однако для более нишевых приложений, над которыми я работаю, это будет полезным способом реализации нескольких экземпляров реальных физических устройств.
Препятствием для использования этого является то, что в настоящее время не существует способа ключа и сопоставления экземпляров IOptions, которые внедряются в классы; см. этот пример (максимально упрощенный):
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; }
}
Запустив этот пример, вы получите одинаковый номер оси для каждого экземпляра, поскольку все они используют один экземпляр IOptions<MotorOptions>:
Motor X Axis: 1
Motor Y Axis: 1
Motor Z Axis: 1
Это можно сделать, вручную создав и привязав каждый экземпляр класса параметров, а затем передав соответствующий экземпляр параметров конструктору для каждого экземпляра службы с ключом:
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)));
Однако этот подход довольно неуклюж, особенно в менее тривиальном примере, где вместе с аргументом IOptions в конструктор вводятся многие другие типы.
Есть какие-нибудь идеи по поводу более изящного решения этой проблемы, или это слишком далеко за пределами предполагаемого использования служб с ключами?





Шаблон параметров имеет поддержку именованных параметров с помощью IConfigureNamedOptions , который можно комбинировать с ServiceKeyAttribute, чтобы получить ключ зарегистрированного экземпляра.
Образец кода:
// 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
}
}
Не знал о «Именованных параметрах», но это именно то, что я искал, спасибо!