Наверное, все сталкивались с таким паттерном проектирования, как 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! только единожды - при старте приложения.
Комментарии (16)
SquirreL_Leonid
15.12.2021 12:26Если мы внедряем временную зависимость в зависимость с заданной областью, то она превращается в зависимость с заданной областью.
Это отношение симметрично?
eugene_naryshkin Автор
15.12.2021 12:27Добрый день!
Нет, это отношение не симметрично. Разрешаться scoped зависимость будет как обычно.
vdasus
15.12.2021 13:35-1Inversion of control(IoC, инверсия управления) и его формой — Dependency Injection (DI, внедрение зависимостей)
Наоборот. IoC — форма DIeugene_naryshkin Автор
15.12.2021 13:49-2Инверсия управления, это один паттернов проектирования, принципов объектного программирования, который имеет несколько способов реализации, в число которых и входит внедрение зависимостей.
Также он может быть реализован через паттерн "Фабрика", через локатор служб.
Так что приведенное мной утверждение про то, что DI является формой IoC - полностью верно.vdasus
15.12.2021 15:23-1Любой IoC это DI, но не любой DI это IoC... DI более общее понятие. По крайней мере я так думаю.
eugene_naryshkin Автор
15.12.2021 15:32-2DI - это способ инверсии управления насколько я знаю. Инвертировать же управление можно не только внедряя зависимости.
Но каждый имеет право на своё мнение, так что я думаю можно и прекратить дискуссию.mayorovp
15.12.2021 16:09Вы оба ерунду говорите.
Да, DI является реализацией IoC, но это не означает что любое применение DI реализует IoC.
Пример DI без признаков IoC — зависимость, не являющаяся точкой расширения. Например, зависимость от конкретного класса.
Пример IoC без признаков DI — aspx. Фреймворк создаёт и вызывает пользовательский код (признак IoC), но при этом управление зависимостями отсутствует в принципе.
eugene_naryshkin Автор
15.12.2021 17:16Ну вроде со своей стороны ерунды я не увидел. DI является формой реализации IoC и IoC также может иметь несколько других форм реализации.
Спасибо за разъяснение - очень удачное.
gdt
Подскажите, пожалуйста, из какой коробки предоставляется этот механизм? А то я открыл студию, создал новый проект .Net 5, и всё же пришлось ставить нугет. С такими раскладами вроде как нет разницы какой нугет ставить.
dotmeer
Из коробки с надписью ".net" на боку.
Это шутка такая.
Этот DI-контейнер поддерживается командой разработки .net, является механизмом по умолчанию.
А вот на счёт "нет разницы", это не так. DI-контейнеры разные, имеют разный API. Проблемы начинаются, когда приходится использовать библиотеки, не знающие о других di, кроме IServiceCollection.
gdt
А для чего библиотекам знать про IServiceCollection, ведь это по сути билдер контейнера? IServiceProvider с другой стороны (если мне не изменяет память) реализуют все контейнеры в той или иной форме.
dotmeer
Есть у меня история про 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'а.
Тогда нашли обходное решение и рекомендовали его использовать в таких случаях, а вот как было дальше, увы, не знаю.
mayorovp
Потому что в таких случаях надо не копировать описания из service collection, а заменять основной контейнер зависимостей. Благо, точку расширения для этого в Microsoft предусмотрели. Надеюсь, что именно это "обходное" решение и нашли в итоге.
VanKrock
насколько я знаю, то контейнер simpleinjector реализует IServiceProvider, так что по сути можно было зарегистрировать .AddSingleton<IServiceProvider, Container>()
mayorovp
А вы создавайте не .Net 5 проект, а ASP.NET Core 5 проект. Там из коробки будет.
gdt
Это понятно, речь про