В чем отличие по сроку жизни зависимостей: singleton, scoped и transient
Что проверяют
Понимаете ли вы, кто владеет объектом и когда он освобождается, а не только три определения. Следом почти всегда спрашивают про внедрение scoped-сервиса в singleton и про DbContext в фоновой задаче.
Как отвечать
Речь о встроенном контейнере Microsoft.Extensions.DependencyInjection в ASP.NET Core:
AddSingleton: один экземпляр на корневой провайдер, то есть на всё приложение. Живёт до остановки, к нему обращаются из многих потоков сразу, поэтому он обязан быть потокобезопасным. Подходит для кэша, конфигурации, клиентов без состояния запроса;AddScoped: один экземпляр наIServiceScope. ASP.NET Core создаёт скоуп на каждый HTTP-запрос, поэтому внутри запроса все получают один и тот же объект. Классический примерDbContext:AddDbContextрегистрирует его именно так. В Blazor Server скоуп живёт всё подключение пользователя, а не один запрос;AddTransient: новый экземпляр при каждом разрешении, даже внутри одного запроса. Для лёгких сервисов без состояния.
Про освобождение: контейнер сам вызывает Dispose() у созданных им объектов с IDisposable, в конце скоупа или при остановке приложения. Transient, полученный из корневого провайдера, удерживается до конца работы, это медленная утечка.
Вне запроса, например в BackgroundService, скоупа нет. Его создают вручную через IServiceScopeFactory.CreateScope() или CreateAsyncScope() и берут сервисы из него.
Где спотыкаются
Внедряют scoped сервис в singleton. Зависимость захватывается навсегда (captive dependency): один DbContext на всё приложение, общий между потоками. В среде Development включена проверка скоупов, и контейнер бросит исключение сразу, а в продакшене без неё ошибка проявится только под нагрузкой.
Считают transient всегда безопасным выбором. Одноразовые transient-объекты, созданные из корня, копятся в памяти до остановки приложения.
Хранят состояние пользователя в singleton-сервисе. При параллельных запросах данные одного пользователя попадают другому.