Captive dependency и другие ловушки DI
↑ 3.2 Dependency Injection · ← Предыдущая · → Следующая
Зачем это на собесе
«Найдите баг в регистрации» — типичная задача на ревью кода. 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.
Практика
- Воспроизведите captive dependency и покажите, как её ловит
ValidateScopes. - Перепишите singleton, используя
IDbContextFactory. - Включите
ValidateOnBuildна своём проекте.
Вопросы с ответами
Что такое captive dependency?
Ситуация, когда сервис с длинным временем жизни удерживает сервис с коротким, из-за чего тот живёт дольше, чем должен.
Как безопасно использовать scoped в singleton?
Через
IServiceScopeFactory(создать scope на операцию) илиIDbContextFactoryдля EF.
Почему Service Locator — антипаттерн?
Зависимости неявны, тесты и анализ кода усложняются, ошибки регистрации проявляются только в рантайме.