Сервисы с ключами появились в .NET 8 и используются в случае, если необходимо зарегистрировать в приложении несколько реализаций одного и того же сервиса.
Использование сервисов с ключами
Достаточно долгое время в .NET существовала проблема так называемой множественной регистрации сервисов. Например, в нашем приложении есть сервис ILogger и его реализация — FileLogger. В какой-то момент времени мы решили, что было бы неплохо использовать в нашем приложении вторую реализацию сервиса. Назовем её FileLoggerExt:
class FileLoggerExt : ILogger
{
const string FILE_NAME = "Log.txt";
private string fileName;
public string FileName
{
get
{
return fileName;
}
set
{
fileName = value;
}
}
private readonly string _newLine = Environment.NewLine;
public FileLoggerExt()
{
fileName = FILE_NAME;
}
public void WriteLine(string message)
{
File.AppendAllText(fileName, $"[Calculator] [{DateTime.Now}] - {message}{_newLine}");
}
}
Если мы зарегистрируем обе реализации сервиса, используя известные нам методы, например, вот так:
builder.Services.AddTransient<ILogger, FileLogger>(); builder.Services.AddTransient<ILogger, FileLoggerExt>();
то, при попытке получить сервис любым из способов, мы всегда будем получать из контейнера DI последнюю зарегистрированную реализацию, то есть FileLoggerExt. Решением этой проблемы «из коробки» является использование сервисов с ключами. Рассмотрим их использование на примере нашего приложения.
Для регистрации сервиса с ключом используются методы расширения IServiceCollection имена которых соответствуют шаблону AddKeyed[Lifetime](), где [Lifetime] — жизненный цикл сервиса.
Сервисы с ключами в .NET MAUI
Изменим код MauiProgram.cs следующим образом:
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});
builder.Services.AddKeyedTransient<ILogger, FileLogger>("simple");
builder.Services.AddKeyedTransient<ILogger, FileLoggerExt>("ext");
#if DEBUG
builder.Logging.AddDebug();
#endif
return builder.Build();
}
}
в качестве ключа при регистрации сервиса мы использовали строку, хотя можем использовать вообще любые объекты. Теперь попробуем запросить оба эти сервиса в нашем приложении. Изменим код класса MainPage следующим образом:
public partial class MainPage : ContentPage
{
int count = 0;
private ILogger _logger;
private ILogger _extlogger;
public MainPage([FromKeyedServices("ext")] ILogger extlogger)
{
InitializeComponent();
_extlogger = extlogger;
HandlerChanged += OnHandlerChanged;
}
private void OnHandlerChanged(object? sender, EventArgs e)
{
_logger ??= Handler.MauiContext.Services.GetKeyedService<ILogger>("simple");
}
private async void OnCounterClicked(object sender, EventArgs e)
{
count++;
if (count == 1)
CounterBtn.Text = $"Clicked {count} time";
else
CounterBtn.Text = $"Clicked {count} times";
SemanticScreenReader.Announce(CounterBtn.Text);
//используем сервис
_logger.WriteLine(CounterBtn.Text);
_extlogger.WriteLine(CounterBtn.Text);
}
}
Здесь мы запрашиваем оба сервиса, используя различные способы. Первый сервис мы запрашиваем через конструктор:
public MainPage([FromKeyedServices("ext")] ILogger extlogger)
а второй — используя локатор сервисов:
_logger ??= Handler.MauiContext.Services.GetKeyedService<ILogger>("simple");
Обратите внимание на то, как запрашивается сервис в конструкторе — здесь мы используем специальный атрибут [FromKeyedServices("ext")] в котором указываем ключ сервиса.
Теперь можно запустить приложение, кликнуть по кнопке и убедиться, что в приложении используется две реализации одного и того де сервиса. Вот как будет выглядеть лог:
[23.03.2025 18:45:18] - Clicked 1 time [Calculator] [23.03.2025 18:45:20] - Clicked 1 time
Итого
Использование сервисов с ключами позволяет зарегистрировать в контейнере DI две и более реализации одного и того же сервиса. Для регистрации сервисов с ключами используются методы расширения IServiceCollection вида AddKeyed[Lifetime](). При запросе сервиса из контейнера используется либо метод GetKeyedService(), либо, при constructor injection — с использованием специального атрибута FromKeyedServices с ключом сервиса.