Ответ
В ASP.NET Core Dependency Injection (DI) время жизни сервиса определяет, как долго контейнер будет хранить его экземпляр. Правильный выбор критичен для производительности и корректности работы приложения.
Три основных времени жизни:
-
Transient – создается новый экземпляр для каждого запроса к контейнеру.
- Используйте для: легковесных, stateless сервисов.
- Проблема: может создать нагрузку, если сервис тяжелый и запрашивается часто.
-
Scoped – один экземпляр создается на область видимости (scope), например, на один HTTP-запрос.
- Используйте для: сервисов, которым нужен контекст в рамках операции (например,
DbContextв Entity Framework Core). - Проблема: попытка разрешить Scoped-сервис из Singleton или вне области видимости вызовет исключение.
- Используйте для: сервисов, которым нужен контекст в рамках операции (например,
-
Singleton – один экземпляр на все время работы приложения.
- Используйте для: stateless сервисов, кэшей, конфигураций.
- Проблема: если Singleton зависит от Scoped-сервиса, этот Scoped-сервис неявно становится Singleton, что может привести к утечке памяти или некорректным данным (например, один
DbContextна все запросы).
Пример проблемы и решения:
// ПРОБЛЕМА: Singleton, зависящий от Scoped-сервиса.
public class CacheService
{
private readonly AppDbContext _dbContext; // Scoped-сервис!
public CacheService(AppDbContext dbContext) => _dbContext = dbContext;
// _dbContext будет захвачен и жить вечно, вызывая утечку.
}
// РЕШЕНИЕ: Внедрять IServiceScopeFactory и создавать область видимости явно.
public class CacheService
{
private readonly IServiceScopeFactory _scopeFactory;
public CacheService(IServiceScopeFactory scopeFactory) => _scopeFactory = scopeFactory;
public async Task<string> GetCachedDataAsync()
{
using (var scope = _scopeFactory.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// Работаем с dbContext в его собственной области видимости.
return await dbContext.Data.FindAsync(1);
}
}
}
Ключевые правила:
- Scoped-сервисы не должны быть разрешены из Singleton-сервисов напрямую.
- Transient-сервисы не должны внедряться в Singleton-сервисы, если они не предназначены для этого, так как тоже станут де-факто Singleton.
- Всегда проверяйте граф зависимостей на предмет captive dependencies (захвата зависимостей с более коротким временем жизни).
Ответ 18+ 🔞
Слушай, а вот эта вся хуйня с временем жизни сервисов в ASP.NET Core — это ж надо понимать, а то приложение накроется медным тазом, причём в самый неподходящий момент, когда уже продакшен горит!
Короче, есть три главных способа зарегистрировать твой сервис, и от выбора зависит, не словишь ли ты утечку памяти на гигабайты или исключение в лоб в пятницу вечером.
Первое — Transient (он же временный, одноразовый). Каждый раз, когда кто-то просит этот сервис, контейнер лезет в карман и выдаёт новый, свеженький экземпляр. Как носок — надел, выбросил. Идеально для всякой лёгкой, безгосударственной хуйни, которая не помнит, что было вчера. Но если твой сервис — это монстр, который жрёт ресурсы как не в себя, и его дергают сто раз в секунду, то приложение просто ляжет, потому что создавать такие тяжеловесы каждый раз — это пиздец какой расход.
Второе — Scoped (область видимости, он же "на один запрос").
Вот это уже умнее. На время одного HTTP-запроса (или любой другой созданной тобой области) контейнер выдаёт один и тот же экземпляр. Все, кто в рамках этого запроса его просят, получают одну и ту же ссылку. Классика — твой DbContext из Entity Framework. Ему же нужно отслеживать изменения в рамках одной операции, а не разбрасываться новыми инстансами как дурак. Но тут главное не обосраться: если попробуешь достать Scoped-сервис из Singleton'а или просто из кода, где нет активной области (scope), то получишь исключение прямо в ебало. Контейнер тебе так и скажет: "Чувак, ты че, охуел? Где scope, блядь?".
Третье — Singleton (он же один на всех, он же "царь и бог"). Создаётся один раз при старте приложения и живёт, пока все не вырубят. Все запросы, все потоки — все тычут пальцем в один и тот же объект. Идеально для кэшей, конфигураций, каких-то статических менеджеров. Но вот тут-то и начинается самая интересная пиздобратия.
Главная засада (она же captive dependency):
Если твой жирный, вечный Singleton возьмёт и внедрит в себя зависимость, которая по жизни Scoped (например, тот же DbContext), то контейнер, создавая Singleton один раз, один раз же и выдаст ему этот DbContext. И этот бедный контекст будет жить вечно, как пленник, таская за собой все подключения и отслеженные сущности, пока память не кончится. Это и есть утечка, ёпта. Или ещё хуже — данные от разных пользователей в одном контексте перемешаются, будет полный пиздец.
Как это выглядит на практике и как не наступить на грабли:
// ПРОБЛЕМА: Синглтон-кэш, который схватил скопленный DbContext и не отпускает.
public class CacheService
{
private readonly AppDbContext _dbContext; // А этот ублюдок по жизни Scoped!
public CacheService(AppDbContext dbContext) => _dbContext = dbContext;
// Контейнер выдал DbContext при создании CacheService, и теперь они умрут вместе.
// DbContext превратился в неявный Singleton. Пиздец, утечка.
}
// РЕШЕНИЕ: Не быть жадным, создавать свою область видимости когда надо.
public class CacheService
{
private readonly IServiceScopeFactory _scopeFactory; // Эта штука всегда Singleton
public CacheService(IServiceScopeFactory scopeFactory) => _scopeFactory = scopeFactory;
public async Task<string> GetCachedDataAsync()
{
// Создаём свою, новую, чистенькую область видимости.
using (var scope = _scopeFactory.CreateScope())
{
// Достаём из ЭТОЙ области свой собственный, свежий DbContext.
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// Поработали с ним...
return await dbContext.Data.FindAsync(1);
// ...а здесь using закроется, scope умрёт, и DbContext благополучно будет уничтожен.
// Никаких пленников, никаких утечек. Красота.
}
}
}
Итоговые правила, которые надо выжечь на подкорке:
- Не суй Scoped-сервисы напрямую в конструктор Singleton'ов. Это как пригласить гостя на одну ночь, а он поселится у тебя навсегда и будет жрать твой холодильник.
- Transient-сервисы, внедрённые в Singleton, тоже становятся его пленниками. Они создаются один раз и живут столько же, сколько и их хозяин. Убедись, что они для этого предназначены.
- Всегда смотри на граф зависимостей. Кто от кого зависит? Не захватывает ли долгоживущий сервис какую-нибудь короткоживущую зависимость? Это и есть проверка на captive dependencies — главную причину ебанины в продакшене.
Короче, думай головой, когда регистрируешь сервисы, а не тыкай AddSingleton на всё подряд. Иначе будет тебе и хиросима, и нагасаки в одном флаконе.