WebApplication, Builder та Dependency Injection
WebApplication, Builder та Філософія Dependency Injection
WebApplicationBuilder і дізнаємося, як він керує всім життям вашого додатку.1. Контекст: Патерн Builder та його мета
Згадайте наш 4-рядковий Program.cs:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello, World!");
app.Run();
Чому ми не створюємо додаток просто як new WebApplication()?
ASP.NET Core — це гігантський механізм. Він має вбудований парсер конфігурацій (JSON, XML, Environment Variables), систему логування, систему маршрутизації, CORS, систему аутентифікації тощо. Якщо створювати клас WebApplication безпосередньо через конструктор, він мав би тисячі параметрів. Це відомий антипатерн "Telescoping Constructor".
Замість цього розробники використовують Патерн Builder (Будівельник).
Ви отримуєте "порожній аркуш" будівельника (builder), налаштовуєте все, що вам потрібно (додаєте сервіси, змінюєте логування), а потім кажете "побудуй!" (Build()). Після виклику Build() додаток стає незмінним (immutable) щодо своїх базових сервісів. Це гарантує стабільність під час обробки тисяч запитів.
WebApplication. Містить п'ять головних властивостей для налаштування майбутнього хоста:Services(IServiceCollection) - ін'єкція залежностейConfiguration(ConfigurationManager) - налаштуванняEnvironment(IWebHostEnvironment) - середовище виконанняLogging(ILoggingBuilder) - управління логамиHost/WebHost- низькорівневі налаштування
2. Глибинна проблема: Тісна зв'язність (Tight Coupling)
Щоб зрозуміти властивість builder.Services, ми повинні зробити крок назад і подивитися на фундаментальну проблему програмування: Залежності.
Уявіть, що ви пишете додаток, який працює з базою даних і відправляє email'и.
public class OrderService
{
private SqlDatabase _db;
private SmtpEmailSender _emailSender;
public OrderService()
{
// ПРОБЛЕМА ТУТ! OrderService сам створює свої залежності
_db = new SqlDatabase("Server=myServer;Database=myDataBase;");
_emailSender = new SmtpEmailSender("smtp.gmail.com");
}
public void ProcessOrder(Order order)
{
_db.Save(order);
_emailSender.Send(order.CustomerEmail, "Замовлення прийнято");
}
}
Чому цей код жахливий?
- Неможливо тестувати (No Unit Tests). Якщо ви захочете протестувати логіку
OrderService, він завжди намагатиметься писати в реальнуSqlDatabaseі відправляти реальний email. Ви не можете підсунути йому "фейкову" (mock) базу. - Нульова гнучкість. Якщо ви вирішите змінити
SqlDatabaseнаMongoDatabase, абоSmtpEmailSenderнаSendGridEmailSender, вам доведеться переписувати класOrderService. А що, як цих сервісів 500? - Виток абстракцій.
OrderService(який займається бізнес-логікою) не повинен знати рядки підключення (Connection Strings) або адреси серверів розсилки.
Священний грааль архітектури: SOLID (зокрема буква D - Dependency Inversion). Класи повинні залежати від абстракцій (інтерфейсів), а не від конкретних реалізацій.
3. Inversion of Control (IoC) та Впровадження Залежностей (DI)
Ми вирішуємо проблему вище за допомогою Dependency Injection (Впровадження залежностей).
public interface IDatabase { void Save(Order order); }
public interface IEmailSender { void Send(string to, string text); }
// Конкретні реалізації
public class SqlDatabase : IDatabase { ... }
public class SmtpEmailSender : IEmailSender { ... }
public class OrderService
{
private readonly IDatabase _db;
private readonly IEmailSender _emailSender;
// OrderService більше не використовує ключове слово 'new'!
// Він ПРОСИТЬ дати йому готові об'єкти.
public OrderService(IDatabase db, IEmailSender emailSender)
{
_db = db;
_emailSender = emailSender;
}
public void ProcessOrder(Order order)
{
_db.Save(order);
_emailSender.Send(order.CustomerEmail, "Success");
}
}
Тепер OrderService чистий, ідеально тестується і гнучкий. Але виникає інша проблема: хто створюватиме і передаватиме ці об'єкти (db і email) всередину OrderService? Якщо це робити вручну, це перетвориться на хаос.
Саме тут на арену виходить ASP.NET Core DI Container (IoC Контейнер) — це фабрика об'єктів. Це і є наш builder.Services.
Ви один раз на початку життєвого циклу реєструєте правила створення: "Гей, Контейнере! Якщо хтось попросить IDatabase, дай йому екземпляр SqlDatabase". А фреймворк автоматично створить потрібний об'єкт і "впорсне" (inject) його в конструктор OrderService.
4. builder.Services: Колекція Сервісів (IServiceCollection)
Зазирніть у наш будівельник:
var builder = WebApplication.CreateBuilder(args);
// РЕЄСТРАЦІЯ СЕРВІСІВ!
builder.Services.AddTransient<IDatabase, SqlDatabase>();
builder.Services.AddSingleton<IEmailSender, SmtpEmailSender>();
builder.Services.AddScoped<OrderService>();
ServiceDescriptor).
Ця колекція збирає всі "інструкції", як створювати об'єкти. Коли викликається builder.Build(), ця колекція трансформується у незмінний фабричний об'єкт IServiceProvider, який використовується ASP.NET для подачі залежностей на запит.Зверніть увагу на слова AddTransient, AddSingleton, AddScoped. Це Життєві Цикли (Lifetimes) об'єктів. Це найважливіша концепція, яку ви маєте зрозуміти в ASP.NET.
4.1. Singleton (Єдиний)
builder.Services.AddSingleton<IEmailSender, SmtpEmailSender>();
Як працює: Контейнер створює об'єкт РІВНО ОДИН РАЗ за весь час роботи серверу. Коли приходить перший запит і просить IEmailSender, контейнер створює його і зберігає в пам'яті. Всі наступні мільйони запитів будуть отримувати цей самий об'єкт з пам'яті.
Коли використовувати: Кешування в пам'яті (In-Memory Cache), налаштування додатку, сервіси-утиліти, які не мають змінного стану (stateless).
!WARNING Якщо ви збережете змінні всередині Singleton, всі користувачі сайту зможуть їх побачити! Уникайте там даних, прив'язаних до конкретного клієнта.
4.2. Transient (Транзитний / Короткостроковий)
builder.Services.AddTransient<IRandomNumberGenerator, RandomNumberGenerator>();
Як працює: Кожного разу, коли будь-який об'єкт просить цю залежність, контейнер створює НОВИЙ екземпляр. Якщо ControllerA і ServiceB просять його протягом одного клієнтського запиту, вони отримають два різні об'єкти в пам'яті.
Коли використовувати: Легкі класи без стану (Math Helpers). Вони швидко видаляються Garbage Collector-ом з пам'яті (Gen 0).
4.3. Scoped (В межах запиту) - Найважливіший!
builder.Services.AddScoped<IDatabase, SqlDatabase>();
Як працює: Створюється РАЗ на кожен HTTP-запит. Користувач A клікає кнопку на сайті -> створюється новий HttpContext. Для цього і всіх вкладених сервісів створюється один екземпляр SqlDatabase. Користувач B клікає кнопку секундою пізніше -> для нього створюється свій незалежний об'єкт. Але якщо під час обробки Запиту A три різних сервіси просять БД, вони отримають тою самий об'єкт. Це геніально для Транзакцій!
Коли використовувати: Майже вся бізнес-логіка, репозиторії бази даних (Entity Framework DbContext), інформація про поточного авторизованого користувача.
5. Анатомія Методів розширення (Extension Methods)
Часто, читаючи документацію Microsoft, ви бачите код типу:
builder.Services.AddControllers();
builder.Services.AddSwaggerGen();
Що це за магічні методи AddControllers? Ви не знайдете їх в інтерфейсі IServiceCollection.
Це — Методи Розширення (Extension Methods).
У .NET є сотні сервісів для конфігурації Swagger, Entity Framework чи Identity. Якби ми реєстрували кожен сервіс вручну, наш Program.cs виглядав би жахливо:
builder.Services.AddTransient<ISwaggerProvider, SwaggerGenerator>();
builder.Services.AddTransient<ISchemaGenerator, DefaultSchemaGenerator>();
builder.Services.AddTransient<IApiDescriptionGroupCollectionProvider, ApiDescriptionGroupCollectionProvider>();
// ...і ще 30 рядків налаштувань лише для Swagger
Щоб сховати цей жах, розробники бібліотек пишуть методи розширення. Це звичайні стаціонарні методи, які "приклеюються" до об'єкта IServiceCollection.
Як створити власний Extension Method?
Ключове слово this перед першим параметром!
// Окремий файл: MyDependenciesExtension.cs
public static class MyDependenciesExtension
{
public static IServiceCollection AddMyBusinessLogic(this IServiceCollection services)
{
// Ховаємо всю складність тут
services.AddScoped<IOrderService, OrderService>();
services.AddScoped<IPaymentGateway, StripePaymentGateway>();
services.AddTransient<IEmailSender, SmtpEmailSender>();
return services; // Обов'язково повертаємо для подальшого "ланцюга" викликів
}
}
Тепер наш Program.cs стає неймовірно красивим, чистим абстрактним шедевром:
var builder = WebApplication.CreateBuilder(args);
// Викликаємо наш власний метод "збірки залежностей"
builder.Services.AddMyBusinessLogic();
var app = builder.Build();
Це називається "Збірка залежностей у модулі". Великі проекти (Clean Architecture) мають багато таких методів-розширень для ізоляції конфігурацій (наприклад, AddInfrastructure, AddApplication). Це є галузевим стандартом у сучасному .NET.
6. Інші Повноваження WebApplicationBuilder
Окрім Сервісів, Builder керує глобальними налаштуваннями додатку перед тим, як він скомпілюється.
6.1. Глобальна Конфігурація (builder.Configuration)
Раніше ми бачили файл appsettings.json. Хто його читає? Builder! Він робить це автоматично під час виклику WebApplication.CreateBuilder(args).
Конфігурація в ASP.NET Core — це не просто читання конфігураційного файлу. Це справжній архітектурний шедевр, заснований на Шарах Перевизначення (Hierarchy of Configuration Providers).
Проблема: Захардкоджені секрети
Уявіть, що ви підключаєтесь до бази даних або інтегруєте Stripe для платежів. Вам потрібен пароль або секретний API ключ.
{
"ConnectionStrings": {
"DefaultConnection": "Server=myServer;User Id=arakviel;Password=SuperSecretPassword123!"
},
"Stripe": {
"ApiKey": "sk_live_verySecretKey"
}
}
Якщо ви закомітите такий appsettings.json у GitHub, боти знайдуть ваш ключ Stripe за 5 секунд і почнуть знімати гроші з вашого рахунку. Базовий appsettings.json йде у репозиторій, тому він ніколи не повинен містити реальних секретів. Лише порожні шаблони або безпечні налаштування (наприклад, LogLevel: Warning).
Рішення: Configuration Providers (Джерела конфігурації)
ASP.NET Core створює так званий IConfiguration об'єкт, який автоматично склеює дані з різних джерел.
За замовчуванням WebApplication.CreateBuilder(args) налаштовує зчитування в такому точному порядку (кожен наступний пункт перезаписує значення з попереднього):
appsettings.json: Базові налаштування (комітяться в Git).appsettings.{Environment}.json: Специфічні для середовища налаштування (наприклад,appsettings.Development.json).- User Secrets (Секрети розробника): Спеціальне сховище на вашому комп'ютері, яке знаходиться поза папкою вашого коду (тобто git його не побачить). Використовується лише під час розробки (локально).
- Environment Variables (Змінні середовища ОС): Ключовий інструмент для деплою (Docker, Linux сервери, AWS).
- Command-line arguments (
args): Те, що ви передаєте при запуску програми в терміналі (наприклад,dotnet run --urls=http://localhost:8080).
appsettings.json— це Конституція країни (глобально для всіх).Environment Variables— це розпорядження мера вашого міста (перекриває Конституцію в певних локальних питаннях для конкретного сервера).Command-line args— це прямий наказ від поліцейського на вулиці (найвищий пріоритет прямо зараз).
Як читати ці дані?
Конфігурація збирається у великий "словник" (Key-Value), доступ до якого здійснюється через єдиний інтерфейс. Ієрархія (наприклад, Stripe -> ApiKey) передається через двокрапку ::
var builder = WebApplication.CreateBuilder(args);
// Читання рядка для Stripe
string stripeKey = builder.Configuration["Stripe:ApiKey"];
// Спеціальний шорткат для підключення до БД (читає з блоку "ConnectionStrings")
string dbString = builder.Configuration.GetConnectionString("DefaultConnection");
app.MapGet("/", () => $"Stripe Key is: {stripeKey ?? "Not Found"}");
app.Run();
Коли спрацьовує цей код, ASP.NET Core не хвилює, звідки прийшов Stripe:ApiKey. Він міг бути в JSON, або в змінній середовища Linux export Stripe__ApiKey=sk_live_... (в Linux замість : використовується __), або в User Secrets. Код залишається незмінним!
Патерн Глибокої Конфігурації (Options Pattern)
Для великих проєктів читати рядки напряму через ["JwtSettings:SecretKey"] вважається поганим тоном (можна зробити помилку в тексті). Професіонали використовують Options Pattern. Вони створюють C# клас, який ідеально відповідає структурі JSON.
{
"JwtConfig": {
"Secret": "SuperSecretKey123",
"ExpirationDays": 7
}
}
// 1. Створюємо C# Клас
public class JwtConfig
{
public string Secret { get; set; } = string.Empty;
public int ExpirationDays { get; set; }
}
// 2. У Program.cs прив'язуємо секцію JSON до класу і кладемо в DI Контейнер
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<JwtConfig>(builder.Configuration.GetSection("JwtConfig"));
var app = builder.Build();
// 3. Тепер будь-який сервіс може попросити IOptions<JwtConfig> через Dependency Injection!
app.MapGet("/token", (IOptions<JwtConfig> options) =>
{
var config = options.Value;
return $"Секрет: {config.Secret}, Діє днів: {config.ExpirationDays}";
});
Це вершина еволюції роботи з налаштуваннями: вони строго типізовані, перевіряються компілятором і легко підміняються в unit-тестах!
Крок 1: Редагування JSON
У файлі appsettings.json додайте нову секцію PaymentGateway з полем ApiKey: "DefaultJsonSecret".
Крок 2: Читання конфігурації
У Program.cs прочитайте це значення і виведіть на екран:
app.MapGet("/config", (IConfiguration config) => config["PaymentGateway:ApiKey"]);
Крок 3: Перевірка JSON
Запустіть додаток через dotnet run і перейдіть на localhost:XXXX/config. Ви побачите "DefaultJsonSecret".
Крок 4: Використання змінних середовища
Тепер зупиніть сервер (Ctrl + C) і запустіть його в терміналі з передачею змінної середовища.
Зверніть увагу на подвійне підкреслення __ замість двокрапки :!
$env:PaymentGateway__ApiKey="ProductionSecret"; dotnet run
export PaymentGateway__ApiKey="ProductionSecret" && dotnet run
Крок 5: Результат
Оновіть сторінку браузера. Значення змінилося на "ProductionSecret", хоча код ви не чіпали! Це і є магія шаруватої конфігурації ASP.NET Core, яка дозволяє безпечно деплоїти додатки.
6.2. Налаштування Логів (builder.Logging)
ASP.NET має вбудовану систему абстрактних логів (щоб не використовувати застарілий Console.WriteLine() або NLog прямо в коді). Будівельник збирає це все тут:
builder.Logging.ClearProviders(); // Очищаємо всі стандартні виводи (включаючи консоль)
builder.Logging.AddConsole(); // Додаємо назад консоль
builder.Logging.AddDebug(); // Логування в панель Debug Visual Studio
!TIP У сучасній розробці ви зазвичай залишаєте
builder.Loggingпорожнім, і використовуєте сторонні потужні бібліотеки типу Serilog, які додаються через власний метод розширення (наприклад,builder.Host.UseSerilog()).
7. Життєвий цикл додатка (Application Lifecycle)
Після того, як ви сконфігурували все, ви викликаєте:
var app = builder.Build();
Тепер app керує Життєвим Циклом (Lifecycle) вашого веб-сервера. Це перехід від стадії "підготовки" до стадії "виконання та завершення".
Коли ми запускаємо app.Run(), воно настільки глибоко інтегровано в операційну систему, що вміє правильно прокидатися і завершуватися.
Вам може знадобитися виконати код в момент, коли сервер тільки-но стартував (наприклад, прочитати дані в кеш) або перед самим закриттям (відправити сповіщення на Slack "Я впав"). Для цього ми маємо інтерфейс IHostApplicationLifetime (він живе в контейнері DI і вилучається через app.Lifetime).
Три гачки подій (Hooks)
ApplicationStarted: Викликається, коли хост повністю налаштувався і почав слухати запити.ApplicationStopping: Викликається, коли ви натиснули Ctrl+C. Додаток припиняє приймати нові запити, але дає час закінчитися вже активним (це називається Graceful Shutdown).ApplicationStopped: Викликається, коли всі підключення закрито, додаток помирає і видаляється з пам'яті.
Ось як підписатися на ці події:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// Використовуємо app.Lifetime для реєстрації подій життєвого циклу
app.Lifetime.ApplicationStarted.Register(() =>
{
Console.WriteLine("✅ Сервер Kestrel успішно піднято. Робимо розігрів кешу...");
});
app.Lifetime.ApplicationStopping.Register(() =>
{
Console.WriteLine("⚠️ Сервер отримує сигнал вимкнення. Блокуємо нові запити.");
});
app.Lifetime.ApplicationStopped.Register(() =>
{
Console.WriteLine("🛑 Сервер вимкнено. Зберігаємо фінальні логи в файл...");
});
app.MapGet("/", () => "Hello Lifecycle!");
app.Run();
Коли ви запустите такий код і потім натиснете Ctrl+C в терміналі, ви побачите, як Kestrel ідеально виконує ваші події в заданому порядку. Це є ознакою Enterprise-системи: додаток ніколи не повинен "просто вмирати" жорстко обриваючи пам'ять; він повинен закрити всі файли, завершити підключення до БД і культурно піти.
Методи керування хостом (відповідно до документації WebApplication):
Run(): Стандартний синхронний запуск.RunAsync(): Асинхронний запуск. Блокує потік, поки додаток не впаде/зупиниться.StartAsync(): Асинхронно запускає прослуховування. Воно ВІДРАЗУ повертає контроль далі по коду! Ви можете запустити сервер і одночасно робити щось інше в іншому потоці!StopAsync(): Програмний запит на зупинку сервера (якщо ми хочемо загасити сайт без натискання адміністратором кнопок).
Наприклад, можна підняти сервер на 10 секунд і вбити його:
await app.StartAsync(); // Сервер стартував
await Task.Delay(10000); // Чекаємо 10 секунд...
await app.StopAsync(); // Сервер зупинився.
Ці низькорівневі методи використовуються здебільшого для Інтеграційного Тестування (Integration Testing), коли комп'ютер сам піднімає веб-сервер у тестовому середовищі, відправляє туди запит і вбиває сервер.
8. Підсумок модуля
- Builder Pattern дозволяє нам конфігурувати надскладну систему (ASP.NET Core) поступово, додаючи сервіси та конфігурації.
- Dependency Injection — це не якась "магія", а просто спосіб писати чистий код (SOLID), перекладаючи відповідальність за створення класів на Контейнер.
- Service Lifetimes (Singleton, Scoped, Transient) — фундаментальні механізми управління пам'яттю об'єктів. Від правильного вибору життєвого циклу залежить, чи зможе ваш додаток працювати при великому навантаженні, чи пам'ять швидко потече (Memory Leak).
- Extension Methods для DI дозволяють зробити файл
Program.csчистим, винісши сотні рядків конфігурацій (наприкладAddControllers) у красиві, лаконічні викликиservices.AddMyAwesomeFeature(). - Application Lifecycle (
IHostApplicationLifetime) дає вам змогу втрутитися в процес народження і смерті додатка, що є необхідним у складних мікросервісних інфраструктурах (Kubernetes).
Що нам залишилося розібрати в анатомії Minimal API? Ми зрозуміли, як створюється додаток. Як його архітектура. Але ми ще не зрозуміли, як він обробляє конкретний запит від браузера! Шлях запиту ("Request Pipeline") — це найцікавіше місце, де ви можете підслуховувати, модифікувати і відбивати атаки. І в наступному модулі ми поговоримо про Middleware!
Перевірка знань
Якщо тест не завантажується, спробуйте самостійно відповісти на завдання для роздумів:
- Що станеться, якщо в
Singletonсервіс впровадитиScopedсервіс через конструктор? (Підказка: це відома помилка "Captive Dependency", контейнер викине виняток). - Чому метод розширення для
IServiceCollectionповинен обов'язково бути статичним класом і мати словоthis? - В чому різниця між подією ApplicationStopping та ApplicationStopped? Де ви повинні закривати з'єднання з базою даних?
Перший додаток на ASP.NET Core
Створення проекту Minimal API за допомогою .NET CLI, Visual Studio та JetBrains Rider, а також глибокий розбір згенерованого коду.
Конвеєр запитів та Middleware
Вичерпний посібник з архітектури Pipeline в ASP.NET Core. Розбираємо Middleware (Use, Run, Map), HttpContext, Request та Response об'єкти.