Содержание
Наши сервисы, обычно, реализуются в виде классов. При этом, классы C# могут иметь несколько конструкторов с различным набором параметров. Когда мы задействуем в работе контейнер DI, то мы фактически перекладываем все обязанности по созданию объектов сервисов на контейнер. Каким образом контейнер DI выбирает наиболее подходящий конструктор для создания объекта? В этой части мы рассмотрим правила обнаружения конструктора в .NET/C# использовании контейнера DI из Microsoft.Extensions.DependencyInjection.
Правила обнаружения конструктора
Если класс содержит сразу несколько конструкторов, то в .NET у контейнера DI включается логика обнаружения конструктора, который будет использоваться для создания объекта. Логика достаточно простая — выбирается тот конструктор, который имеет больше всего параметров, в которых типы могут разрешаться с внедрением зависимостей. Проверим эту логику на примере нашего класса Calculator
(полный исходный код приложения можно скопировать из этой части) — добавим для класса ещё несколько конструкторов:
public class Calculator { private readonly IMessageWriter writer; private readonly IMessageWriter simpleWriter; public Calculator([FromKeyedServices("colorWriter")]IMessageWriter writer) { ArgumentNullException.ThrowIfNull(writer); this.writer = writer; } public Calculator([FromKeyedServices("colorWriter")] IMessageWriter writer, [FromKeyedServices("simpleWriter")] IMessageWriter simpleWriter) { ArgumentNullException.ThrowIfNull(simpleWriter); ArgumentNullException.ThrowIfNull(writer); this.writer = writer; this.simpleWriter = simpleWriter; } public Calculator([FromKeyedServices("colorWriter")] IMessageWriter writer, [FromKeyedServices("simpleWriter")] IMessageWriter simpleWriter, string helloString="Hello world!") : this(writer) { ArgumentNullException.ThrowIfNull(simpleWriter); ArgumentNullException.ThrowIfNull(writer); this.writer = writer; this.simpleWriter = simpleWriter; simpleWriter.SendMessage(helloString); } public void Sum(int a, int b) { writer.SendMessage($"Сумма двух чисел равна: {a + b}"); } public void Multiply(int a, int b) { writer.SendMessage($"Произведение двух чисел равно: {a * b}"); } }
Здесь у нас определено три конструктора — с одним сервисом, с двумя сервисами и третий — с двумя сервисами и параметром в виде обычной строки. Можно запустить приложение и убедиться, что при разрешении зависимостей, контейнер DI выберет второй конструктор в котором запрашивается два сервиса без каких-либо дополнительных параметров, так как третий параметр (строка), а точнее её значение не может быть разрешено как зависимость в контейнере DI. Однако, если вам крайне необходимо использовать такой конструктор, то вы всегда можете зарегистрировать сервис с использование метода расширения в котором используется фабрика для создания экземпляров сервисов.
Второй момент, который стоит учитывать можно сформулировать так: избегайте создания в классе нескольких конструкторов с равным количеством зависимостей в параметрах. Такая ситуация приводит к исключению. Например, изменим наш класс Calculator
(не забудьте потом удалить лишний код)
using Microsoft.Extensions.DependencyInjection; namespace Example1 { public interface IAnotherService { void Foo(); } public class AnotherService : IAnotherService { public void Foo() { } } public class Calculator { private readonly IMessageWriter writer; private readonly IAnotherService anotherService; //private readonly IMessageWriter simpleWriter; public Calculator([FromKeyedServices("colorWriter")]IMessageWriter writer) { ArgumentNullException.ThrowIfNull(writer); this.writer = writer; } public Calculator(IAnotherService another) { ArgumentNullException.ThrowIfNull(another); anotherService = another; } public void Sum(int a, int b) { writer.SendMessage($"Сумма двух чисел равна: {a + b}"); } public void Multiply(int a, int b) { writer.SendMessage($"Произведение двух чисел равно: {a * b}"); } } }
Здесь, для примера, добавлен новый сервис IAnotherService
и его реализация AnotherService
(сервис также зарегистрирован в контейнере DI в файле Program.cs). У класса Calculator
определено два конструктора в каждом из которых запрашивается свой сервис. Как только мы попробуем запустить приложение, то получим следующее исключение:
Так как два конструктора имеют равное количество параметров, которые разрешаются контейнером DI, то возникает ошибка — класс содержит два неоднозначных конструктора. Чтобы избегать таких ситуаций, лучше в одном конструкторе запрашивать все необходимые зависимости, например, так:
public Calculator([FromKeyedServices("colorWriter")]IMessageWriter writer, IAnotherService another) { ArgumentNullException.ThrowIfNull(writer); ArgumentNullException.ThrowIfNull(another); this.writer = writer; anotherService = another; }
Итого
Если класс, реализующий сервис, содержит несколько открытых конструкторов с различным количеством параметров, то для создания объекта выбирается тот конструктор, который имеет наибольшее число параметров, которые могут быть разрешены с использованием механизма внедрения зависимостей. При создании таких классов следует избегать создания конструкторов с равным количеством параметров, которые могут быть разрешены с использованием контейнера DI — это приводит к неоднозначности при определении необходимого конструктора. которая выливается в ошибку на этапе работы приложения.