Что такое AddScoped и его отличие от AddTransient в .NET и ASP.NET

от автора

Глава 1. Теория

Мы притворимся настоящими серьезными профессионалами и что такое 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, но применение может быть самое разное. Если знаете еще примеры нативной поддержки — пишите, пожалуйста, в комментариях.

Глава 2. Учебный пример

Ниже пример кода (в спойлере) на дефолтных нетовских скопах, а еще ниже — разбор кода по блокам. Программа по (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;         }     }

Глава 3. Применение на практике.

Интерфейсы IServiceScopeFactory, IServiceScope, IServiceProvider:

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

Вместе всего в комплект связанны три интерфейса, вложенные друг в друга. На самом деле — ничего реализовывать уже не надо, все и так уже есть в комплекте фреймворка, то есть доступно «из коробки».

В том числе, это же работает и внутри библиотек фреймворка, а точнее — при запросах в сервисах. Например, в классе Controller, который используется в MVC (ASP.NET). В этом случае новый scope создается на каждый запрос, о чем и пишут о жизненном цикле зависимостей на metainit.

В то же время, никто не запрещает использовать свою логику для определения scope, пример реализации BackgroundService из оффициальной документации:

namespace App.ScopedService;  public sealed class ScopedBackgroundService : BackgroundService {     private readonly IServiceProvider _serviceProvider;     private readonly ILogger<ScopedBackgroundService> _logger;      public ScopedBackgroundService(         IServiceProvider serviceProvider,         ILogger<ScopedBackgroundService> logger) =>         (_serviceProvider, _logger) = (serviceProvider, logger);      protected override async Task ExecuteAsync(CancellationToken stoppingToken)     {         _logger.LogInformation(             $"{nameof(ScopedBackgroundService)} is running.");          await DoWorkAsync(stoppingToken);     }      private async Task DoWorkAsync(CancellationToken stoppingToken)     {         _logger.LogInformation(             $"{nameof(ScopedBackgroundService)} is working.");          using (IServiceScope scope = _serviceProvider.CreateScope())         {             IScopedProcessingService scopedProcessingService =                 scope.ServiceProvider.GetRequiredService<IScopedProcessingService>();              await scopedProcessingService.DoWorkAsync(stoppingToken);         }     }      public override async Task StopAsync(CancellationToken stoppingToken)     {         _logger.LogInformation(             $"{nameof(ScopedBackgroundService)} is stopping.");          await base.StopAsync(stoppingToken);     } }

Ссылки:

P.S.

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


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


Комментарии

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

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