В чем разница Scope и Transient в Dependency Injection
Что проверяют
Понимаете ли вы жизненный цикл сервисов во встроенном контейнере .NET и чем грозит их неправильное сочетание. Следом почти всегда спрашивают про Singleton и про то, что будет, если внедрить Scoped-сервис в Singleton.
Как отвечать
В Microsoft.Extensions.DependencyInjection три времени жизни:
- Transient (
AddTransient): новый экземпляр на каждое разрешение зависимости. Если сервис нужен двум классам в одном запросе, каждый получит свой; - Scoped (
AddScoped): один экземпляр на область. В ASP.NET Core область создаётся на каждый HTTP-запрос, и все классы внутри запроса делят один объект. Классический пример:DbContext, которыйAddDbContextпо умолчанию регистрирует как Scoped, поэтому репозитории в одном запросе работают в одной транзакционной единице; - Singleton (
AddSingleton): один экземпляр на всё приложение.
Правило выбора простое. Сервис без состояния и дешёвый в создании можно делать Transient. Всё, что держит состояние в пределах операции (контекст БД, текущий пользователь, единица работы), делают Scoped.
Вне HTTP-запроса области нет. В BackgroundService, который сам Singleton, и в консольном приложении её создают руками через IServiceScopeFactory.CreateScope() или CreateAsyncScope() и берут сервисы из scope.ServiceProvider.
Контейнер сам вызывает Dispose у созданных им объектов при закрытии их области, так что Transient освобождается вместе с областью, а не сразу после использования.
Хорошо упомянуть Blazor Server: там область живёт столько же, сколько соединение пользователя (circuit), и Scoped фактически становится сервисом на сессию.
Где спотыкаются
Внедряют Scoped в Singleton. Получается captive dependency: DbContext живёт всё время работы приложения и используется из разных потоков. В Development это ловит проверка ValidateScopes с InvalidOperationException, в продакшене она по умолчанию выключена.
Получают IDisposable Transient из корневого провайдера. Контейнер держит ссылку на каждый такой экземпляр до остановки приложения, и память течёт.
Говорят, что Scoped живёт «на сессию пользователя». В ASP.NET Core это ровно один HTTP-запрос, сессия тут ни при чём.