Minimal API

Конфігурація: Паттерн Options

У попередньому розділі ми дізналися, що можемо діставати будь-яке налаштування просто викликавши config["Ключ:ВкладенийКлюч"].

Конфігурація: Паттерн Options

У попередньому розділі ми дізналися, що можемо діставати будь-яке налаштування просто викликавши config["Ключ:ВкладенийКлюч"].

Хоча це легко, у великих проєктах такий підхід (відомий як Magic Strings - "магічні рядки") стає небезпечним:

  1. Легко зробити помилку в тексті ключа (написати "Datebase" замість "Database"). Компілятор вам не допоможе, але програма впаде в Runtime.
  2. Значення повертаються як string, і вам доведеться щоразу їх перетворювати на числа, булеві значення тощо.
  3. Ваші розкидані по коду сервіси будуть "жорстко" прив'язані до інтерфейсу IConfiguration.

Щоб вирішити ці проблеми, ASP.NET Core пропонує використовувати Паттерн Options. Його суть — прив'язати (Bind) секцію з JSON до суворого, типізованого C# класу, і в сервісах (роутах) запитувати вже цей клас, а не IConfiguration.

Створення моделі налаштувань

Уявіть, що у нас є така секція в appsettings.json:

appsettings.json
{
    "PaymentGateway": {
        "ApiUrl": "https://api.stripe.com/v1/",
        "ApiKey": "sk_test_123456",
        "TimeoutSeconds": 30
    }
}

Крок 1: створюємо звичайний C# клас, властивості якого співпадають з іменами ключів у JSON.

Options/PaymentOptions.cs
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).

Program.cs
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), до яких можна підключитися, щоб виконати код саме в момент, коли хтось зберіг файл на диску.

Program.cs
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) для ваших класів конфігурації.

Options/EmailOptions.cs
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:

Program.cs
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>. Поверніть значення клієнту.
Copyright © 2026