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

от автора

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


ссылка на оригинал статьи https://habr.com/ru/articles/595613/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *