Что такое Scoped-сервис в ASP.NET Core DI?

Ответ

Scoped — это одно из трёх основных времён жизни сервиса в системе внедрения зависимостей (DI) ASP.NET Core. Scoped-сервис создаётся один раз в рамках определённой области (scope) и используется повторно всеми компонентами внутри этой области.

Ключевое применение: В веб-приложении каждый HTTP-запрос автоматически создаёт новую область. Поэтому Scoped-сервис живёт ровно столько, сколько длится обработка одного запроса.

Регистрация:

// В Program.cs или Startup.ConfigureServices
services.AddScoped<IMyService, MyService>();
services.AddScoped<ApplicationDbContext>(); // Типичный пример для EF Core

Почему это важно? Практический пример с Entity Framework Core: Контекст БД (DbContext) по умолчанию регистрируется как Scoped. Это гарантирует, что:

  1. В рамках одного запроса все репозитории и сервисы работают с одним экземпляром DbContext.
  2. Отслеживание изменений (change tracking) работает корректно.
  3. В конце запроса контекст автоматически освобождается.
  4. Разные запросы не мешают друг другу, так как используют разные экземпляры.

Важные правила и ограничения:

  • Нельзя внедрять Scoped-сервис в Singleton-сервис. Это приведёт к тому, что Scoped-сервис будет захвачен Singleton'ом и станет вечно живущим, что вызовет проблемы (например, утечку памяти в DbContext).

    // НЕПРАВИЛЬНО:
    services.AddSingleton<IBadService>(provider => 
        new BadService(provider.GetRequiredService<IMyScopedService>())); // Scoped захвачен Singleton'ом
  • Для явного создания области (например, в фоновой задаче или консольном приложении) используйте IServiceScopeFactory.

    public class BackgroundWorker
    {
        private readonly IServiceScopeFactory _scopeFactory;
        public BackgroundWorker(IServiceScopeFactory scopeFactory) { _scopeFactory = scopeFactory; }
    
        public void DoWork()
        {
            using (var scope = _scopeFactory.CreateScope())
            {
                var scopedService = scope.ServiceProvider.GetRequiredService<IMyScopedService>();
                // Работаем с scopedService в изолированной области
            } // Здесь scopedService будет уничтожен
        }
    }

Другие времена жизни для сравнения:

  • Transient: Создаётся новый экземпляр при каждом запросе сервиса. Подходит для лёгких, stateless-сервисов.
  • Singleton: Создаётся один экземпляр на всё время работы приложения. Подходит для stateless-сервисов, кэшей, конфигураций.

Ответ 18+ 🔞

Давай разжую про Scoped, чтобы понятно стало, как этот зверь живёт. Представь себе, что твоё веб-приложение — это общепит, а DI-контейнер — суровый завскладом, который решает, кому и когда выдавать ингредиенты.

Scoped — это как порция салата оливье, которую готовят один раз на конкретный заказ (HTTP-запрос) и ставят на стол. Все, кто сидят за этим столом (контроллеры, сервисы, репозитории внутри одного запроса), едят из одной и той же миски. Как только гости ушли — миску в помойку, на следующий заказ будет новая.

Как его впихнуть в систему:

// Тыкаешь это где-то в Program.cs
services.AddScoped<IMyService, MyService>();
// Или вот классика — контекст БД:
services.AddScoped<ApplicationDbContext>(); // EF Core, ёпта

А нахуя это нужно? Ну смотри на примере Entity Framework Core. Контекст БД (DbContext) по умолчанию — Scoped. Это гениально, потому что:

  1. Весь твой запрос, от контроллера до последнего репозитория, долбит базу через один и тот же экземпляр DbContext.
  2. Отслеживание сущностей (change tracking) не ебёт мозг, всё согласовано.
  3. Как запрос отработал — контекст прибивается, память чиста.
  4. Параллельные запросы не дерутся за один контекст, у каждого своя изолированная песочница. Красота!

Но есть подводные ебли, которые надо знать:

  • НЕЛЬЗЯ тыкать Scoped-сервис в Singleton. Это пиздец. Singleton живёт вечно, он схватит твой Scoped-сервис (например, тот же DbContext) и будет таскать его с собой, как привязанную покойницу. Контекст начнёт копить отслеживаемые сущности, утечка памяти, гонки данных — всё, пизда приложению.

    // ТАК НЕ ДЕЛАТЬ, А ТО ВЫЛЬЕТСЯ В БОЛЬ:
    services.AddSingleton<IBadService>(provider => 
        new BadService(provider.GetRequiredService<IMyScopedService>())); // Scoped теперь в плену у Singleton
  • Если тебе вдруг надо создать область вручную (допустим, в фоновой хуйне или консольном приложении), бери в руки IServiceScopeFactory.

    public class BackgroundWorker
    {
        private readonly IServiceScopeFactory _scopeFactory;
        public BackgroundWorker(IServiceScopeFactory scopeFactory) { _scopeFactory = scopeFactory; }
    
        public void DoWork()
        {
            using (var scope = _scopeFactory.CreateScope())
            {
                // Достаём сервис из новой, свежеиспечённой области
                var scopedService = scope.ServiceProvider.GetRequiredService<IMyScopedService>();
                // Делаем что надо...
            } // Всё, scope кончился — сервисы внутри него отправились на свалку истории.
        }
    }

Чтобы было совсем ясно, глянь на других его братьев:

  • Transient: Как одноразовые стаканчики. Каждый раз, когда кто-то просит "дай сервис" — новый стаканчик. Лёгкие, без состояния.
  • Singleton: Как единственный самовар на весь ресторан. Создали один раз при старте и он стоит, греется, все из него пьют. Только для тех, кто не портится и не хранит состояние запроса.