Наверное, все сталкивались с таким паттерном проектирования, как Inversion of control(IoC, инверсия управления) и его формой - Dependency Injection (DI, внедрение зависимостей). .NET и, в частности, .Net Core предоставляют этот механизм «из коробки». Очень важным моментом является такое понятие, как Lifetime или, время существования зависимости.
В .NET существует три способа зарегистрировать зависимость:
AddTransient (Временная зависимость)
AddScoped (Зависимость с заданной областью действия)
AddSingleton (Одноэкземплярная зависимость)
Надо бы разобраться в различиях, поскольку и буквы в вышеописанных способах разные, и вообще, смысл слов тоже от способа к способу отличается. Хорошо, открываем поисковую систему и начинаем искать, в чём собственно различия. Так, везде написано про Asp.Net. Как же понять, как этот механизм работает в общем, отдельно от Asp.Net и запросов?
Перед тем, как приступить к рассмотрению различий, давайте немного освежим свои знания о составе механизма внедрения зависимостей, необходимого нам:
IServiceCollection, представляет собой коллекцию дескрипторов служб
IServiceProvider, представлет собой контейнер служб .NET
IServiceScopeFactory, фабрика для создания служб с заданной областью
Все объяснения про различия времени существования разрешенной зависимости сводятся к приведению примеров, построенных на запросах к веб-приложению. Ну, в принципе понятно - новый запрос => новый сервис(за исключением AddSingleton). Что ж, давайте попробуем понять более подробно, в чём же всё-таки различия.
Приведу простой пример на почтальонах. Давайте представим, что мы ждём получения письма. В обязанности почтальона будет входить следующая последовательность действий:
Забрать письмо из отделения.
Донести письмо до адресата.
Получить подпись адресата о вручении.
Вручить письмо адресату.
Эти действия определим в интерфейсе 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! только единожды - при старте приложения.
