Конфігурація: Паттерн Options
Конфігурація: Паттерн Options
У попередньому розділі ми дізналися, що можемо діставати будь-яке налаштування просто викликавши config["Ключ:ВкладенийКлюч"].
Хоча це легко, у великих проєктах такий підхід (відомий як Magic Strings - "магічні рядки") стає небезпечним:
- Легко зробити помилку в тексті ключа (написати
"Datebase"замість"Database"). Компілятор вам не допоможе, але програма впаде в Runtime. - Значення повертаються як
string, і вам доведеться щоразу їх перетворювати на числа, булеві значення тощо. - Ваші розкидані по коду сервіси будуть "жорстко" прив'язані до інтерфейсу
IConfiguration.
Щоб вирішити ці проблеми, ASP.NET Core пропонує використовувати Паттерн Options. Його суть — прив'язати (Bind) секцію з JSON до суворого, типізованого C# класу, і в сервісах (роутах) запитувати вже цей клас, а не IConfiguration.
Створення моделі налаштувань
Уявіть, що у нас є така секція в appsettings.json:
{
"PaymentGateway": {
"ApiUrl": "https://api.stripe.com/v1/",
"ApiKey": "sk_test_123456",
"TimeoutSeconds": 30
}
}
Крок 1: створюємо звичайний C# клас, властивості якого співпадають з іменами ключів у JSON.
public class PaymentOptions
{
// C# властивість "ApiUrl" автоматично співставиться з JSON-ключем "ApiUrl"
public string ApiUrl { get; set; } = string.Empty;
public string ApiKey { get; set; } = string.Empty;
// Тип int - фреймворк виконає конвертацію "рядка з JSON у число" автоматично!
public int TimeoutSeconds { get; set; }
}
Проекція конфігурації (Binding)
Тепер нам потрібно "зв'язати" секцію PaymentGateway з класом PaymentOptions в нашому контейнері залежностей (Dependency Injection).
var builder = WebApplication.CreateBuilder(args);
// Крок 2: Зв'язуємо (Bind) секцію конфігурації з класом.
// Метод Configure<> реєструє PaymentOptions у контейнері DI.
builder.Services.Configure<PaymentOptions>(
builder.Configuration.GetSection("PaymentGateway")
);
var app = builder.Build();
// Крок 3: Використовуємо в роутах
app.MapGet("/info", (IOptions<PaymentOptions> options) =>
{
// Тепер ми маємо доступ до суворо типізованих властивостей з автозаповненням (IntelliSense)!
var url = options.Value.ApiUrl;
var timeout = options.Value.TimeoutSeconds;
return $"Gateway URL: {url}, Timeout: {timeout}s";
});
app.Run();
Анатомія коду:
- Ми використали
builder.Services.Configure<T>(). Цей метод вказує фреймворку: "Коли хтось попроситьPaymentOptions, візьми для нього секціюPaymentGatewayз конфігурації, створи екземпляр класу і заповни його властивості". - В аргументах
MapGetми просимо не простоPaymentOptions, а спеціальну обгорткуIOptions<PaymentOptions>. Це важливо! Система Options працює лише через ці спеціальні інтерфейси-обгортки. - Щоб отримати сам екземпляр вашого класу з даними, ми звертаємося до властивості
.Value.
IOptions<>? Вона дозволяє фреймворку керувати життєвим циклом ваших налаштувань, кешувати їх, а головне — відстежувати зміни файлу в реальному часі (про це нижче).Інтерфейси Options: Snapshot та Monitor
Уявіть, що сервер працює, і ви вирішили оновити таймаут підключення з 30 до 45 безпосередньо в appsettings.json на диску сервера.
Що станеться?
- Якщо ви використовуєте
IOptions<T>: Налаштування застосовуються лише один раз, під час запуску додатку (Singleton). Ваш код все ще побачить таймаут30. Щоб побачити45, доведеться перезапустити сервер. - Що робити? Використовувати динамічні обгортки!
IOptionsSnapshot<T>
IOptionsSnapshot<T> обчислює налаштування на початку кожного HTTP запиту (у нього Scope життєвий цикл).
app.MapGet("/info-snapshot", (IOptionsSnapshot<PaymentOptions> options) =>
{
// Якщо файл зміниться на диску під час роботи сервера,
// НАСТУПНИЙ http-запит отримає вже оновлені дані!
return options.Value.TimeoutSeconds;
});
IOptionsMonitor<T>
IOptionsMonitor<T> працює в реальному часі (Singleton) і навіть надає події (events), до яких можна підключитися, щоб виконати код саме в момент, коли хтось зберіг файл на диску.
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<PaymentOptions>(builder.Configuration.GetSection("PaymentGateway"));
var app = builder.Build();
// Використання Monitor в DI (наприклад, у фоновому сервісі або складнішому контролері)
app.MapGet("/info-monitor", (IOptionsMonitor<PaymentOptions> options) =>
{
// CurrentValue завжди містить найнайсвіжіші дані в поточний мілісекунду
return options.CurrentValue.TimeoutSeconds;
});
// Підписка на подію зміни конфігурації (тільки для IOptionsMonitor)
var monitor = app.Services.GetRequiredService<IOptionsMonitor<PaymentOptions>>();
monitor.OnChange(newConfig =>
{
Console.WriteLine($"Увага! Хтось щойно змінив налаштування. Новий API URL: {newConfig.ApiUrl}");
});
app.Run();
Анатомія коду:
IOptionsMonitorідеально підходить для глобальних (Singleton) сервісів, які працюють роками без зупинок, і яким потрібно моментально дізнаватися про зміни конфігурації. Для цього він відкриває доступ доCurrentValueзамістьValueта методуOnChange(...).
Валідація налаштувань (Options Validation)
Ще одна потужна сторона типізованих налаштувань — вбудована валідація. Якщо ви забули вказати ApiKey у JSON файлі, краще, щоб програма впала на старті з чіткою помилкою, а не десь в процесі запиту клієнта з невідомим NullReferenceException.
ASP.NET Core дозволяє використовувати звичайні DataAnnotation атрибути (як у Entity Framework) для ваших класів конфігурації.
using System.ComponentModel.DataAnnotations;
public class EmailOptions
{
[Required(ErrorMessage = "Хост поштової скриньки обов'язковий!")]
public string Host { get; set; } = string.Empty;
[Range(1, 65535, ErrorMessage = "Порт повинен бути від 1 до 65535")]
public int Port { get; set; }
[EmailAddress]
public string SenderEmail { get; set; } = string.Empty;
}
Щоб змусити фреймворк перевіряти ці правила під час прив'язки конфігурації, ми змінюємо Configure на дві інші команди — AddOptions з подальшою перевіркою ValidateDataAnnotations:
var builder = WebApplication.CreateBuilder(args);
// Замість прямого builder.Services.Configure<>
builder.Services.AddOptions<EmailOptions>()
.Bind(builder.Configuration.GetSection("Email"))
.ValidateDataAnnotations() // Активує перевірку атрибутів [Required], [Range] тощо
.ValidateOnStart(); // Перевірити файл одразу при запуску додатку (або він не запуститься)
var app = builder.Build();
app.Run();
Тепер, якщо ви забудете вказати Host у appsettings.json, додаток негайно кине виняток у консоль з повідомленням "Хост поштової скриньки обов'язковий!" і завершить роботу. Так ви ніколи не розгорнете "зламаний" код на Production.
Практичні завдання
appsettings.json створіть секцію "ThirdPartyAPI": { "BaseUrl": "...", "Retries": 3 }.
Створіть відповідний C# клас. Зареєструйте його в DI як Options.
Створіть роут GET /api-info та заінжектуйте туди ваш клас за допомогою інтерфейсу IOptions<T>. Поверніть значення клієнту.IOptionsSnapshot<T>.
Запустіть програму (dotnet run). Зробіть запит до /api-info.
Не зупиняючи програму в консолі, відкрийте appsettings.json, змініть кількість Retries з 3 на 5 і збережіть файл.
Зімітуйте новий запит до /api-info (просто оновіть сторінку в браузері). Що відбулося?[Required] на BaseUrl та [Range(1, 10)] на Retries.
Використайте AddOptions<T>().Bind(...).ValidateDataAnnotations().ValidateOnStart().
Спробуйте поставити у appsettings.json значення Retries = 999 і запустити програму. Опишіть поведінку фреймворка і яку помилку ви бачите.Конфігурація в ASP.NET Core: Основи
Будь-якому додатку потрібні налаштування. Рядки підключення до бази даних (Connection Strings), секретні ключі для JWT-токенів, адреси сторонніх API, таймаути чи навіть просто увімкнення режиму "технічних робіт" — усе це не можна тримати "захардкодженим" (hardcoded) прямо в C#.
Логування в ASP.NET Core: Основи
Уявіть, що ви ведете щоденник. Коли все добре, ви записуєте туди дрібні радощі. Коли виникає проблема — ви описуєте її детально, щоб потім проаналізувати. Логування (Logging) у програмуванні — це ведення такого "щоденника" вашим додатком.