В этой статье я постарался собрать краткий гайд по 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 (со Scoped зависимостями внутри), иначе будет ошибка:
Cannot consume scoped service ScopedService from singleton ISingletonService
Такая ошибка возникнет если SingletonService явно или транзитивно зависит от Scoped сервиса:
public class SingletonService : ISingletonService { // 🚫 так нельзя: у Singleton в качестве зависимостей не могут быть Scoped сервисы public SingletonService(IScopedService scopedService, ITransientService transientService) { } }
При этом, Transient сервисы без транзитивных Scoped зависимостей все еще допустимы
public class SingletonService : ISingletonService { // ⚠️ так можно, но только до тех пор, пока ITransientService не имеет транзитивных Scoped зависимостей public SingletonService(ITransientService transientService) { } }
Для лучшего контроля, в конструктор Singleton сервиса желательно передавать только другие синглтоны.
Так например, для использования 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... } }
Краткая таблица ограничений:
Что инжектим ---> | ---> Куда инжектим | ||
Singleton | Transient | Scoped | |
Singleton | ✅ можно | ✅ можно | ✅ можно |
Transient | ⚠️ можно, но осторожно | ✅ можно | ✅ можно |
Scoped | ❌ ошибка | ✅ можно | ✅ можно |
⚠️ - Transient не должен иметь транзитивных Scoped зависимостей чтобы инжектиться в Singleton
Контроль освобождения ресурсов через 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();
Надеюсь, материал был хоть немного полезен :-)
