OOP Course
Сьогодні

Підрозділ 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 ніколи не буде використано

Реєстрація та розпізнавання сервісів у IServiceCollection

Перевірка на валідність реєстрацій

Реальний 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.

Розроблено Tomka Yurii · © 2026 ·