Каково время жизни объекта при его добавлении в DI-контейнер в ASP.NET Core?

Ответ

Время жизни (lifetime) объекта в DI-контейнере ASP.NET Core определяется при регистрации сервиса и влияет на то, как часто создается новый экземпляр.

Существует три основных варианта:

  1. Transient (Временный) – новый экземпляр создается каждый раз, когда сервис запрашивается из контейнера. Подходит для легковесных, stateless-сервисов.
    services.AddTransient<IMyService, MyService>();
  2. Scoped (В рамках области) – один экземпляр создается на одну область (scope). В веб-приложении область по умолчанию — это один HTTP-запрос. Это самый частый выбор для сервисов, работающих с контекстом запроса (например, DbContext в Entity Framework Core).
    services.AddScoped<IMyRepository, MyRepository>();
  3. 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>();

Идеально для кеша, логгера, конфигурации. Но есть нюанс, ёпта!


А теперь, блядь, самое важное, где все обжигаются:

  1. Несоответствие времён жизни — дорога в ад. Представь, что у тебя Singleton сервис зависит в конструкторе от Scoped сервиса. Что получается? Singleton создаётся один раз при старте, хватает в свои объятия тот Scoped сервис, который был в момент его создания (в какой-то левой области), и больше его не отпускает. А потом, когда придут реальные пользователи, они будут получать доступ к старому, кривому контексту базы данных от того первого запроса. Это пиздец, это крах, это ошибка при компиляции в новых версиях, и слава богу. Правило простое: сервис с большей продолжительностью жизни НЕ МОЖЕТ зависеть от сервиса с меньшей. Singleton не может зависеть от Scoped или Transient. Scoped не может зависеть от Transient (если только этот Transient не зарегистрирован как-то хитро). Иначе будет больно.

  2. Scoped вне веба — головная боль. В консольном приложении или в фоновой задаче (например, в IHostedService) области видимости по умолчанию нет, её надо создавать руками. Забыл создать — получишь исключение, мол, не могу тебе Scoped сервис выдать, потому что не знаю, где границы твоей области. Спасает IServiceScopeFactory:

    using (var scope = scopeFactory.CreateScope())
    {
        var service = scope.ServiceProvider.GetRequiredService<IMyScopedService>();
        // Работаем тут
    } // Здесь область закроется, и сервисы умрут
  3. Потокобезопасность для Singleton — не пустой звук. Если твой синглтон имеет какое-то состояние (поле, список, словарь), и к нему могут обращаться параллельно десять потоков, то надо это состояние защищать (lock, ConcurrentDictionary и т.д.). Иначе будет классическая история: "Оно вроде работало, а потом данные поплыли, и я три дня искал, где же я накосячил".

  4. Утечка памяти через Singleton — тихий убийца. Если твой вечноживущий синглтон где-то в себе хранит ссылки на какие-то объекты (например, кеширует результаты, но никогда не чистит), или, что ещё хуже, подписывается на события от других сервисов и не отписывается, то память будет потихоньку утекать, как вода из дырявого таза. Приложение будет жрать оперативку, пока не накроется медным тазом. Надо следить за этим.

Короче, суть в чём: прежде чем тыкнуть AddSingleton, подумай — а точно он должен жить вечно? Чаще всего нужен Scoped. Transient — для чего-то совсем простого и без состояния. А синглтон — это ответственно, как атомная бомба, обращаться надо с умом.