Какие типы жизненного цикла сервисов существуют в ASP.NET Core?

Ответ

В ASP.NET Core Dependency Injection (DI) контейнер управляет временем жизни сервисов. Существует три основных типа (Lifetime), которые определяют, как и когда создается экземпляр сервиса:

Тип Время жизни Регистрация Типичное использование
Transient Создается каждый раз при запросе из контейнера. AddTransient<TService, TImplementation>() Легковесные, статусные сервисы (например, Random, IStringLocalizer). Не должны хранить состояние между вызовами.
Scoped Создается один раз на область видимости (scope). Для веб-приложения — один на HTTP-запрос. AddScoped<TService, TImplementation>() Сервисы, работающие в контексте одного запроса: DbContext (Entity Framework Core), репозитории, сервисы бизнес-логики.
Singleton Создается один раз на все время работы приложения. AddSingleton<TService, TImplementation>() Сервисы с общим состоянием или дорогие в создании: кэши (IMemoryCache), клиенты (HttpClient с IHttpClientFactory), конфигурация.

Критические моменты и best practices:

  1. Зависимости между сервисами с разным временем жизни:

    • Singleton НИКОГДА не должен зависеть от Scoped или Transient сервисов. Это приведет к captive dependency — Scoped-сервис, захваченный Singleton-ом, будет жить вечно, что может вызвать утечку памяти (например, DbContext будет кэшировать данные и не освобождать соединения).
      // НЕПРАВИЛЬНО: Singleton зависит от Scoped.
      public class BadCacheService
      {
      private readonly AppDbContext _context; // Scoped!
      public BadCacheService(AppDbContext context) => _context = context; // Ошибка!
      }
  2. Работа с Scoped сервисами вне HTTP-запроса (в фоновых задачах, через IHostedService):

    • Необходимо вручную создать область видимости с помощью IServiceScopeFactory.

      public class BackgroundWorker : IHostedService
      {
      private readonly IServiceScopeFactory _scopeFactory;
      public BackgroundWorker(IServiceScopeFactory scopeFactory) => _scopeFactory = scopeFactory;
      
      public async Task DoWorkAsync()
      {
          // Создаем новую область видимости
          using (var scope = _scopeFactory.CreateScope())
          {
              // Получаем Scoped-сервис из новой области
              var scopedService = scope.ServiceProvider.GetRequiredService<IMyScopedService>();
              await scopedService.ProcessDataAsync();
          } // Здесь scoped-сервисы будут корректно удалены.
      }
      }

Как выбрать? Всегда начинайте с Scoped для сервисов с логикой запроса. Используйте Transient для простых, не имеющих состояния утилит. Singleton — только для сервисов с общим состоянием, доступ к которому должен быть синхронизирован.

Ответ 18+ 🔞

А, слушай, про эту вашу инъекцию зависимостей в ASP.NET Core! Ну, там же три главных типа, как они живут, да? Это ж надо понимать, а то потом такие косяки будут, что охуеешь.

Вот смотри, есть три штуки:

Transient — это как одноразовый стаканчик. Каждый раз, когда кто-то просит этот сервис, контейнер тебе новый, свеженький делает. Зарегистрировать — AddTransient. Юзать это для всякой легкой хуйни, которая не должна состояние хранить. Ну, типа Random там, или локализатор какой. Сделал дело — выкинул, похуй.

Scoped — вот это уже поинтереснее. Создается один раз на область видимости. В веб-приложении — один раз на HTTP-запрос. Все в рамках одного запроса юзают один и тот же экземпляр. Регистрация — AddScoped. Это для всего, что завязано на контекст запроса: твой DbContext из Entity Framework, репозитории, сервисы бизнес-логики. Удобно, логично.

Singleton — это уже монументальная хуйня. Создается один раз на всё время работы приложения и живёт, пока приложение не сдохнет. AddSingleton. Это для сервисов с общим состоянием или которые овердохуища ресурсов жрут при создании: кэши (IMemoryCache), клиенты (через фабрику), конфигурация какая-нибудь глобальная.

А теперь, внимание, ебушки-воробушки, главные грабли, на которые все наступают:

  1. Singleton НИ В КОЕМ СЛУЧАЕ не должен зависеть от Scoped или Transient сервисов. Вообще, блядь, никогда! Это называется captive dependency — зависимость в плену. Представь: у тебя Singleton-сервис, как царь и бог, живёт вечно. А ты в него засунул Scoped-сервис, который по задумке должен умирать после каждого запроса. И что? А него нихуя не умрёт! Он будет висеть в памяти, как проклятый. Особенно весело с DbContext — он кэшировать данные начнёт, соединения не будет отпускать, и в итоге у тебя приложение ебнет по памяти. Пиздец и разбор полётов.

    // НЕ, НЕ, НЕ! СОВСЕМ НЕ ТАК! Singleton, зависящий от Scoped — это билет в ад.
    public class BadCacheService
    {
        private readonly AppDbContext _context; // Scoped, Карл!
        public BadCacheService(AppDbContext context) => _context = context; // Вот тут и начинается пиздец.
    }
  2. А если надо Scoped сервис юзать вне HTTP-запроса? Ну, в фоновой задаче, в каком-нибудь IHostedService. Там же запроса нет, scope-а нет. И что делать? А вот так: руками создавать область видимости через IServiceScopeFactory. Смотри, как гениально просто:

    public class BackgroundWorker : IHostedService
    {
        private readonly IServiceScopeFactory _scopeFactory; // Берём фабрику
        public BackgroundWorker(IServiceScopeFactory scopeFactory) => _scopeFactory = scopeFactory;
    
        public async Task DoWorkAsync()
        {
            // Создаём новую, чистенькую область видимости
            using (var scope = _scopeFactory.CreateScope())
            {
                // Достаём из неё Scoped-сервис
                var scopedService = scope.ServiceProvider.GetRequiredService<IMyScopedService>();
                await scopedService.ProcessDataAsync();
            } // Всё! Здесь область закроется, и все Scoped-сервисы внутри неё корректно почистятся. Красота.
        }
    }

Так как же выбирать-то, спросишь ты? Да всё просто, чувак.

  • Scoped — это твой дефолтный, рабочий вариант для 90% сервисов, где есть какая-то логика, связанная с запросом или транзакцией. Начинай думать с него.
  • Transient — для простых, безголовых утилиток, которые не хранят состояния между вызовами. Сделал — выбросил.
  • Singleton — только для реально глобальных, общих штук, доступ к которым нужно как-то синхронизировать. Или которые так дороги в создании, что проще один раз создать и забыть. Но без фанатизма, а то опять captive dependency получишь, и волосы на жопе вылезут.