Підрозділ 21.2
IServiceCollection — реєстрація та розпізнавання сервісів
21.2. IServiceCollection — реєстрація та розпізнавання сервісів У попередньому розділі ми побачили, що Generic Host бере на себе управління lifecycle і надає DI контейнер. Сам контейнер будується в два окремі к
21.2. IServiceCollection — реєстрація та розпізнавання сервісів
У попередньому розділі ми побачили, що Generic Host бере на себе управління lifecycle і надає DI-контейнер. Сам контейнер будується в два окремі кроки: спочатку реєстрація (IServiceCollection), потім розпізнавання (IServiceProvider). Це навмисне розділення: список зареєстрованих сервісів формується один раз при запуску, а потім контейнер стає незмінним — жоден код у runtime не може додати новий сервіс або змінити вже зареєстрований.
Розуміння того, що саме зберігається в колекції сервісів і як контейнер вирішує, який об'єкт повернути, — це ключ до ефективної роботи з будь-яким DI-фреймворком у .NET.
ServiceDescriptor: атомарна одиниця реєстрації
Кожна реєстрація в IServiceCollection — це один об'єкт ServiceDescriptor. Він містить три речі:
ServiceDescriptor
├── ServiceType : Type — який тип запитується (зазвичай інтерфейс)
├── ImplementationType / ImplementationFactory / ImplementationInstance
│ — як створити об'єкт
└── Lifetime : ServiceLifetime — Singleton | Scoped | TransientКоли контейнер отримує запит на IPatientRepository, він шукає ServiceDescriptor з ServiceType == typeof(IPatientRepository), дивиться на lifetime, і за допомогою ImplementationType або фабрики (ImplementationFactory) створює або повертає вже існуючий екземпляр.
Три способи реєстрації
Реальний IServiceCollection надає три перевантажені методи, що відповідають трьом ситуаціям:
Реєстрація через тип — найчастіша форма:
services.AddSingleton<IPatientRepository, SqlPatientRepository>();
services.AddScoped<AppointmentService>(); // ServiceType == ImplementationType
services.AddTransient<INotificationService, SmtpNotificationService>();Реєстрація через фабрику — коли потрібна додаткова логіка при створенні:
services.AddSingleton<IPatientRepository>(provider =>
{
var config = provider.GetRequiredService<IConfiguration>();
var connStr = config["Database:ConnectionString"];
return new SqlPatientRepository(connStr);
});Реєстрація через готовий екземпляр — тільки для Singleton (об'єкт уже існує):
var sharedCache = new MemoryCache();
services.AddSingleton<ICache>(sharedCache);Побудуємо власну IServiceCollection
Розберемо механізм реєстрації та розпізнавання детально — реалізуємо повноцінний варіант, що підтримує всі три типи реєстрації і правильно обробляє циклічні залежності:
Множинна реєстрація одного інтерфейсу
Важлива особливість: один інтерфейс можна зареєструвати кілька разів з різними реалізаціями. При запиті через GetService<T>() повертається остання реєстрація. При запиті через GetServices<T>() (або IEnumerable<T>) — всі реєстрації. Це використовується у патернах типу «ланцюжок обробників» або «список валідаторів»:
// Всі валідатори для AppointmentRequest
services.AddTransient<IAppointmentValidator, TimeSlotValidator>();
services.AddTransient<IAppointmentValidator, DoctorAvailabilityValidator>();
services.AddTransient<IAppointmentValidator, PatientInsuranceValidator>();
// У AppointmentService:
// IEnumerable<IAppointmentValidator> validators ← отримуємо всі триTryAdd: реєстрація «якщо ще не зареєстровано»
Методи TryAddSingleton, TryAddScoped, TryAddTransient реєструють сервіс тільки якщо такого ServiceType ще немає в колекції. Це корисно для бібліотек, що хочуть надати реалізацію «за замовчуванням», але не перезаписувати ту, що встановив користувач:
// У бібліотеці:
services.TryAddSingleton<IPatientRepository, DefaultPatientRepository>();
// Якщо користувач вже зареєстрував свій:
services.AddSingleton<IPatientRepository, CustomPatientRepository>();
// → DefaultPatientRepository ніколи не буде використано
Перевірка на валідність реєстрацій
Реальний ServiceProvider у .NET підтримує опцію ValidateOnBuild = true. При увімкненні вона перевіряє при BuildServiceProvider():
- чи не є якийсь зареєстрований сервіс невирішуваним (залежність не зареєстрована)
- чи немає «captive dependencies» (Singleton залежить від Scoped — помилка, бо Scoped живе коротше)
У Development-середовищі Generic Host автоматично вмикає ці перевірки. В Production — вимикає заради продуктивності.
Підсумок
IServiceCollection — це не просто список об'єктів. Це специфікація того, як система будується. Розділення реєстрації (IServiceCollection) і розпізнавання (IServiceProvider) — принципово важливе архітектурне рішення: воно гарантує, що склад системи визначається один раз на старті, а не довільно змінюється під час роботи. Це робить додаток передбачуваним, тестованим і правильно сконструйованим.
Антипатерн: Service Locator
Service Locator — це патерн, де клас отримує залежності не через конструктор, а самостійно запитуючи їх у контейнера (IServiceProvider) всередині своїх методів. Це антипатерн, що руйнує всі переваги DI.
// ПОГАНО: Service Locator — AppointmentService тягне IServiceProvider і сам витягує залежності
class AppointmentService
{
private readonly IServiceProvider _sp; // зберігаємо контейнер!
public AppointmentService(IServiceProvider sp) => _sp = sp;
public void Book(string patientId, string doctorId)
{
// Залежності прихованi — видно тільки зсередини методу
var repo = _sp.GetRequiredService<IPatientRepository>(); // ← Service Locator
var schedule = _sp.GetRequiredService<IDoctorSchedule>(); // ← Service Locator
var logger = _sp.GetRequiredService<ILogger>(); // ← Service Locator
// ... логіка
}
}
// ПРАВИЛЬНО: явні залежності через конструктор (Dependency Injection)
class AppointmentServiceCorrect
{
private readonly IPatientRepository _repo;
private readonly IDoctorSchedule _schedule;
private readonly ILogger _logger;
// Усі залежності видно у публічному API класу
public AppointmentServiceCorrect(
IPatientRepository repo,
IDoctorSchedule schedule,
ILogger logger)
{
_repo = repo;
_schedule = schedule;
_logger = logger;
}
public void Book(string patientId, string doctorId)
{
_logger.Log($"Записую {patientId} до {doctorId}");
// ...
}
}Чому Service Locator — антипатерн:
| Проблема | Service Locator | DI через конструктор |
|---|---|---|
| Залежності | Приховані в методах | Явні в конструкторі |
| Тестування | Потрібен реальний або складний mock IServiceProvider |
Достатньо mock-ів конкретних інтерфейсів |
| Помилки | Виникають в рантаймі (GetRequiredService кидає) |
Виявляються при старті (якщо ValidateOnBuild = true) |
| Принцип | Порушує DIP — клас знає про контейнер | Відповідає DIP — клас знає лише свої абстракції |
Коли IServiceProvider все ж доречний:
- У
Program.cs/Startup.csпри конфігурації — це root composition root, де контейнер і повинен бути присутній - У
IServiceScopeFactory.CreateScope()у фонових сервісах (Singleton, що потребує Scoped-сервісів) - У middleware або factory-методах, де тип сервісу визначається динамічно
У всіх інших місцях — ін'єктуйте через конструктор, а не через IServiceProvider.