Captive dependency и другие ловушки DI

↑ 3.2 Dependency Injection · ← Предыдущая · → Следующая

Обязательно~3 мин чтенияУровень: middle

Зачем это на собесе

«Найдите баг в регистрации» — типичная задача на ревью кода. Captive dependency — самая известная ловушка.

Объяснение

Captive dependency — долгоживущий сервис держит ссылку на короткоживущий, продлевая ему жизнь.

builder.Services.AddSingleton<ReportCache>();   // singleton
builder.Services.AddScoped<AppDbContext>();      // scoped
 
public class ReportCache(AppDbContext db) { }   // db «застрял» в singleton: один DbContext на всё приложение, гонки и устаревший кэш

Исправление: зависеть от IServiceScopeFactory / IDbContextFactory<T> и создавать scope на операцию.

public class ReportCache(IServiceScopeFactory scopes)
{
    public async Task<Report> LoadAsync(int id)
    {
        using var scope = scopes.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        return await db.Reports.FindAsync(id);
    }
}

Другие ловушки

ЛовушкаПоследствиеКак избежать
Service Locatorскрытые зависимости, сложные тестывнедрять через конструктор
Transient IDisposable в корневом контейнереутечка памятиScoped или создавать вручную
Блокирующая логика в конструкторемедленный запрос, deadlockвыносить в InitializeAsync
Циклические зависимостиисключениевыделить третий сервис или события
Разрешение из корня scoped-сервисаисключение с включённой валидациейиспользовать scope
Тяжёлый граф на запрослишние аллокациипересмотреть время жизни
new внутри сервиса вместо DIнельзя заменить в тестахвнедрить зависимость

Инструменты

ServiceProviderOptions { ValidateScopes = true, ValidateOnBuild = true } находит captive dependency и незарегистрированные зависимости при старте.

Нюансы и подводные камни

  • Валидация областей включена по умолчанию только в Development.
  • Async-инициализация в конструкторе невозможна — используйте фабричные методы или hosted-service «прогрев».
  • Registered IEnumerable<T> порядок зависит от порядка регистрации; последняя регистрация выигрывает при запросе одиночного T.

Практика

  1. Воспроизведите captive dependency и покажите, как её ловит ValidateScopes.
  2. Перепишите singleton, используя IDbContextFactory.
  3. Включите ValidateOnBuild на своём проекте.

Вопросы с ответами

Связанные темы