Singleton, Transient, Scoped в .NET: краткая памятка

в 14:15, , рубрики: ASP.NET, asp.net core, asp.net mvc, dependency injection, di, dotnet, scoped, singleton, transient

В этой статье я постарался собрать краткий гайд по Singleton, Transient и Scoped. Статья рассчитана на тех, кто хотя бы немного знаком с DI в .NET и уже умеет добавлять/запрашивать сервисы через startup/controller или иным образом.


Стандартный DI контейнер в .NET, представленный интерфейсом IServiceCollection имеет 3 способа регистрации зависимостей: Singleton, Transient и Scoped:

services.AddSingleton<ISingletonService, SingletonService>();
services.AddTransient<ITransientService, TransientService>();
services.AddScoped<IScopedService, ScopedService>();

Способ регистрации влияет на время жизни, момент создания, момент уничтожения, вызов Dispose, а так же накладывает некоторые ограничения при внедрении зависимостей между сервисами с разными жизненными циклами.


Singleton - создает один единственный экземпляр зависимости, который передается при каждой инъекции. Это аналогично созданию одного new SingletonService() и сохранению его в глобальную переменную. Может быть полезно для производительности, если нет необходимости каждый раз создавать новый экземпляр.

// singletonService1 и singletonService2 - это ссылки на ОДИН и тот же объект
var singletonService1 = serviceProvider.GetService<ISingletonService>();
var singletonService2 = serviceProvider.GetService<ISingletonService>();

Transient - создает новый экземпляр зависимости при каждой инъекции. То есть, при каждом запросе ITransientService, мы будем получать только что созданный new TransientService()

// transientService1 и transientService2 - это ссылки на РАЗНЫЕ объекты
var transientService1 = serviceProvider.GetService<ITransientService>();
var transientService2 = serviceProvider.GetService<ITransientService>();

Scoped - это что-то среднее между предыдущими двумя вариантами. Глобально - мы будем многократно создавать новый scope, но внутри каждого scope будет создан только 1 ScopedService.

// scopedService1 и scopedService2 - это ссылки на ОДИН и тот же объект, созданный внутри scope1
var scope1 = serviceProvider.CreateScope();
var scopedService1 = scope1.ServiceProvider.GetService<IScopedService>();
var scopedService2 = scope1.ServiceProvider.GetService<IScopedService>();

// НО scopedService3 и scopedService4 - это ссылки на другой объект, созданный внутри scope2
var scope2 = serviceProvider.CreateScope();
var scopedService3 = scope2.ServiceProvider.GetService<IScopedService>();
var scopedService4 = scope2.ServiceProvider.GetService<IScopedService>();

Это полезно, когда нужно выделить изолированные рабочие сессии. В ASP.NET Core с использованием контроллеров, при каждом http-запросе автоматически создается новый scope:

public class MyController : Controller
{
    // контроллеры, пришедшие из MVC, регистрируются как Scoped
    // при каждом http запросе они создаются в новом scope вместе с новыми экземплярами scoped зависимостей
    public MyController(IScopedService scopedService)
    {
    }
}

Ограничения Scoped

Все Scoped, а так же Transient сервисы со scoped зависимостями должны создаваться ТОЛЬКО внутри конкретного scope, иначе будет выброшено исключение:

Cannot resolve scoped service from root provider

// 🚫 если ISomeService является scoped или transient со scoped-зависимостю внутри - упадет ошибка
var someService = serviceProvider.GetService<ISomeService>();
        
// ✅ правильный вариант, если ISomeService является scoped или transient со scoped-зависимостю внутри
var scope = serviceProvider.CreateScope();
var someService = scope.ServiceProvider.GetService<ISomeService>();

Ограничения Singleton.

Внутрь Singleton нельзя инжектить Scoped и Transient сервисы. В конструктор Singleton сервиса можно передавать только другие синглтоны, иначе будет выброшено исключение:

Cannot consume transient/scoped service SomeService from singleton ISingletonService

public class SingletonService : ISingletonService
{
    // 🚫 так нельзя: у Singleton в качестве зависимостей могут быть только другие Singleton
    public SingletonService(IScopedService scopedService, ITransientService transientService)
    {
    }
}

Для использования transient и scoped сервисов внутри singleton, необходимо передать в качестве зависимости IServiceProvider и вручную вызывать нужные сервисы через GetService/CreateScope:

public class SingletonService : ISingletonService
{
    private readonly IServiceProvider _serviceProvider;

    // ✅ IServiceProvider - это Singleton
    public SingletonService(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public async Task ExecuteWorkflow()
    {
        await using var scope = _serviceProvider.CreateAsyncScope();

        var scopedService = scope.ServiceProvider.GetService<IScopedService>();
        var transientService = scope.ServiceProvider.GetService<ITransientService>();

        // workflow...
    }
}

Контроль освобождения ресурсов через Dispose в DI

Некоторые сервисы могут реализовывать IDisposable (или IAsyncDisposable) и требовать явного освобождения ресурсов. В этом случае, для сервисов, созданных вне scope, требуется явно вызывать Dispose():

// если это многократно порождаемые IDisposable Transient сервисы, созданные "из корня"
var someService = serviceProvider.GetService<ISomeService>();
var otherService = serviceProvider.GetService<IOtherService>();

// Dispose необходимо вызывать явно
someService.Dispose();
otherService.Dispose();

Однако, если мы создаем Scope, мы можем вызвать Dispose() на нем, чтобы "задиспозить" сразу все зависимости, которые он породил:

// если сервисы создаются внутри Scope
var scope = serviceProvider.CreateScope();
var someService = scope.ServiceProvider.GetService<ISomeService>();
var otherService = scope.ServiceProvider.GetService<IOtherService>();
        
// Dispose скоупа таким образом так же вызовет Dispose() на всех IDisposable сервисах, которые были в нем созданы
scope.Dispose();

// 🚫 повторный вызов Dispose() в таком случае может привести к ошибке
someService.Dispose();
otherService.Dispose();

CreateAsyncScope

Для наглядности, почти во всех примерах выше, scope создавался следующим образом:

var scope = serviceProvider.CreateScope();

В современных версиях dotnet рекомендуется использовать новый метод CreateAsyncScope(), который возвращает расширенный AsyncScope, реализующий IAsyncDisposable вместо IDisposable, что улучшает работу с зависимостями, реализующими IAsyncDisposable:

await using var scope = serviceProvider.CreateAsyncScope();

Надеюсь, материал был хоть немного полезен :-)

Автор: Samidara

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js