Ответ
Время жизни (lifetime) объекта в DI-контейнере ASP.NET Core определяется при регистрации сервиса и влияет на то, как часто создается новый экземпляр.
Существует три основных варианта:
- Transient (Временный) – новый экземпляр создается каждый раз, когда сервис запрашивается из контейнера. Подходит для легковесных, stateless-сервисов.
services.AddTransient<IMyService, MyService>(); - Scoped (В рамках области) – один экземпляр создается на одну область (scope). В веб-приложении область по умолчанию — это один HTTP-запрос. Это самый частый выбор для сервисов, работающих с контекстом запроса (например, DbContext в Entity Framework Core).
services.AddScoped<IMyRepository, MyRepository>(); - Singleton (Одиночка) – один экземпляр создается на все время работы приложения. Используется для сервисов, которые должны быть общими для всех запросов (например, кэш в памяти, логгер, конфигурация).
services.AddSingleton<IMemoryCache, MemoryCache>();
Критически важные моменты:
- Несоответствие времен жизни: Сервис с более коротким временем жизни не должен зависеть от сервиса с более долгим. Например,
Scopedсервис не может зависеть отTransientсервиса, если этотTransientсервис не был зарегистрирован правильно. - Особенности
Scopedвне веб-контекста: В консольных приложениях или фоновых задачах область видимости нужно создавать вручную черезIServiceScopeFactory.CreateScope(). - Потокобезопасность:
Singletonсервисы должны быть спроектированы как потокобезопасные. - Утечки памяти:
Singleton, который хранит ссылки наScopedсервисы или большие данные, может привести к утечке памяти.
Ответ 18+ 🔞
Давай разберем эту тему про время жизни сервисов, а то народ часто в неё втыкается, как слепой в ебучую витрину.
Смотри, в ASP.NET Core, когда ты регистрируешь свой сервис в контейнере, ты должен ему сказать: "Слушай, дружок, а как часто тебя создавать-то будем?". И тут у тебя три пути, и от выбора зависит, не словишь ли ты потом пиздец с утечками памяти или с тем, что данные одного пользователя другому показываются.
Вариант первый — Transient (Временный).
Это как одноразовые стаканчики. Каждый раз, когда кто-то просит этот сервис (через конструктор или GetService), контейнер хватает новый, чистенький экземпляр и выдает. Сделал своё дело — на свалку истории. Идеально для каких-нибудь легковесных утилиток, которые не хранят состояние.
services.AddTransient<IMyService, MyService>();
Запросил его десять раз в одном методе — получил десять разных объектов. Никакой связи между ними, чистая анонимность.
Вариант второй — Scoped (Область видимости).
А вот это уже поинтереснее, и тут чаще всего ебутся. В рамках ОДНОЙ ОБЛАСТИ (scope) — один и тот же экземпляр. В веб-приложении по умолчанию одна область — это один HTTP-запрос. Весь запрос крутится, и все, кто просит этот сервис, получают один и тот же объект. Это пиздец как удобно для DbContext в Entity Framework — все операции в рамках одного запроса используют одно подключение, одну транзакцию.
services.AddScoped<IMyRepository, MyRepository>();
Но запрос кончился — область умерла, и её сервисы тоже. Новый запрос — новая область, новые экземпляры. Красота.
Вариант третий — Singleton (Одиночка). Ну тут всё просто, как три копейки. Создали один раз при старте приложения — и он живёт, пока приложение не вырубят. Все запросы, все пользователи, все потоки тыкаются в один и тот же объект.
services.AddSingleton<IMemoryCache, MemoryCache>();
Идеально для кеша, логгера, конфигурации. Но есть нюанс, ёпта!
А теперь, блядь, самое важное, где все обжигаются:
-
Несоответствие времён жизни — дорога в ад. Представь, что у тебя
Singletonсервис зависит в конструкторе отScopedсервиса. Что получается?Singletonсоздаётся один раз при старте, хватает в свои объятия тотScopedсервис, который был в момент его создания (в какой-то левой области), и больше его не отпускает. А потом, когда придут реальные пользователи, они будут получать доступ к старому, кривому контексту базы данных от того первого запроса. Это пиздец, это крах, это ошибка при компиляции в новых версиях, и слава богу. Правило простое: сервис с большей продолжительностью жизни НЕ МОЖЕТ зависеть от сервиса с меньшей.Singletonне может зависеть отScopedилиTransient.Scopedне может зависеть отTransient(если только этотTransientне зарегистрирован как-то хитро). Иначе будет больно. -
Scoped вне веба — головная боль. В консольном приложении или в фоновой задаче (например, в
IHostedService) области видимости по умолчанию нет, её надо создавать руками. Забыл создать — получишь исключение, мол, не могу тебеScopedсервис выдать, потому что не знаю, где границы твоей области. СпасаетIServiceScopeFactory:using (var scope = scopeFactory.CreateScope()) { var service = scope.ServiceProvider.GetRequiredService<IMyScopedService>(); // Работаем тут } // Здесь область закроется, и сервисы умрут -
Потокобезопасность для Singleton — не пустой звук. Если твой синглтон имеет какое-то состояние (поле, список, словарь), и к нему могут обращаться параллельно десять потоков, то надо это состояние защищать (
lock,ConcurrentDictionaryи т.д.). Иначе будет классическая история: "Оно вроде работало, а потом данные поплыли, и я три дня искал, где же я накосячил". -
Утечка памяти через Singleton — тихий убийца. Если твой вечноживущий синглтон где-то в себе хранит ссылки на какие-то объекты (например, кеширует результаты, но никогда не чистит), или, что ещё хуже, подписывается на события от других сервисов и не отписывается, то память будет потихоньку утекать, как вода из дырявого таза. Приложение будет жрать оперативку, пока не накроется медным тазом. Надо следить за этим.
Короче, суть в чём: прежде чем тыкнуть AddSingleton, подумай — а точно он должен жить вечно? Чаще всего нужен Scoped. Transient — для чего-то совсем простого и без состояния. А синглтон — это ответственно, как атомная бомба, обращаться надо с умом.