Правила обнаружения конструктора

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

Подписаться
Уведомить о
guest
0 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии