Содержание
Наши сервисы, обычно, реализуются в виде классов. При этом, классы 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 — это приводит к неоднозначности при определении необходимого конструктора. которая выливается в ошибку на этапе работы приложения.
