Наверное, все сталкивались с таким паттерном проектирования, как Inversion of control(IoC, инверсия управления) и его формой - Dependency Injection (DI, внедрение зависимостей). .NET и, в частности, .Net Core предоставляют этот механизм «из коробки». Очень важным моментом является такое понятие, как Lifetime или, время существования зависимости.

В .NET существует три способа зарегистрировать зависимость:

  • AddTransient (Временная зависимость)

  • AddScoped (Зависимость с заданной областью действия)

  • AddSingleton (Одноэкземплярная зависимость)

Надо бы разобраться в различиях, поскольку и буквы в вышеописанных способах разные, и вообще, смысл слов тоже от способа к способу отличается. Хорошо, открываем поисковую систему и начинаем искать, в чём собственно различия. Так, везде написано про Asp.Net. Как же понять, как этот механизм работает в общем, отдельно от Asp.Net и запросов?

Перед тем, как приступить к рассмотрению различий, давайте немного освежим свои знания о составе механизма внедрения зависимостей, необходимого нам:

  • IServiceCollection, представляет собой коллекцию дескрипторов служб

  • IServiceProvider, представлет собой контейнер служб .NET

  • IServiceScopeFactory, фабрика для создания служб с заданной областью

Все объяснения про различия времени существования разрешенной зависимости сводятся к приведению примеров, построенных на запросах к веб-приложению. Ну, в принципе понятно - новый запрос => новый сервис(за исключением AddSingleton). Что ж, давайте попробуем понять более подробно, в чём же всё-таки различия.

Приведу простой пример на почтальонах. Давайте представим, что мы ждём получения письма. В обязанности почтальона будет входить следующая последовательность действий:

  1. Забрать письмо из отделения.

  2. Донести письмо до адресата.

  3. Получить подпись адресата о вручении.

  4. Вручить письмо адресату.

Эти действия определим в интерфейсе IPostmanService:

using System;

namespace DependencyInjectionConsole.Interfaces
{
    public interface IPostmanService
    {
        void PickUpLetter(string postmanType);
        void DeliverLetter(string postmanType);
        void GetSignature(string postmanType);
        void HandOverLetter(string postmanType);
    }
}

Также определим расширяющие интерфейс IPostmanService интерфейсы ITransientPostmanService, IScopedPostmanService, ISingletonPostmanService, для регистрации зависимости разными способами одной и той же реализации PostmanService:

using DependencyInjectionConsole.Interfaces;
using Microsoft.Extensions.Logging;
using System;

namespace DependencyInjectionConsole.Services
{
    public class PostmanService : 
        ITransientPostmanService,
        IScopedPostmanService,
        ISingletonPostmanService
    {
        private readonly string _name;
        private readonly string[] _possibleNames = new string[] { "Peter", "Jack", "Bob", "Alex" };
        private readonly string[] _possibleLastNames = new string[] { "Brown", "Jackson", "Gibson", "Williams" };
        private readonly ILogger<PostmanService> _logger;

        public PostmanService(ILogger<PostmanService> logger)
        {
            _logger = logger;

            var rnd = new Random();
            _name = $"{_possibleNames[rnd.Next(0, _possibleNames.Length - 1)]} {_possibleLastNames[rnd.Next(0, _possibleLastNames.Length - 1)]}";

            _logger.LogInformation($"Hi! My name is {_name}.");
        }

        public void DeliverLetter(string postmanType)
        {
            _logger.LogInformation($"Postman {_name} delivered the letter. [{postmanType}]");
        }

        public void GetSignature(string postmanType)
        {
            _logger.LogInformation($"Postman {_name} got a signature. [{postmanType}]");
        }

        public void HandOverLetter(string postmanType)
        {
            _logger.LogInformation($"Postman {_name} handed the letter. [{postmanType}]");
        }

        public void PickUpLetter(string postmanType)
        {
            _logger.LogInformation($"Postman {_name} took the letter. [{postmanType}]");
        }
    }
}

Все ключевые действия почтальона мы будем вызывать через некоего директора PostmanHandler, именно ему в конструктор будут внедряться зависимости наших почтальонов:

using DependencyInjectionConsole.Interfaces;
using System;

namespace DependencyInjectionConsole
{
    public class PostmanHandler
    {
        private readonly ITransientPostmanService _transientPostman;
        private readonly IScopedPostmanService _scopedPostman;
        private readonly ISingletonPostmanService _singletonPostman;

        public PostmanHandler(ITransientPostmanService transientPostman, IScopedPostmanService scopedPostman, ISingletonPostmanService singletonPostman)
        {
            _transientPostman = transientPostman;
            _scopedPostman = scopedPostman;
            _singletonPostman = singletonPostman;
        }

        public void PickUpLetter()
        {
            _transientPostman.PickUpLetter(nameof(_transientPostman));
            _scopedPostman.PickUpLetter(nameof(_scopedPostman));
            _singletonPostman.PickUpLetter(nameof(_singletonPostman));
        }

        public void DeliverLetter()
        {
            _transientPostman.DeliverLetter(nameof(_transientPostman));
            _scopedPostman.DeliverLetter(nameof(_scopedPostman));
            _singletonPostman.DeliverLetter(nameof(_singletonPostman));
        }

        public void GetSignature()
        {
            _transientPostman.GetSignature(nameof(_transientPostman));
            _scopedPostman.GetSignature(nameof(_scopedPostman));
            _singletonPostman.GetSignature(nameof(_singletonPostman));
        }

        public void HandOverLetter()
        {
            _transientPostman.HandOverLetter(nameof(_transientPostman));
            _scopedPostman.HandOverLetter(nameof(_scopedPostman));
            _singletonPostman.HandOverLetter(nameof(_singletonPostman));
        }
    }
}

И наконец, определим код в классе Program:

using DependencyInjectionConsole.Interfaces;
using DependencyInjectionConsole.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;

namespace DependencyInjectionConsole
{
    class Program
    {
        private static IServiceCollection ConfigureServices()
        {
            var services = new ServiceCollection();
            services.AddTransient<ITransientPostmanService, PostmanService>();
            services.AddScoped<IScopedPostmanService, PostmanService>();
            services.AddSingleton<ISingletonPostmanService, PostmanService>();

            services.AddTransient<PostmanHandler>();

            services.AddLogging(loggerBuilder =>
            {
                loggerBuilder.ClearProviders();
                loggerBuilder.AddConsole();
            });

            return services;
        }

        static void Main(string[] args)
        {
            PostmanHandler postman;

            var services = ConfigureServices();
            var serviceProvider = services.BuildServiceProvider();
            var scopeFactory = serviceProvider.GetService<IServiceScopeFactory>();

            postman = serviceProvider.GetService<PostmanHandler>();

            postman.PickUpLetter();
            postman = serviceProvider.GetService<PostmanHandler>();
            postman.DeliverLetter();
            postman = serviceProvider.GetService<PostmanHandler>();
            postman.GetSignature();
            postman = serviceProvider.GetService<PostmanHandler>();
            postman.HandOverLetter();

            Console.WriteLine("-----------------Scope changed!---------------------");

            using (var scope = scopeFactory.CreateScope())
            {
                postman = scope.ServiceProvider.GetService<PostmanHandler>();

                postman.PickUpLetter();
                postman = serviceProvider.GetService<PostmanHandler>();
                postman.DeliverLetter();
                postman = serviceProvider.GetService<PostmanHandler>();
                postman.GetSignature();
                postman = serviceProvider.GetService<PostmanHandler>();
                postman.HandOverLetter();
            }

            Console.ReadKey();
        }
    }
}

После запуска приложения мы увидим следующий вывод в консоли:

Консольный вывод

Hi! My name is Bob Gibson.
Hi! My name is Jack Jackson.
Hi! My name is Peter Jackson.
Postman Bob Gibson took the letter. [_transientPostman]
Postman Jack Jackson took the letter. [_scopedPostman]
Postman Peter Jackson took the letter. [_singletonPostman]
Hi! My name is Bob Gibson.
Postman Bob Gibson delivered the letter. [_transientPostman]
Postman Jack Jackson delivered the letter. [_scopedPostman]
Postman Peter Jackson delivered the letter. [_singletonPostman]
Hi! My name is Jack Gibson.
Postman Jack Gibson got a signature. [_transientPostman]
Postman Jack Jackson got a signature. [_scopedPostman]
Postman Peter Jackson got a signature. [_singletonPostman]
Hi! My name is Bob Gibson.
Postman Bob Gibson handed the letter. [_transientPostman]
Postman Jack Jackson handed the letter. [_scopedPostman]
Postman Peter Jackson handed the letter. [_singletonPostman]
-----------------Scope changed!---------------------
Hi! My name is Bob Gibson.
Hi! My name is Bob Brown. (Нас будет интересовать этот момент)
Postman Bob Gibson took the letter. [_transientPostman]
Postman Bob Brown took the letter. [_scopedPostman]
Postman Peter Jackson took the letter. [_singletonPostman]
Hi! My name is Peter Jackson.
Postman Peter Jackson delivered the letter. [_transientPostman]
Postman Jack Jackson delivered the letter. [_scopedPostman]
Postman Peter Jackson delivered the letter. [_singletonPostman]
Hi! My name is Bob Jackson.
Postman Bob Jackson got a signature. [_transientPostman]
Postman Jack Jackson got a signature. [_scopedPostman]
Postman Peter Jackson got a signature. [_singletonPostman]
Hi! My name is Peter Brown.
Postman Peter Brown handed the letter. [_transientPostman]
Postman Jack Jackson handed the letter. [_scopedPostman]
Postman Peter Jackson handed the letter. [_singletonPostman]

Пока немного отступим в сторону абстрактного объяснения на почтальонах.

Сначала посмотрим, как всё это будет выглядеть, если отделение почты зарегистрировало нам временного почтальона, т.е. воспользовавшись методом AddTransient:

Если перед выполнением каждого действия мы будем получать нового директора, то вместе с ним почтальон тоже будет создаваться новый. И так, каждое действие будет выполнять разный почтальон. Но, если директора мы будем использовать одного – то почтальон будет один.

Перейдём к более интересному способу регистрации зависимости – с заданной областью действия, т.е. AddScoped:

Нам абсолютно неважно, какой будет директор при выполнении каждого действия – каждый раз новый, или старый. Любой директор всегда будет вызывать одного и того же почтальона. Так будет происходить до тех пор, пока мы находимся в одной области (scope). Как только мы сменим область – почтальон также изменится. Этим и объясняются все примеры, связанные с Asp.Net – при каждом запросе создаётся новая область, в рамках которой выполняется работа.

И последний из способов – одноэкземплярная зависимость, т.е. AddSingleton:

Наш директор, как и любой другой знает – нет почтальона лучше, чем зарекомендовавший себя ветеран почтовых войн. При таком способе регистрации зависимости директор всегда будет получать одного и того же почтальона, неважно находимся ли мы в новой области или старой – весь срок жизни приложения наш почтальон будет с нами.

Из консольного вывода мы видим, что наш временный почтальон всегда разный. Наш почтальон с заданной областью меняется лишь единожды - после смены области через IServiceScopeFactory.CreateScope(). Наш одноэкземплярный почтальон остаётся всегда одним, даже когда мы меняем область.

Есть одна особенность контейнера зависимостей, о которой разработчик на платформе .NET должен помнить всегда при создании программного продукта.

Если мы внедряем временную зависимость в зависимость с заданной областью, то она превращается в зависимость с заданной областью.
Например если мы зарегистрируем зависимость PostmanHandler как Scoped:

            var services = new ServiceCollection();
            services.AddTransient<ITransientPostmanService, PostmanService>();
            services.AddScoped<IScopedPostmanService, PostmanService>();
            services.AddSingleton<ISingletonPostmanService, PostmanService>();
						//Теперь зависимость нашего директора имеет тип Scoped
            services.AddScoped<PostmanHandler>();

            services.AddLogging(loggerBuilder =>
            {
                loggerBuilder.ClearProviders();
                loggerBuilder.AddConsole();
            });

Наш временный почтальон станет почтальоном более постоянным (с заданной областью) и мы увидим от него Hi! только два раза, первый при старте приложения, второй при смене области.


Если же мы будем внедрять временную или с заданной областью зависимость в зависимость одноэкземплярную, то все они превратятся в зависимости одноэкземплярные.
Давайте зарегистрируем зависимость нашего директора PostmanHandler как Singleton:

            var services = new ServiceCollection();
            services.AddTransient<ITransientPostmanService, PostmanService>();
            services.AddScoped<IScopedPostmanService, PostmanService>();
            services.AddSingleton<ISingletonPostmanService, PostmanService>();
						//Теперь зависимость нашего директора имеет тип Singleton
            services.AddSingleton<PostmanHandler>();

            services.AddLogging(loggerBuilder =>
            {
                loggerBuilder.ClearProviders();
                loggerBuilder.AddConsole();
            });

            return services;

Наши временный и с заданной областью почтальоны станут бесповоротно постоянными (одноэкземплярными) и мы увидим от них Hi! только единожды - при старте приложения.

Комментарии (16)


  1. gdt
    15.12.2021 06:07

    Подскажите, пожалуйста, из какой коробки предоставляется этот механизм? А то я открыл студию, создал новый проект .Net 5, и всё же пришлось ставить нугет. С такими раскладами вроде как нет разницы какой нугет ставить.


    1. dotmeer
      15.12.2021 07:57

      Из коробки с надписью ".net" на боку.

      Это шутка такая.

      Этот DI-контейнер поддерживается командой разработки .net, является механизмом по умолчанию.

      А вот на счёт "нет разницы", это не так. DI-контейнеры разные, имеют разный API. Проблемы начинаются, когда приходится использовать библиотеки, не знающие о других di, кроме IServiceCollection.


      1. gdt
        15.12.2021 07:59

        А для чего библиотекам знать про IServiceCollection, ведь это по сути билдер контейнера? IServiceProvider с другой стороны (если мне не изменяет память) реализуют все контейнеры в той или иной форме.


        1. dotmeer
          15.12.2021 08:58
          +1

          Есть у меня история про simpleinjector как раз об этом.

          Такое поведение было в .net core 2.2, .net core 3.1, другие не использовал, так что утверждать не стану. Доступа к тому коду тоже нет, так что примеров не дам, к сожалению. Да и технические детали уже забылись, история будет о внешнем проявлении.

          Был в одной небольшой компании внутренний фреймворк, где использовался simpleinjector. У него и возможностей поболее, и пожестче он был в требованиях. Так что регистрировались все зависимости в контейнере si. Кроме тех, что в startup-классе aspnet-приложения, потому что там-то все на методах расширения service collection построено. И был у simple injector метод расширения, который (вроде бы) копировал все зависимости из контейнера service collection в контейнер simple injector.

          То есть si знает обо всех зависимостях, а service collection только о своих.

          Коллеги как-то столкнулись с тем, что у них не резолвятся зависимости, относящиеся к их классам при использовании в какой-то сторонней библиотеке. (Уже и подзабылись детали как-то.) Когда это дело поисследовали, оказалось, что зависимости этой библиотеки регистрировались и резолвились ServiceProvider'ом, а вот используемые далее классы лежали в контейнере simpleinjector'а.

          Тогда нашли обходное решение и рекомендовали его использовать в таких случаях, а вот как было дальше, увы, не знаю.


          1. mayorovp
            15.12.2021 10:42

            Потому что в таких случаях надо не копировать описания из service collection, а заменять основной контейнер зависимостей. Благо, точку расширения для этого в Microsoft предусмотрели. Надеюсь, что именно это "обходное" решение и нашли в итоге.


          1. VanKrock
            15.12.2021 17:56
            -1

            насколько я знаю, то контейнер simpleinjector реализует IServiceProvider, так что по сути можно было зарегистрировать .AddSingleton<IServiceProvider, Container>()


    1. mayorovp
      15.12.2021 10:44

      А вы создавайте не .Net 5 проект, а ASP.NET Core 5 проект. Там из коробки будет.


      1. gdt
        15.12.2021 10:48

        Это понятно, речь про

        NET и, в частности, .Net Core предоставляют этот механизм «из коробки»


  1. SquirreL_Leonid
    15.12.2021 12:26

    Если мы внедряем временную зависимость в зависимость с заданной областью, то она превращается в зависимость с заданной областью.

    Это отношение симметрично?


    1. eugene_naryshkin Автор
      15.12.2021 12:27

      Добрый день!
      Нет, это отношение не симметрично. Разрешаться scoped зависимость будет как обычно.


  1. vdasus
    15.12.2021 13:35
    -1

    Inversion of control(IoC, инверсия управления) и его формой — Dependency Injection (DI, внедрение зависимостей)


    Наоборот. IoC — форма DI


    1. eugene_naryshkin Автор
      15.12.2021 13:49
      -2

      Инверсия управления, это один паттернов проектирования, принципов объектного программирования, который имеет несколько способов реализации, в число которых и входит внедрение зависимостей.
      Также он может быть реализован через паттерн "Фабрика", через локатор служб.
      Так что приведенное мной утверждение про то, что DI является формой IoC - полностью верно.


      1. vdasus
        15.12.2021 15:23
        -1

        Любой IoC это DI, но не любой DI это IoC... DI более общее понятие. По крайней мере я так думаю.


        1. eugene_naryshkin Автор
          15.12.2021 15:32
          -2

          DI - это способ инверсии управления насколько я знаю. Инвертировать же управление можно не только внедряя зависимости.
          Но каждый имеет право на своё мнение, так что я думаю можно и прекратить дискуссию.


          1. mayorovp
            15.12.2021 16:09

            Вы оба ерунду говорите.


            Да, DI является реализацией IoC, но это не означает что любое применение DI реализует IoC.


            Пример DI без признаков IoC — зависимость, не являющаяся точкой расширения. Например, зависимость от конкретного класса.


            Пример IoC без признаков DI — aspx. Фреймворк создаёт и вызывает пользовательский код (признак IoC), но при этом управление зависимостями отсутствует в принципе.


            1. eugene_naryshkin Автор
              15.12.2021 17:16

              Ну вроде со своей стороны ерунды я не увидел. DI является формой реализации IoC и IoC также может иметь несколько других форм реализации.
              Спасибо за разъяснение - очень удачное.