В чём разница между жизненными циклами зависимостей (Transient, Scoped, Singleton) в ASP.NET Core DI?

«В чём разница между жизненными циклами зависимостей (Transient, Scoped, Singleton) в ASP.NET Core DI?» — вопрос из категории ASP.NET Core, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Время жизни сервиса определяет, как долго контейнер Dependency Injection будет хранить и повторно использовать его экземпляр. Правильный выбор критичен для архитектуры приложения.

1. Transient (Мгновенный)

  • Создание: Новый экземпляр при каждом запросе сервиса, даже в рамках одного вызова метода.
  • Аналогия: Как new MyService() каждый раз.
  • Когда использовать:
    • Легковесные сервисы.
    • Сервисы со внутренним состоянием (stateful), которое не должно разделяться (например, обработчик команды).
    • Сервисы, создание которых дёшево.
  • Предостережение: Регистрация «тяжёлого» сервиса (например, с сетевыми вызовами в конструкторе) как Transient может убить производительность.

2. Scoped (В рамках области)

  • Создание: Один экземпляр в рамках области видимости (scope). В веб-приложении область — это HTTP-запрос.
  • Аналогия: Один экземпляр на одну бизнес-транзакцию.
  • Когда использовать:
    • Классический пример — DbContext Entity Framework Core. Это гарантирует, что все репозитории в рамках одного запроса работают с одним контекстом, отслеживают одни сущности и фиксируют изменения атомарно.
    • Сервисы, требующие контекста запроса (например, информация о текущем пользователе).
  • Как создать область вручную (например, в фоновой задаче):
    using (var scope = serviceProvider.CreateScope())
    {
        var scopedService = scope.ServiceProvider.GetRequiredService<IMyScopedService>();
        // Работаем с scopedService
    } // Здесь scopedService будет уничтожен, если реализует IDisposable

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

  • Создание: Один экземпляр на всё время жизни приложения (точнее, корневого контейнера).
  • Когда использовать:
    • Статусные (stateless), потокобезопасные сервисы.
    • Сервисы-кэши (IMemoryCache).
    • Клиенты для внешних API (например, HttpClient, зарегистрированный правильно через IHttpClientFactory).
    • Конфигурация приложения.
  • Критически важное правило:
    • Не инжектируйте Scoped-сервис в Singleton! Это антипаттерн Captive Dependency. Singleton, созданный один раз, «захватит» Scoped-сервис, и тот никогда не будет освобождён, что ведёт к утечкам памяти (например, соединения с БД не закрываются). Контейнер в ASP.NET Core выбрасывает исключение при такой попытке.

Вывод: Выбор времени жизни — это компромисс между производительностью (создание vs. повторное использование), потреблением памяти и корректностью работы с состоянием.