Мы притворимся настоящими серьезными профессионалами и что такое Dependency Injection или внедрение зависимостей мы якобы все тут знаем, поэтому пережевывать эту тему я, с вашего позволения, не буду. Вот офф. документация, довольно подробная.

К делу. В первую очередь нам понадобится официальный nuget пакет.

Microsoft.Extensions.DependencyInjection

Далее, мы используем класс этой библиотеки ServiceCollection реализующийIServiceCollection . Содержит коллекцию экземпляров и способов их инициализации. Затем можно создавать экземпляры через ServiceProvider : IServiceProvider . В библиотеке есть расширение с методом .BuildServiceProvider() , но перед получением провайдера, нужно определить правила инъекций.

Всего есть 3 типа жизни экземпляра:

  • Singleton - одиночка, создается один раз и используется во время использования всего процесса, метод .AddSingleton<T>().

  • Transient - временный, создается каждый раз при запросе его из провайдера, метод .AddTransient<T>().

  • Scoped - ограниченный, новый экземпляр создается в определённой области видимости (scope) в интерфейсе IServiceScope. Добавляется методом .AddScoped<T>().

Если с первыми двумя все понятно - то вот третий вызывает вопросы. Собственно ради него статью и пишу.

Заданные через scope экземпляры будут жить в области видимости IScopedService и будут уничтожены через IDisposable тогда, когда будет уничтожен сам скоп. В каждой области видимости будет создаваться и жить свой экземпляр.

Таким же образом работает и под капотом: один скоп создается на один http запрос в ASP.NET, но применение может быть самое разное. Если знаете еще примеры нативной поддержки - пишите, пожалуйста, в комментариях.

Ниже пример кода (в спойлере) на дефолтных нетовских скопах, а еще ниже - разбор кода по блокам. Программа по (IFeed) кормлению уточек (IDuck). Каждая уточка живет в своей границе scope. Мы можем создать кормильца (DuckFeeder) для уточки, но только в определенных границах. Всего кормильца три: Саша, Миа и Рейли. Саша кормит уточку без определенных границ (в границе scope, созданной по умолчанию). Два остальных - в одном скопе и кормят одну свою уточку.

Код кормления уточек на C#
/* C#10 .NET6 */
using DuckFeeding;

/* This is the nugget package we need */
using Microsoft.Extensions.DependencyInjection;

/* This is what happens in Startup class */
var provider = new ServiceCollection()
    .AddTransient<List<IFood>>()
    .AddScoped<Duck>()

    /* Factory method for getting instance of Duck in scope */
    .AddTransient<IFeed>(s => s.GetRequiredService<Duck>())
    .AddTransient<IDuck>(s => s.GetRequiredService<Duck>())
    .AddTransient<DuckFeeder>()
    .BuildServiceProvider();

/* Sasha feeds duck in main scope */
var sasha = provider.GetRequiredService<DuckFeeder>();
sasha.Feed(new Milk());
sasha.Ask();/* 1. Milk */

/* Mia & Riley feeds duck in second scope */
using (var duckFeedScope = provider.CreateScope())
{
    var mia = duckFeedScope.ServiceProvider.GetRequiredService<DuckFeeder>();
    mia.Feed(new Apple());
    mia.Ask();/* 2. Apple */

    var riley = duckFeedScope.ServiceProvider.GetRequiredService<DuckFeeder>();
    riley.Feed(new Banana());
    riley.Ask();/* 2. Apple, Banana */

    /* Scope does'nt matters for Sasha, she still feeds her scoped duck */
    sasha.Feed(new Milk());
    sasha.Ask();/* 1. Milk,Milk */
}

//  Result output
//  #1. Milk { }                    <- Sasha
//  #2. Apple { }                       <- Mia, From scope #2
//  #2. Apple { }, Banana { }           <- Riley, From scope #2
//  #1. Milk { }, Milk { }           <- Sasha, from scope #1
//  Disposed:#2. Count: 2               <- Disposed, From scope #2

/* Implementation */
namespace DuckFeeding
{
    internal record DuckFeeder(IFeed Feeded, IDuck Duck)
    {
        public void Feed(IFood food) => Feeded.Eat(food);
        public void Ask() => Duck.Quack();
    }

    public interface IFeed { void Eat(IFood food); }
    public interface IFood { }
    internal record Apple() : IFood;
    internal record Banana() : IFood;
    internal record Milk() : IFood;

    public interface IDuck { void Quack(); }
    internal record Duck(List<IFood> Foods) : IDuck, IFeed, IDisposable
    {
        private bool _disposed = false;
        private static uint Id { get; set; }
        private string Name { get; } = ++Id + ".";
        public void Quack() => Console.WriteLine("#{0} {1}", Name, string.Join(", ", Foods));
        public void Eat(IFood food) => Foods.Add(food);
        public void Dispose()
        {
            if (_disposed) 
            { 
                return;
            }
            Console.WriteLine("Disposed:#{0} Count: {1}", Name, Foods.Count);
            Foods.Clear();
            _disposed = true;
        }
    }
}

Ниже, мы определяем, что всё скоротечно кроме уточки, которая определена как принадлежащая определенной области видимости.

То, что она будет реализовывать интерфейсы IFeed и IDuck - мы задаем через фабричный метод s => s.GetRequiredService() в противном случае, .NET будет создавать новый экземпляр на каждый тип интерфейса. В third-party библиотеках это реализуется иначе и красивше, но у нас тут ванила, нативность и немножко хардкор.

/* This is what happens in Startup class */
var provider = new ServiceCollection()
    .AddTransient<List<IFood>>()
    .AddScoped<Duck>()

    /* Factory method for getting instance of Duck in scope */
    .AddTransient<IFeed>(s => s.GetRequiredService<Duck>())
    .AddTransient<IDuck>(s => s.GetRequiredService<Duck>())
    .AddTransient<DuckFeeder>()
    .BuildServiceProvider();

Далее мы запрашиваем из провайдера кормильца Сашу. Т.к. scope не определен - будет использоваться по умолчанию созданный, т.н. основной.

/* Sasha feeds duck in main scope */
var sasha = provider.GetRequiredService<DuckFeeder>();
sasha.Feed(new Milk());

sasha.Ask();/* 1. Milk */

Тут мы создаем новую область видимости scope, и в ней получаем свой отдельный провайдер, который будет все добавленные через .AddScoped() объекты создавать заново. Поэтому mia создаст новую уточку, а riley - будет кормить созданную в этой области. sasha же продолжит кормить уточку из своей области.

/* Mia & Riley feeds duck in second scope */
using (var duckFeedScope = provider.CreateScope())
{
    var mia = duckFeedScope.ServiceProvider.GetRequiredService<DuckFeeder>();
    mia.Feed(new Apple());
    mia.Ask();/* 2. Apple */

    var riley = duckFeedScope.ServiceProvider.GetRequiredService<DuckFeeder>();
    riley.Feed(new Banana());
    riley.Ask();/* 2. Apple, Banana */

    /* Scope dont matters for Sasha, she still feeds her scope duck */
    sasha.Feed(new Milk());
    sasha.Ask();/* 1. Milk, Milk */
}

Реализацию классов я разжевывать не буду, только основной - Duck. Тут из интересного только IDisposable. Область видимости IScopedService так же наследует IDisposable. И все объекты, которые были созданы в этой области видимости уничтожаются вместе с ней. Поэтому тут наследован IDisposable, когда скоп уничтожается - уточка исчезает вместе с ней (в сборщик мусора).

 internal record Duck(List<IFood> Foods) : IDuck, IFeed, IDisposable
    {
        private bool _disposed = false;
        private static uint Id { get; set; }
        private string Name { get; } = ++Id + ".";
        public void Quack() => Console.WriteLine("#{0} {1}", Name, string.Join(", ", Foods));
        public void Eat(IFood food) => Foods.Add(food);
        public void Dispose()
        {
            if (_disposed) 
            { 
                return;
            }
            Console.WriteLine("Disposed:#{0} Count: {1}", Name, Foods.Count);
            Foods.Clear();
            _disposed = true;
        }
    }

Теперь, когда все беременные и слабонервные ушли, можно поговорить и о настоящей жести. То бишь - о собственной реализации IServiceScopeFactory и всего с ним связанного. Вместе всего в комплект связанны три интерфейса, вложенные друг в друга. На самом деле - ничего реализовывать уже не надо, все и так уже есть в комплекте фреймворка, то есть доступно "из коробки". А если очень очень хочется - то конечно можно, вот только зачем?

    public interface IServiceScopeFactory
    {
        IServiceScope CreateScope();
    }
  
    public interface IServiceScope : IDisposable
    {
        IServiceProvider ServiceProvider
        {
            get;
        }
    }
  
    public interface IServiceProvider
    {
        object? GetService(Type serviceType);
    }

P.S. Хотел дополнить еще примерами и применимостью, но время было уже 5 утра на момент написания, а с утра - на работу. Хотел дописать на след. день, но по результатам обратной связи:

❗ Postscriptum

Ссылка на код с проектом в GitLab.

Если заметили ошибку или неточность - пишите в личку, незачем писать комментарий. Иначе текст изменится, а комментарий - останется.

Слава Савельеву.

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


  1. gdt
    10.12.2021 05:10

    Подскажите, пожалуйста, а вот так не будет работать?

    .AddTransient<IFeed, Duck>()

    Не силён в Microsoft.Extensions.DependencyInjection, но в других популярных контейнерах обычно это срабатывает.


    1. ildarin Автор
      10.12.2021 05:28
      +1

      То, что она будет реализовывать интерфейсы IFeed и IDuck - мы задаем через фабричный метод s => s.GetRequiredService() в противном случае, .NET будет создавать новый экземпляр на каждый тип интерфейса. 

      По Вашему - везде где, например, инъекция в конструктор .ctor(IFeed feed) будет создаваться новый объект new Duck() в IFeed. Таков .NET.


      1. gdt
        10.12.2021 05:30

        Жаль, тот же SimpleInjector хоть и прост но достаточно умён для того, чтобы использовать предыдущую регистрацию Duck (в том случае, если она есть).


        1. pankraty
          10.12.2021 06:03
          +3

          Мне кажется, дело тут не столько в том, насколько "умён" DI-фреймворк, а в том, что в разных ситуациях разработчику может требоваться разное поведение - иногда ему принципиально важно получить один и тот же экземпляр, внедряя зависимость по любому из его интерфейсов, а в других случаях - принципиально важно, чтобы создавался новый экземпляр. DI от Microsoft предоставляет возможность это сделать, а значит свою задачу решает (пусть и не самым интуитивным образом). Тем более что в подавляющем большинстве случаев это вообще не имеет значение, т.к. класс регистрируется по одному интерфейсу.


          1. gdt
            10.12.2021 06:38
            +1

            Вы знаете я перепроверил, и SimpleInjector в принципе так же себя ведёт - только кидает исключение о том, что для регистраций используются разные lifestyle, из-за чего каждый раз будут получены новые экземпляры (если не отключить автоверификацию контейнера). Жаль, очень жаль :)


  1. Kolonist
    10.12.2021 07:52
    +1

    А почему в коде везде record-ы? В этом есть какой-то профит в .NET 6 или это просто упрощение/случайность для примера?


    1. ildarin Автор
      10.12.2021 10:22

      Упрощение.


  1. korsetlr473
    10.12.2021 10:29

    Здрасте, если в конструктор передаю классы с дефолтным конструктором ,в этом DI появилась фишка как в Ninject без мэпинга? что бы не писать вот такие партянки

    s.AddTransient<Duck>()

    s.AddTransient<Duck2>()

    s.AddTransient<Duck3>()

    s.AddTransient<Duck4>()


    1. ildarin Автор
      10.12.2021 10:44

      Дефолтный - пустой? Не слышал, врят-ли.


    1. gdt
      10.12.2021 10:55
      +1

      Всегда можно написать небольшой модуль, подгружающий нужные вам типы через reflection. Также обычно у контейнеров есть точки расширения, позволяющие создавать то, чего в принципе в виде отдельного класса не существует в проекте (но не знаю как с этим тут).


      1. korsetlr473
        10.12.2021 11:57

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

        provider.AddScoped(NotMapped.AutoCreateInstanceWithDefaultConstructors);


        1. gdt
          10.12.2021 12:55

          Возможно кто-то уже озадачился и сделал nuget с экстеншнами, как это часто бывает (или нет :))


  1. ol_x
    10.12.2021 11:56

    как по мне, это уже не DI, а извращенный локатор сервисов.


    1. ildarin Автор
      10.12.2021 14:46

      Минус я поставил, если что) Сейчас объясню.

      Почему извращенный, почему локатор? Смотрим в namespace DuckFeeding

      Там нет ссылок на сервис локатор. Вообще никаких. Т.е. эта область кода не завязывается никак на сервисы. В этом и профит.

      А точка сборки не отличается для локатора и DI, она в самом начале листинга. Это и инкапсулировано за реализацией, в .NET, я просто показал, как оно внутри работает.

      Советую почитать http://sergeyteplyakov.blogspot.com/2013/03/di-service-locator.html


      1. mvv-rus
        11.12.2021 02:52
        -1

        Почему извращенный, почему локатор? Смотрим в namespace DuckFeeding

        А если не закрывать глаза, а посмотреть на программу целиком, то что мы видим?
        Тот самый локатор и видим:

        var sasha = provider.GetRequiredService();
        Вообще, в самом языке C# никакого DI нет. А иллюзия поддержки DI создается конкретными фремворками, например — ASP .NET Core. И работает магия DI исключительно потому, что написанные пользователем классы(т.е. их конструкторы) и/или делегаты вызываются изнутри фреймворка, который имеет и использует ссылку на интерфейс IServiceProvider своего контейнера сервисов. Точно так же и ограниченная область, в пределах которой создаются объекты, предоставляющие сервисы с временем жизни Scoped, создается обращением к IServiceScope.ServiceProvider где-то внутри фреймворка.
        У вас фреймворка нет, поэтому вам к контейнеру сервисов приходится обращаться явно.
        Так что ваш несправедливый минус я убрал.


        1. ildarin Автор
          11.12.2021 03:28

          Вообще, в самом языке C# никакого DI нет. 

          Ну нет так нет) Боб Мартин врал нам всё это время! ???? А ведь мы ему верили... как же я был глуп и наивен! ????

          У вас фреймворка нет

          Ну ок, нет так нет. Прям как в анекдоте "Товарищъ сеньор, я свой фреймворк не чувствую"???? Правда не ясно, как код компилится и запускается, без фреймворка, ну да ладно, "как-то магически" тоже вариант))

          Scoped, создается обращением к IServiceScope.ServiceProvider где-то внутри фреймворка.

          Ну ок, но зачем пересказывать статью в комментарии-критике к этой же статье) И не "где-то", а вполне конкретные места есть и я их даже видел)

          DI - это я так сократил di ioc. В карму плюсы всем комментаторам, т.к. чот тут совсем тухло стало, я как бы 8 лет сюда не заходил) Но в коммент - минус, т.к. складывается ощущение, будто пишущие вообще не понимают, о чем пишут.


          1. mvv-rus
            11.12.2021 06:49
            +1

            Ну нет так нет) Боб Мартин врал нам всё это время!

            Вы хотите мне возразить? В таком случае покажите ту конструкцию (одну или несколько) языка C# из language reference которая реализует DI без явного вызова библиотечной функции/метода.
            Правда не ясно, как код компилится и запускается, без фреймворка, ну да ладно, «как-то магически» тоже вариант))

            Дык, я код компилил и запускал лет десять, наверное, прежде чем столкнулся с первым в своей жизни фреймворком (это была Borland Object Windows Library) — ту штуку, с помощью которой я это делал называли «система программирования» или ещё как-то примерно так. Ключевое отличие этой штуки от фреймворка — она не навязывала структуру программы.
            Впрочем, если вам нравится называть систему программирования C# словом «фреймворк» — ваше право. Только, в таком случае перефразирую предыдущий вопрос: покажите, где в этом фреймворке живет DI. Классы из пространства имен Microsoft.Extensions.DependencyInjection не предлагать: про них по самому названию пространства имен понятно, что это — расширение, набор неких дополнительных возможностей.
            Если вы хотите сказать при этом за IServiceProvider — что он из основной системной библиотеки — то я вам скажу, что в DI просто использовали подходящий интерфейс, который был задолго до DI (в древние времена он использовался для взаимодействия с COM).
            Ну ок, но зачем пересказывать статью в комментарии-критике к этой же статье) И не «где-то», а вполне конкретные места есть и я их даже видел)

            Дык, я тоже видел. А про то, как контейнер сервисов инициализуется в ASP.NET Core Generic Host Template даже статью писал здесь, на Хабре. Но я за другое хотел сказать: пользовательский код, который использует DI, типа контроллеров MVC — он вызывается изнутри фреймворка, и именно потому может невозбранно использовать магию DI для подстановки ссылок на реализации интерфейсов или классов в конструкторах и т.д.
            Без ASP.NET Core или чего-то подобного, т.е. на чистом C#, такой код написать нельзя: где-то потребуется явно вызвать локатор сервисов.
            Ну, а так в статье все хорошо — разве что, лично я не люблю уток ;-)
            PS За плюс к карме благодарю.


            1. ildarin Автор
              11.12.2021 09:46

              Для начала объясню по понятиям:

              DI - это внедрение зависимости. И пример: два класса, один без DI, второй с инъекцией в конструктор (DuckDi). Это реализация принципа IoC, когда зависимости мы отдаем на "внешний код", например, IoC контейнеру, которым является ServiceCollection. Он сам уже по тем или иным принципам выбирает, какой IFeed выдать утке.

              class Duck{
                IFeed Feed
                Duck()
                  {
                  	Feed = new MyFeed();
                  }
                }
              }
              
              class DuckDi{
                IFeed Feed;
                Duck(IFeed feed)
                {
                	Feed = feed;
                }
              }

              А вот пример применения паттерна ServiceLocator. Разница с Ioc Contaner в том, где определяются зависимости. Если в самом классе локатора - то, это локатор. Если где-то снаружи - то это контейнер. Как раз в namespace DuckFeed - контейнер снаружи.

              class Duck{
               IFeed Feed;
                Duck(){
                  var locator = new ServiceLocator();
                 	Feed = locator.GetService<IFeed>() 
                }
              }
              class ServiceLocator{
               T GetService<T>(){
                 return new ServFeed();
               }
              }

              Думаю теперь, когда я привел примеры, тема сабжа стала понятнее. Это как раз то, что я и пропустил в статье в начале, т.к. об этом написано куча статей и даже книг, тот самый Мартин - как раз одну такую написал, рекомендую к прочтению.

              Фре́ймворк — программная платформа, определяющая структуру программной системы; программное обеспечение, облегчающее разработку и объединение разных компонентов большого программного проекта.

              Без .NET Framework скомпилированный код C# даже не запуститься, поэтому он и Framework. Т.к. компилируется он в байт код для машины CLR, а не в прямые команды процессора.

              Ответ на коммент ниже:

              2. При вызове конструктора можно вместо ссылки на сервис указать ключевое слово, которое вызывает запрос в DI, типа так:
              var obj=new Xyzzy(inject,inject).

              Тут я вообще не понимаю, в чем смысл?

              А система программирования находит и подставляет реализации сервисов из контейнера DI.

              Она и так это делает, только без всяких атрибутов.


          1. mvv-rus
            11.12.2021 07:26

            Подумал, что после предыдущего комментария меня опять могут понять неправильно, и решил пояснить, как могла бы выглядеть поддержка DI в самом языке C#.

            1. Классы, которые реализуют сервисы, помечаются специальным атрибутом, типа так
            [DependencyInjection]
            class Abc: IAbc
            {

            }
            Основываясь на этом атрибуте система программирования создает и наполняет контейнер DI.
            2. При вызове конструктора можно вместо ссылки на сервис указать ключевое слово, которое вызывает запрос в DI, типа так:
            var obj=new Xyzzy(inject,inject).
            А система программирования находит и подставляет реализации сервисов из контейнера DI.

            К сожалению, ничего подобного в C# не завезли.


            1. dopusteam
              11.12.2021 10:02

              Да просто в ветке смешались Dependency Inversion и Dependecy Injection)


    1. ildarin Автор
      10.12.2021 14:48

      Можно было бы еще выделить один уровень абстракции и разделить, но просто больше кода будет.