Как стать автором
Обновить

Dependency Injection в .NET на почтальонах

Время на прочтение8 мин
Количество просмотров25K

Наверное, все сталкивались с таким паттерном проектирования, как 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! только единожды - при старте приложения.

Теги:
Хабы:
Всего голосов 4: ↑2 и ↓20
Комментарии20

Публикации

Истории

Работа

Ближайшие события