Содержание
В своих примерах ранее, мы использовали следующий подход регистрации сервисов в контейнере DI: один сервис – одна реализация. Это, по-видимому, самый распространённый способ работы с сервисами в DI. Даже, если для одного сервиса у нас разработано несколько реализаций, то всё равно, мы при регистрации сервиса указываем только один тип. При этом может возникнуть ситуация, когда нам потребуется использовать несколько реализаций одного сервиса.
Проблема множественной регистрации сервисов
Для начала вернемся к нашему проекту с калькулятором. Сейчас его код выглядит следующим образом:
Файл Program.cs
using Microsoft.Extensions.DependencyInjection; namespace Example1 { internal class Program { static void Main(string[] args) { ServiceCollection services = new ServiceCollection(); //определяем объект какого типа необходимо создать Type type; string typeName; if (args.Length == 0) //аргументы командной строки не задана - используем тип по умолчанию typeName = "Example1.MessageWriter"; else typeName = args[0]; //пытаемся получить тип данных, объект которого необходимо создать type = Type.GetType(typeName, throwOnError: true); services.AddSingleton<Calculator>(); services.AddSingleton(implementationFactory: _ => (IMessageWriter)Activator.CreateInstance(type)); IServiceProvider serviceProvider = services.BuildServiceProvider(); Calculator? calculator = serviceProvider.GetService<Calculator>(); if (calculator != null) { calculator.Sum(10, 20); calculator.Sum(1, 2); } } } }
Файл Calculator.cs
namespace Example1 { public class Calculator { private readonly IMessageWriter writer; public Calculator(IMessageWriter writer) { ArgumentNullException.ThrowIfNull(writer); this.writer = writer; } public void Sum(int a, int b) { writer.SendMessage($"Сумма двух чисел равна: {a + b}"); } public void Multiply(int a, int b) { writer.SendMessage($"Произведение двух чисел равно: {a * b}"); } } }
Файл MessageWriter.cs
namespace Example1 { public interface IMessageWriter { public void SendMessage(string text); } public class MessageWriter: IMessageWriter { public void SendMessage(string text) { Console.WriteLine(text); } } public class ColorMessageWriter : IMessageWriter { public void SendMessage(string text) { Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine(text); Console.ForegroundColor = ConsoleColor.White; } } }
Наш класс Calculator
, в зависимости от настроек приложения, использует одну из двух реализаций сервиса IMessageWriter
. Но что произойдет, если мы попытаемся зарегистрировать и использовать обе реализации сервиса? Перепишем код Program.cs следующим образом:
using Microsoft.Extensions.DependencyInjection; namespace Example1 { internal class Program { static void Main(string[] args) { ServiceCollection services = new ServiceCollection(); services.AddTransient<Calculator>(); services.AddTransient<IMessageWriter, MessageWriter>(); services.AddTransient<IMessageWriter, ColorMessageWriter>(); IServiceProvider serviceProvider = services.BuildServiceProvider(); Calculator? calculator = serviceProvider.GetService<Calculator>(); if (calculator != null) { calculator.Sum(10, 20); calculator.Sum(1, 2); } } } }
Здесь мы зарегистрировали обе реализации сервиса — и MessageWriter
и ColorMessageWriter
. При этом, конструктор класса Calculator
мы оставили без изменения. Возникает вопрос: если мы никак не влияем на работу контейнера DI, то какая реализация сервиса будет использована при работе Calculator
? Запустим приложение и посмотрим на результат:
Сумма двух чисел равна: 3
В результате такой множественной регистрации, при разрешении зависимостей контейнер DI вернул нам последнюю зарегистрированную регистрацию сервиса. В этом можно убедиться, если поменять местами вызов методов регистрации:
services.AddTransient<IMessageWriter, ColorMessageWriter>(); services.AddTransient<IMessageWriter, MessageWriter>();
Теперь при запуске приложения будет использована реализация MessageWriter
. Более того, даже, если мы попытаемся изменить конструктор класса Calculator
вот так:
public Calculator(IMessageWriter writer, IMessageWriter colorWrite)
в обоих параметрах вернется последняя зарегистрированная реализация сервиса. Собственно, в этом и заключалась проблема множественной регистрации сервисов — как использовать несколько реализаций одного и того же сервиса?
Эта проблема вполне решаема и в .NET 7 и в более ранних версиях. Однако, начиная с версии .NET 8 платформа предоставляет способ множественной регистрации и использования сервисов по умолчанию.
Сервисы с ключами (Keyed Services)
В .NET 8 и более поздних версиях для регистрации сервисов появилась новая группа методов, соответствующая следующему шаблону:
AddKeyed[Жизненный_цикл]()
где [Жизненный_цикл]
— это название одного из трех жизненных циклов сервиса: Transient, Scoped, Singleton. Отличием этих методов от ранее рассмотренных является то, что регистрации сервиса с помощью этх методов мы можем указать ключ сервиса. Ключом сервиса может быть любой объект. Воспользуемся этими методами для регистрации наших сервисов. Перепишем немного метод Main()
, а именно — способ регистрации сервисов:
services.AddKeyedTransient<IMessageWriter, ColorMessageWriter>("colorWriter"); services.AddKeyedTransient<IMessageWriter, MessageWriter>("simpleWriter");
Здесь мы в качестве ключей сервисов используем обычные строки. Теперь, когда у нас имеется два сервиса с ключами, мы можем их использовать в нашей программе.
Способы получения сервисов с ключами
Использование методов GetKeyedService() / GetRequiredKeyedService()
Аналогично тому, как мы ранее получали сервис Calculator
, мы можем использовать метод GetKeyedService()
для получения сервиса с ключом. Например,
IMessageWriter? writer = serviceProvider.GetKeyedService<IMessageWriter>("colorWriter"); ArgumentNullException.ThrowIfNull(writer); writer.SendMessage("Привет");
Или, можно при регистрации сервиса, используя один из доступных нам методов, зарегистрировать фабрику, которая будет передавать в конструктор необходимые зависимости. На примере нашего сервиса Calculator это может выглядеть вот так:
services.AddTransient(x => { return new Calculator(x.GetRequiredKeyedService<IMessageWriter>("colorWriter")); });
Здесь мы использовали метод GetRequiredKeyedService()
для получения сервиса с ключом и передачи его в конструктор. Отличие этого метода от GetKeyedService()
заключается в том, что GetRequiredKeyedService()
вызовет исключение, если по каким-то причинам мы не сможем запросить необходимый нам сервис, а GetKeyedService()
просто вернет нам значение null
.
Использование атрибута [FromKeyedServices]
Также, если используется запрос сервиса через конструктор класса, мы можем использовать специальный атрибут, расположенный в пространстве имен Microsoft.Extensions.DependencyInjection — [FromKeyedServices]
. Например,
public Calculator([FromKeyedServices("colorWriter")]IMessageWriter writer) { ArgumentNullException.ThrowIfNull(writer); this.writer = writer; }
Теперь даже, если мы зарегистрируем несколько реализаций сервиса, указав для каждого ключи, то, используя этот атрибут мы можем указать конкретную реализацию для работы.
Итого
Начиная с версии .NET 8 мы можем регистрировать сервисы с ключами, что позволяет нам обеспечить множественную регистрацию сервисов — когда на один сервис в приложении используется несколько его реализаций. Для указания того, какая реализация сервиса нам необходима, мы можем использовать ключ сервиса, указав его, например, в специальном атрибуте [FromKeyedServices]
.