Сервисы с ключами появились в .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
с ключом сервиса.