Глава 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.
Если заметили ошибку или неточность - пишите в личку, незачем писать комментарий. Иначе текст изменится, а комментарий - останется.