OOP Course
Сьогодні

Підрозділ 21.1

Generic Host — архітектура та життєвий цикл додатку

21.1. Generic Host — архітектура та життєвий цикл додатку Будь який реальний додаток потребує більше, ніж просто виконання рядків коду зверху вниз. Йому потрібно запуститися, налаштуватись, підключитись до баз

21.1. Generic Host — архітектура та життєвий цикл додатку

Будь-який реальний додаток потребує більше, ніж просто виконання рядків коду зверху вниз. Йому потрібно запуститися, налаштуватись, підключитись до баз даних, логерів, черг повідомлень, дочекатись сигналу зупинки і правильно завершити роботу — зберегти стан, закрити з'єднання, завершити незакінчені операції. Якщо все це реалізовувати вручну у кожному проекті, код Main() перетвориться на величезний монолітний метод, а будь-яка зміна в порядку ініціалізації ризикує зламати всю систему.

Саме для вирішення цієї проблеми Microsoft у .NET Core 2.1 (2018) ввів концепцію Generic Host (Microsoft.Extensions.Hosting). Generic Host — це інфраструктурна оболонка, що бере на себе відповідальність за управління життєвим циклом додатку: запуск, виконання фонових сервісів, обробку сигналів завершення і коректний shutdown. До цього аналогічний механізм існував лише для ASP.NET Core (WebHost), але з появою Generic Host він став доступним для будь-якого типу додатків — консольних, Windows Services, worker services, мікросервісів.

Проблема: «розсипаний» Main без хоста

Розглянемо типову еволюцію консольного додатку для клінічної системи. Спочатку все здається простим:

Цей код має щонайменше п'ять структурних проблем. Жодна з них не критична сама по собі — але разом вони перетворюють додаток на крихку конструкцію, яку важко розширювати й майже неможливо тестувати.

Що таке Generic Host

Generic Host — це об'єкт типу IHost, що виконує три ролі одночасно:

1. Service Container (DI-контейнер) — зберігає реєстрацію всіх сервісів і відповідає за їхнє створення та управління їхнім часом життя. Замість того, щоб кожен клас сам створював свої залежності через new, він отримує їх через конструктор — а контейнер знає, як їх побудувати.

2. Configuration Provider — агрегує конфігурацію з різних джерел: appsettings.json, змінні середовища, аргументи командного рядка, секрети — і надає єдиний уніфікований доступ через IConfiguration.

3. Lifecycle Manager — відстежує стан додатку, запускає фонові сервіси (IHostedService), перехоплює системні сигнали (SIGTERM, Ctrl+C) і гарантує коректний порядок зупинки компонентів.

┌─────────────────────────── IHost ────────────────────────────────┐
│                                                                   │
│   ┌─────────────────┐   ┌──────────────────┐   ┌─────────────┐  │
│   │ IServiceProvider│   │ IConfiguration   │   │  Lifecycle  │  │
│   │ (DI Container)  │   │ (Config Sources) │   │  Manager    │  │
│   └─────────────────┘   └──────────────────┘   └─────────────┘  │
│          │                       │                     │          │
│    реєстрація             appsettings.json        Start / Stop   │
│    та resolve             env variables          IHostedService   │
│    сервісів               cmd args               CancellationToken│
└─────────────────────────────────────────────────────────────────┘

Архітектура: Builder Pattern для хоста

Generic Host будується за Builder Pattern: спочатку конфігурується IHostBuilder, потім викликається .Build() і отримується IHost. Реальний API виглядає так:

// Реальний код з Microsoft.Extensions.Hosting (не runnable в браузері)
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;

IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        // Реєструємо сервіси
        services.AddSingleton<DatabaseConnection>();
        services.AddScoped<AppointmentService>();
        services.AddTransient<ReportService>();
    })
    .Build();

await host.RunAsync();

Host.CreateDefaultBuilder() — це фабричний метод, що автоматично налаштовує:

  • завантаження appsettings.json та appsettings.{Environment}.json
  • читання змінних середовища: з префіксом DOTNET_ — для конфігурації самого хоста (середовище, кореневий каталог), а всі змінні середовища без префікса — для конфігурації застосунку (IConfiguration)
  • User Secrets — лише в середовищі Development
  • читання аргументів командного рядка
  • логування в консоль, Debug та EventSource (на Windows — ще й EventLog)
  • встановлення кореневого каталогу додатку (поточний каталог)
  • у середовищі Development — перевірку контейнера під час Build() (ValidateOnBuild і ValidateScopes, див. 21.2–21.3)

Починаючи з .NET 7, поряд із цим «callback»-стилем є і лінійний API — Host.CreateApplicationBuilder(args), який повертає HostApplicationBuilder з властивостями Services, Configuration і Logging. Його налаштовують звичайними інструкціями без вкладених лямбд — так само, як WebApplication.CreateBuilder у веб-проєктах, — і саме його використовують сучасні шаблони Worker Service:

var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddSingleton<IPatientRepository, InMemoryPatientRepository>();
builder.Services.AddHostedService<AppointmentReminderService>();
using IHost host = builder.Build();
await host.RunAsync();

Обидва варіанти будують той самий IHost з однаковими налаштуваннями за замовчуванням; відрізняється лише стиль конфігурування.

Реалізуємо спрощений хост «з нуля»

Щоб зрозуміти, що відбувається всередині IHost, побудуємо власну спрощену реалізацію. Це найкращий спосіб зрозуміти будь-яку інфраструктурну абстракцію — побудувати її самостійно.

Ключові компоненти реального Generic Host

Розглянемо API, що ви побачите у реальному коді .NET-проектів:

IHost — центральний інтерфейс. Надає доступ до IServiceProvider через властивість Services. Методи StartAsync() / StopAsync() / RunAsync() керують lifecycle.

IHostBuilder — будівельник хоста. Надає fluent API для конфігурації:

  • ConfigureServices(Action<IServiceCollection>) — реєстрація сервісів
  • ConfigureAppConfiguration(Action<IConfigurationBuilder>) — додаткові джерела конфігурації
  • ConfigureLogging(Action<ILoggingBuilder>) — налаштування логування
  • UseEnvironment(string) — встановлення середовища (Development, Production)

IHostedService — інтерфейс фонового сервісу. Два методи: StartAsync(CancellationToken) і StopAsync(CancellationToken). Хост запускає всі зареєстровані IHostedService при старті і зупиняє у зворотному порядку при shutdown.

BackgroundService — абстрактний базовий клас, що реалізує IHostedService. Достатньо перевизначити один метод: ExecuteAsync(CancellationToken stoppingToken) — і писати в ньому нескінченний цикл або очікування.

Generic Host — архітектура та потік запускуGeneric Host — архітектура та потік запуску

Порядок старту та shutdown

Розуміння порядку є критичним для коректної роботи:

Запуск:

  1. IHostBuilder.Build() — збирається DI-контейнер; у середовищі Development ще й перевіряється, чи всі зареєстровані сервіси можна створити (у Production ця перевірка вимкнена, і помилка реєстрації проявиться лише при першому запиті сервісу)
  2. IHost.StartAsync() — послідовно викликає StartAsync() у кожного IHostedService у порядку реєстрації
  3. Після старту всіх служб IHostApplicationLifetime сповіщає підписників ApplicationStarted; хост входить у стан «running» і чекає сигналу зупинки

Важлива деталь щодо BackgroundService: його StartAsync запускає ExecuteAsync і повертає керування, щойно той дійде до першого справжнього await. Тому довгий синхронний код на початку ExecuteAsync (до першого await) блокує запуск усіх наступних служб.

Shutdown (при Ctrl+C, SIGTERM або явному виклику StopAsync()):

  1. IHostApplicationLifetime сповіщає підписників ApplicationStopping
  2. IHost.StopAsync() — послідовно викликає StopAsync() у кожного IHostedService у зворотному порядку
  3. IHostApplicationLifetime сповіщає підписників ApplicationStopped
  4. DI-контейнер диспозиться — викликається Dispose() на всіх IDisposable-об'єктах, які створив кореневий контейнер (singleton-сервіси і transient-сервіси, отримані з кореня). Об'єкти, передані готовими через AddSingleton(instance), контейнер не звільняє — ними володіє той, хто їх створив

На весь shutdown хост дає обмежений час — HostOptions.ShutdownTimeout, за замовчуванням 30 секунд (до .NET 6 — 5 секунд). Служба, що не встигла завершити StopAsync, отримує скасування токена, і хост продовжує зупинку без неї.

Наведений порядок перевірено на .NET 10 для двох служб A і B, зареєстрованих саме в такому порядку: A start → B start → ApplicationStarted → ApplicationStopping → B stop → A stop → ApplicationStopped → Dispose.

Зворотний порядок shutdown є навмисним і важливим: перший зупинений сервіс може залежати від ресурсів, що надають пізніше зупинені сервіси. Наприклад, EmailSender (зупиняється першим) може надсилати Shutdown notification через SmtpClient (зупиняється останнім).

Generic Host vs WebApplication

Починаючи з .NET 6, для ASP.NET Core проектів з'явився WebApplication — ще більш спрощений API (var app = WebApplication.Create()). Він побудований поверх Generic Host і фактично є синтаксичним цукром для найпоширенішого сценарію — веб-додатків. Для консольних програм, Worker Services, мікросервісів без HTTP — Generic Host залишається правильним вибором.

Generic Host WebApplication
Тип додатку будь-який ASP.NET Core
Підключення NuGet-пакет Microsoft.Extensions.Hosting спільний фреймворк Microsoft.AspNetCore.App (SDK Microsoft.NET.Sdk.Web)
HTTP pipeline немає є (middleware)
Запуск host.RunAsync() app.RunAsync()
IServiceCollection ConfigureServices() builder.Services

Підсумок

Generic Host вирішує проблему «розсипаного Main» шляхом введення чіткої відповідальності: хост знає про lifecycle, конфігурацію і контейнер сервісів — а бізнес-код нічого цього не знає і не зобов'язаний знати. Це пряме втілення принципів SRP та DIP зі SOLID: кожна частина системи відповідає за одне, і залежності течуть через абстракції.

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