OOP Course
Сьогодні

Підрозділ 21.4

Options Pattern — типізована конфігурація з IOptions<T>

21.4. Options Pattern — типізована конфігурація з IOptions&lt;T&gt; Конфігурація — невід'ємна частина будь якого реального додатку. Рядок підключення до бази даних, адреса SMTP сервера, максимальна кількість за

21.4. Options Pattern — типізована конфігурація з IOptions<T>

Конфігурація — невід'ємна частина будь-якого реального додатку. Рядок підключення до бази даних, адреса SMTP-сервера, максимальна кількість записів у черзі, тайм-аути — усе це параметри, що змінюються між середовищами (Development, Staging, Production) і не повинні бути захардкоджені у коді. У .NET для роботи з конфігурацією існує ціла екосистема, центром якої є Options Pattern.

Options Pattern — це узгоджений підхід до читання конфігурації через типізовані класи. Замість читання рядків через IConfiguration["Section:Key"] скрізь по коду, ви оголошуєте POCO-клас (Plain Old C# Object), прив'язуєте його до секції конфігурації один раз при старті — і далі отримуєте через DI вже готовий, типізований, перевірений об'єкт.

Проблема: IConfiguration по всьому коду

Розглянемо типову еволюцію роботи з конфігурацією без Options Pattern:

// Антипаттерн: рядок ключа розкиданий по коду
class AppointmentService
{
    private readonly IConfiguration _config;
    public AppointmentService(IConfiguration config) { _config = config; }

    public void Book(...)
    {
        var maxPerDay = int.Parse(_config["Appointments:MaxPerDay"]); // magic string!
        var timeout   = TimeSpan.FromSeconds(
                            double.Parse(_config["Appointments:TimeoutSeconds"])); // дублювання!
        // ...
    }
}

class ReportService
{
    private readonly IConfiguration _config;
    public void Generate()
    {
        var maxPerDay = int.Parse(_config["Appointments:MaxPerDay"]); // ще раз дублювання
        // Якщо назву ключа зміните — помилка лише в runtime, не в compile time!
    }
}

Ця практика має щонайменше чотири вади:

  1. Magic strings — будь-яка помилка в ключі виявляється лише у runtime
  2. Дублювання — один ключ читається в десяти місцях
  3. Немає типізаціїIConfiguration повертає string?, потрібне ручне парсування
  4. Важко тестувати — для тесту AppointmentService потрібно мокати весь IConfiguration

Options Pattern: рішення

Options Pattern пропонує три кроки:

1. Оголошення класу налаштувань:

class AppointmentOptions
{
    public int MaxPerDay    { get; set; } = 20;
    public int TimeoutSeconds { get; set; } = 30;
    public string AllowedStatuses { get; set; } = "Scheduled,Confirmed";
}

2. Реєстрація і прив'язка до секції:

services.Configure<AppointmentOptions>(
    configuration.GetSection("Appointments"));

3. Ін'єкція через IOptions<T>:

class AppointmentService
{
    private readonly AppointmentOptions _opts;
    public AppointmentService(IOptions<AppointmentOptions> opts)
        => _opts = opts.Value;

    public void Book(...)
    {
        if (_opts.MaxPerDay < 1) throw new ...;
        // Типізовано, з автодоповненням, без magic strings
    }
}

Три варіанти IOptions

Інтерфейс Оновлення Scope Використання
IOptions<T> ні (Singleton) будь-який типові налаштування
IOptionsSnapshot<T> при кожному Scoped Scoped перезавантаження при зміні файлу
IOptionsMonitor<T> реактивно (колбек) Singleton hot reload, повідомлення про зміни

IOptions<T> реєструється як Singleton: значення зчитується один раз при запуску. IOptionsSnapshot<T> — Scoped: якщо файл конфігурації змінився між двома HTTP-запитами, другий запит отримає нові значення. IOptionsMonitor<T> — Singleton, але надає метод OnChange(Action<T>) для реакції на зміни в реальному часі.

Реалізуємо Options Pattern «з нуля»

Валідація налаштувань

Реальний IOptions підтримує валідацію через Data Annotations або кастомний IValidateOptions<T>. Це дозволяє перевірити конфігурацію при запуску і не запускати додаток з некоректними налаштуваннями:

using System.ComponentModel.DataAnnotations;

class DatabaseOptions
{
    [Required]
    [MinLength(10)]
    public string ConnectionString { get; set; } = "";

    [Range(1, 1000)]
    public int MaxConnections { get; set; } = 10;
}

// У реєстрації:
services.AddOptions<DatabaseOptions>()
        .Bind(configuration.GetSection("Database"))
        .ValidateDataAnnotations()     // перевірка через атрибути
        .ValidateOnStart();            // викид виключення при запуску, якщо невалідно

Це позволяє уникнути ситуацій, коли додаток запускається, але падає через відсутнє або некоректне значення конфігурації через кілька хвилин роботи.

Named Options

Іноді потрібні кілька різних екземплярів одного класу налаштувань. Типовий приклад — два SMTP-сервери: один для транзакційних листів, інший для маркетингових:

services.Configure<SmtpOptions>("Transactional",
    configuration.GetSection("Smtp:Transactional"));
services.Configure<SmtpOptions>("Marketing",
    configuration.GetSection("Smtp:Marketing"));

// Використання:
class EmailService
{
    private readonly SmtpOptions _transactional;
    private readonly SmtpOptions _marketing;

    public EmailService(IOptionsSnapshot<SmtpOptions> opts)
    {
        _transactional = opts.Get("Transactional");
        _marketing     = opts.Get("Marketing");
    }
}

Options Pattern — IOptions, IOptionsSnapshot, IOptionsMonitor

Де зберігати конфігурацію

Generic Host автоматично завантажує конфігурацію з декількох джерел у порядку зростаючого пріоритету (кожне наступне перекриває попереднє):

  1. appsettings.json — базова конфігурація для всіх середовищ
  2. appsettings.{Environment}.json — специфічна для середовища (наприклад, appsettings.Development.json)
  3. Секрети (User Secrets) — для чутливих даних у Development
  4. Змінні середовища — стандартний спосіб конфігурації в контейнерах (Docker/Kubernetes)
  5. Аргументи командного рядка — найвищий пріоритет, зручно для тестів та CI/CD

Чутливі дані (паролі, API-ключі, рядки з'єднань) ніколи не повинні потрапляти до appsettings.json, що зберігається у git. Для Production використовують змінні середовища або секретні сховища (Azure Key Vault, AWS Secrets Manager тощо).

Підсумок

Options Pattern вирішує проблему розсипаної конфігурації так само, як DI вирішує проблему розсипаних залежностей: збирає все в одному місці, дає типізацію і перевірку при запуску, і надає через DI вже готовий об'єкт — без magic strings і ручного парсування. Разом Generic Host, IServiceCollection і Options Pattern утворюють повну інфраструктуру для побудови добре структурованих, конфігурованих і тестованих .NET-додатків.

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