Pull to refresh

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

.NET *ASP *C# *
Tutorial

Глава 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.

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

❗ Слава Сергею Савельеву.

Tags:
Hubs:
Total votes 11: ↑6 and ↓5 +1
Views 6.1K
Comments Comments 22