Какие проблемы решает использование времени жизни (lifetime) в ASP.NET Core?

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

Ответ

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

Три основных времени жизни:

  1. Transient – создается новый экземпляр для каждого запроса к контейнеру.

    • Используйте для: легковесных, stateless сервисов.
    • Проблема: может создать нагрузку, если сервис тяжелый и запрашивается часто.
  2. Scoped – один экземпляр создается на область видимости (scope), например, на один HTTP-запрос.

    • Используйте для: сервисов, которым нужен контекст в рамках операции (например, DbContext в Entity Framework Core).
    • Проблема: попытка разрешить Scoped-сервис из Singleton или вне области видимости вызовет исключение.
  3. Singleton – один экземпляр на все время работы приложения.

    • Используйте для: stateless сервисов, кэшей, конфигураций.
    • Проблема: если Singleton зависит от Scoped-сервиса, этот Scoped-сервис неявно становится Singleton, что может привести к утечке памяти или некорректным данным (например, один DbContext на все запросы).

Пример проблемы и решения:

// ПРОБЛЕМА: Singleton, зависящий от Scoped-сервиса.
public class CacheService
{
    private readonly AppDbContext _dbContext; // Scoped-сервис!
    public CacheService(AppDbContext dbContext) => _dbContext = dbContext;
    // _dbContext будет захвачен и жить вечно, вызывая утечку.
}

// РЕШЕНИЕ: Внедрять IServiceScopeFactory и создавать область видимости явно.
public class CacheService
{
    private readonly IServiceScopeFactory _scopeFactory;
    public CacheService(IServiceScopeFactory scopeFactory) => _scopeFactory = scopeFactory;

    public async Task<string> GetCachedDataAsync()
    {
        using (var scope = _scopeFactory.CreateScope())
        {
            var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
            // Работаем с dbContext в его собственной области видимости.
            return await dbContext.Data.FindAsync(1);
        }
    }
}

Ключевые правила:

  • Scoped-сервисы не должны быть разрешены из Singleton-сервисов напрямую.
  • Transient-сервисы не должны внедряться в Singleton-сервисы, если они не предназначены для этого, так как тоже станут де-факто Singleton.
  • Всегда проверяйте граф зависимостей на предмет captive dependencies (захвата зависимостей с более коротким временем жизни).