В чём разница между Transient, Scoped и Singleton в ASP.NET Core Dependency Injection?

Ответ

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

1. Transient (AddTransient)

  • Поведение: Создаётся новый экземпляр сервиса каждый раз, когда он запрашивается из контейнера.
  • Использование: Для лёгких, статусных (stateful) или дорогих в создании сервисов, которые не должны разделяться.
  • Пример:
    services.AddTransient<IMyService, MyService>();
    // В контроллере дважды запрошен IMyService:
    var service1 = scope.ServiceProvider.GetService<IMyService>(); // Новый экземпляр
    var service2 = scope.ServiceProvider.GetService<IMyService>(); // Ещё один новый экземпляр

2. Scoped (AddScoped)

  • Поведение: Создаётся один экземпляр на область видимости (scope). В веб-приложении область видимости по умолчанию соответствует одному HTTP-запросу.
  • Использование: Идеально для сервисов, которым необходим контекст в рамках одной операции (например, DbContext Entity Framework Core).
  • Пример:
    services.AddScoped<IMyRepository, MyRepository>();
    // В рамках одного запроса несколько классов получат один и тот же экземпляр репозитория.

3. Singleton (AddSingleton)

  • Поведение: Создаётся один экземпляр на всё время жизни приложения (с момента первого разрешения).
  • Использование: Для статусных (stateless), потокобезопасных сервисов, таких как кэш в памяти, логгеры или клиенты для внешних API.
  • Пример:
    services.AddSingleton<IMemoryCache, MemoryCache>();

Критические правила и best practices:

  • Потокобезопасность: Singleton-сервисы должны быть потокобезопасными, так как к ним возможен одновременный доступ из множества потоков.
  • Зависимости (Captive Dependency): Никогда не инжектируйте Scoped-сервис в Singleton. Singleton «захватит» Scoped-сервис, и тот будет жить вечно, что может привести к утечке памяти (например, DbContext не будет освобождать соединения).
  • Производительность: Transient для тяжёлых объектов может создать нагрузку, а Singleton для объектов с состоянием — ошибки.

Ответ 18+ 🔞

Слушай, давай разберём эту тему про времена жизни сервисов в ASP.NET Core, а то народ часто путается и потом приложение ведёт себя как пьяный ёжик в тумане.

Вот есть три основных варианта, как сервис может жить. Выбираешь неправильно — получаешь либо утечку памяти, либо баги, либо производительность на уровне улитки в смоле.

1. Transient (Временный, AddTransient)

  • Как работает: Каждый раз, когда ты просишь этот сервис из контейнера, тебе выдают новый, свежеиспечённый экземпляр. Без раздумий, просто плодят их как кроликов.
  • Когда юзать: Для сервисов, которые лёгкие, но с состоянием, или которые создавать — недорого. Либо наоборот, для таких тяжёлых, которые делить между клиентами нельзя — чтобы каждый тащил своё бремя.
  • Пример в коде:
    services.AddTransient<IMyService, MyService>();
    // Два вызова подряд:
    var service1 = scope.ServiceProvider.GetService<IMyService>(); // Новый!
    var service2 = scope.ServiceProvider.GetService<IMyService>(); // И снова новый! Это два разных объекта, Карл!

2. Scoped (С областью видимости, AddScoped)

  • Как работает: Здесь уже умнее. Один экземпляр создаётся на одну "область" (scope). В веб-приложении по умолчанию одна область — это один HTTP-запрос. Всё, что в рамках одного запроса, — делит один экземпляр. Запрос кончился — экземпляр на свалку.
  • Когда юзать: Идеально для вещей, которые завязаны на контекст операции. Классика — DbContext из Entity Framework Core. Чтобы все репозитории в одном запросе работали с одной сессией БД и не устраивали бардак.
  • Пример в коде:
    
    services.AddScoped<IMyRepository, MyRepository>();
    // Два разных класса в одном запросе получат ссылку на один и тот же объект репозитория. Как братья-близнецы, только без драк.

3. Singleton (Одиночка, AddSingleton)

  • Как работает: Самый простой и опасный. Один экземпляр на всё время работы приложения. Создали при старте — и он живёт, пока приложение не убьют. Все запросы, все потоки — лезут к одной и той же сущности.
  • Когда юзать: Только для сервисов без состояния (stateless), которые к тому же потокобезопасны. Типичные кандидаты: кэш в памяти, логгер, какой-нибудь клиент для внешнего API, который умеет работать в многопоточке.
  • Пример в коде:
    services.AddSingleton<IMemoryCache, MemoryCache>();

А теперь, блядь, главные правила, которые нарушать — себе дороже:

  • Потокобезопасность для Singleton — это закон. Если твой синглтон не умеет работать, когда два потока одновременно в него тычутся, — будет пиздец, тихий и неприметный, пока продакшн не ляжет. Проверяй это в первую очередь.
  • Никогда не втыкай Scoped-сервис в Singleton! Это грех смертный, называется "Captive Dependency". Синглтон живёт вечно, он схватит скопед-сервис при своём рождении и будет держать его до скончания времён. Если это был DbContext, то соединение с базой так и не закроется, и память будет течь, как решето. В рот меня чих-пых, не делай так!
  • С производительностью думай. Transient для объекта, который весит как чугунный мост, — убьёт перфоманс, потому что его будут создавать каждую секунду. Singleton для объекта с состоянием — гарантированная порча данных между пользователями. Думай, что ты делаешь, а не тыкай наугад.

Вот и вся магия. Выбирай с умом, и приложение будет летать, а не ползать, как рак по шерсти.