Шаблон параметров в .NET

Шаблон параметров использует классы для обеспечения строго типизированного доступа к группам связанных параметров. Когда параметры конфигурации изолируются по сценарию в отдельных классах, в приложениях соблюдаются два важных принципа программной инженерии.

В параметрах также предусмотрен механизм для проверки данных конфигурации. Дополнительные сведения см. в разделе Проверка параметров.

Привязка иерархической конфигурации

Предпочтительный способ чтения связанных значений конфигурации — использование шаблона параметров. Шаблон параметров можно использовать через IOptions<TOptions> интерфейс, где параметр TOptions универсального типа ограничен значением class. Позднее можно предоставить IOptions<TOptions> через внедрение зависимостей. Дополнительные сведения см. в статье Внедрение зависимостей в .NET.

Например, чтобы считывать выделенные значения конфигурации из файла appsettings.json :

{
    "SecretKey": "Secret key value",
    "TransientFaultHandlingOptions": {
        "Enabled": true,
        "AutoRetryDelay": "00:00:07"
    },
    "Logging": {
        "LogLevel": {
            "Default": "Information",
            "Microsoft": "Warning",
            "Microsoft.Hosting.Lifetime": "Information"
        }
    }
}

Создайте следующий класс TransientFaultHandlingOptions:

public sealed class TransientFaultHandlingOptions
{
    public bool Enabled { get; set; }
    public TimeSpan AutoRetryDelay { get; set; }
}

При использовании шаблона параметров класс параметров должен иметь следующие характеристики:

  • Являться неабстрактным с открытым конструктором без параметров.
  • Содержать открытые свойства для чтения и записи для привязки (поля не привязываются).

Следующий код является частью файла Program.cs C# и:

  • Вызывает ConfigurationBinder.Bind для привязки класса TransientFaultHandlingOptions к разделу "TransientFaultHandlingOptions".
  • Отображает данные конфигурации.
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using ConsoleJson.Example;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Configuration.Sources.Clear();

IHostEnvironment env = builder.Environment;

builder.Configuration
    .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
    .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, true);

TransientFaultHandlingOptions options = new();
builder.Configuration.GetSection(nameof(TransientFaultHandlingOptions))
    .Bind(options);

Console.WriteLine($"TransientFaultHandlingOptions.Enabled={options.Enabled}");
Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={options.AutoRetryDelay}");

using IHost host = builder.Build();

// Application code should start here.

await host.RunAsync();

// <Output>
// Sample output:

В приведенном выше коде файл конфигурации JSON привязан "TransientFaultHandlingOptions" к экземпляру TransientFaultHandlingOptions . Это гидратирует свойства объектов C# с соответствующими значениями из конфигурации.

ConfigurationBinder.Get<T> привязывает и возвращает указанный тип. Метод ConfigurationBinder.Get<T> может быть более удобным, чем ConfigurationBinder.Bind. В приведенном ниже примере кода демонстрируются способы использования ConfigurationBinder.Get<T> с классом TransientFaultHandlingOptions:

var options =
    builder.Configuration.GetSection(nameof(TransientFaultHandlingOptions))
        .Get<TransientFaultHandlingOptions>();

Console.WriteLine($"TransientFaultHandlingOptions.Enabled={options.Enabled}");
Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={options.AutoRetryDelay}");

В приведенном выше коде используется для получения экземпляра TransientFaultHandlingOptions объекта со значениями свойств, ConfigurationBinder.Get<T> заполненными базовой конфигурацией.

Внимание

Класс ConfigurationBinder предоставляет несколько интерфейсов API, например .Bind(object instance) и .Get<T>(), которые не ограничены атрибутом class. При использовании любого из интерфейсов параметров необходимо соблюдать вышеупомянутые ограничения для класса параметров.

Альтернативный подход при использовании шаблона параметров — привязать раздел "TransientFaultHandlingOptions" и добавить его в контейнер службы внедрения зависимостей. В следующем коде TransientFaultHandlingOptions добавляется в контейнер службы с помощью интерфейса Configure и привязывается к конфигурации:

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.Configure<TransientFaultHandlingOptions>(
    builder.Configuration.GetSection(
        key: nameof(TransientFaultHandlingOptions)));

В builder предыдущем примере используется экземпляр HostApplicationBuilder.

Совет

Параметр key содержит имя раздела конфигурации для поиска. Это значение не обязано совпадать с именем типа, который его представляет. Например, у вас может существовать раздел с именем "FaultHandling", представленный классом TransientFaultHandlingOptions. В этом случае вы будете передавать "FaultHandling" в функцию GetSection. Оператор nameof используется для удобства, когда имя именованного раздела совпадает с типом, которому он соответствует.

С помощью приведенного выше кода следующий код считывает параметры расположения:

using Microsoft.Extensions.Options;

namespace ConsoleJson.Example;

public sealed class ExampleService(IOptions<TransientFaultHandlingOptions> options)
{
    private readonly TransientFaultHandlingOptions _options = options.Value;

    public void DisplayValues()
    {
        Console.WriteLine($"TransientFaultHandlingOptions.Enabled={_options.Enabled}");
        Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={_options.AutoRetryDelay}");
    }
}

В приведенном выше коде изменения в файле конфигурации JSON, внесенные после запуска приложения, не считываются. Чтобы прочитать изменения после запуска приложения, используйте IOptionsSnapshot или IOptionsMonitor для отслеживания изменений по мере их возникновения и реагирования соответствующим образом.

Интерфейсы параметров

IOptions<TOptions>:

IOptionsSnapshot<TOptions>:

IOptionsMonitor<TOptions>:

Интерфейс IOptionsFactory<TOptions> отвечает за создание экземпляров параметров. Он имеет единственный метод Create. При реализации по умолчанию принимаются все зарегистрированные интерфейсы IConfigureOptions<TOptions> и IPostConfigureOptions<TOptions>. Также сначала выполняются все основные настройки, а затем действия после конфигурации. Она различает интерфейсы IConfigureNamedOptions<TOptions> и IConfigureOptions<TOptions> и вызывает только соответствующий интерфейс.

IOptionsMonitorCache<TOptions> используется интерфейсом IOptionsMonitor<TOptions> для записи экземпляров TOptions в кэш. IOptionsMonitorCache<TOptions> делает экземпляры параметров в мониторе недействительными, что приводит к повторному вычислению значений (TryRemove). Значения можно также вводить вручную с помощью TryAdd. Метод Clear используется, если необходимо повторно создать все именованные экземпляры по требованию.

IOptionsChangeTokenSource<TOptions> используется для получения IChangeToken отслеживания изменений в базовом TOptions экземпляре. Дополнительные сведения о примитивах маркеров изменений см. в разделе "Уведомления об изменениях".

Преимущества интерфейсов параметров

Использование универсального типа оболочки позволяет отвязать время существования параметра от контейнера внедрения зависимостей. Интерфейс IOptions<TOptions>.Value предоставляет уровень абстракции для типа параметров, включая универсальные ограничения. Это обеспечивает следующие преимущества:

  • Вычисление экземпляра конфигурации T выполняется только при доступе к IOptions<TOptions>.Value, а не при его внедрении. Это важно, так как вы можете использовать параметр T из разных мест и выбирать семантику времени существования, не изменяя данные о T.
  • При регистрации параметров с типом T вам не придется явно регистрировать тип T. Это очень удобно при создании библиотеки с простыми параметрами по умолчанию, если вы не хотите вынуждать вызывающую сторону регистрировать параметры в контейнере внедрения зависимостей с конкретным временем существования.
  • С точки зрения API это позволяет создавать ограничения типа T (в нашем примере параметр T ограничен ссылочным типом).

Использование IOptionsSnapshot для чтения обновленных данных

При использовании IOptionsSnapshot<TOptions> параметры вычисляются один раз на каждый запрос при обращении к ним и кэшируются на все время существования запроса. Изменения конфигурации считываются после запуска приложения при использовании поставщиков конфигурации, поддерживающих чтение обновленных значений конфигурации.

Разница между IOptionsMonitor и IOptionsSnapshot:

  • IOptionsMonitor — это одноэлементная служба, которая получает текущие значения параметров в любое время, что особенно полезно в одноэлементных зависимостях.
  • IOptionsSnapshot — это служба с заданной областью действия, предоставляющая моментальный снимок параметров на момент создания объекта IOptionsSnapshot<T>. Моментальные снимки параметров предназначены для использования с временными зависимостями и зависимостями с заданной областью действия.

В приведенном ниже коде используется IOptionsSnapshot<TOptions>.

using Microsoft.Extensions.Options;

namespace ConsoleJson.Example;

public sealed class ScopedService(IOptionsSnapshot<TransientFaultHandlingOptions> options)
{
    private readonly TransientFaultHandlingOptions _options = options.Value;

    public void DisplayValues()
    {
        Console.WriteLine($"TransientFaultHandlingOptions.Enabled={_options.Enabled}");
        Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={_options.AutoRetryDelay}");
    }
}

Следующий код регистрирует экземпляр конфигурации, к которому привязывается TransientFaultHandlingOptions.

builder.Services
    .Configure<TransientFaultHandlingOptions>(
        configurationRoot.GetSection(
            nameof(TransientFaultHandlingOptions)));

В приведенном выше коде Configure<TOptions> метод используется для регистрации экземпляра конфигурации, который TOptions привязывается к и обновляет параметры при изменении конфигурации.

IOptionsMonitor

Чтобы использовать монитор параметров, объекты параметров настраиваются таким же образом из раздела конфигурации.

builder.Services
    .Configure<TransientFaultHandlingOptions>(
        configurationRoot.GetSection(
            nameof(TransientFaultHandlingOptions)));

В следующем примере используется IOptionsMonitor<TOptions>.

using Microsoft.Extensions.Options;

namespace ConsoleJson.Example;

public sealed class MonitorService(IOptionsMonitor<TransientFaultHandlingOptions> monitor)
{
    public void DisplayValues()
    {
        TransientFaultHandlingOptions options = monitor.CurrentValue;

        Console.WriteLine($"TransientFaultHandlingOptions.Enabled={options.Enabled}");
        Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={options.AutoRetryDelay}");
    }
}

В приведенном выше коде изменения в файле конфигурации JSON, внесенные после запуска приложения, считываются.

Совет

Некоторые файловые системы, такие как контейнеры Docker и сетевые папки, не могут надежно отправлять уведомления об изменениях. При использовании интерфейса IOptionsMonitor<TOptions> в этих средах задайте переменной среды DOTNET_USE_POLLING_FILE_WATCHER значение 1 или true, чтобы выполнять опрос на наличие изменений в файловой системе. Интервал этого опроса составляет четыре секунды и не подлежит настройке.

Дополнительные сведения о контейнерах Docker см. в статье о контейнеризации приложений .NET.

Поддержка именованных параметров с использованием IConfigureNamedOptions

Именованные параметры:

  • полезны, если несколько разделов конфигурации привязываются к одним и тем же свойствам;
  • используются с учетом регистра.

Рассмотрите следующий файл appsettings.json:

{
  "Features": {
    "Personalize": {
      "Enabled": true,
      "ApiKey": "aGEgaGEgeW91IHRob3VnaHQgdGhhdCB3YXMgcmVhbGx5IHNvbWV0aGluZw=="
    },
    "WeatherStation": {
      "Enabled": true,
      "ApiKey": "QXJlIHlvdSBhdHRlbXB0aW5nIHRvIGhhY2sgdXM/"
    }
  }
}

Вместо создания двух классов для привязки Features:Personalize и Features:WeatherStation для каждого раздела используется следующий класс:

public class Features
{
    public const string Personalize = nameof(Personalize);
    public const string WeatherStation = nameof(WeatherStation);

    public bool Enabled { get; set; }
    public string ApiKey { get; set; }
}

Следующий код служит для настройки именованных параметров:

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

// Omitted for brevity...

builder.Services.Configure<Features>(
    Features.Personalize,
    builder.Configuration.GetSection("Features:Personalize"));

builder.Services.Configure<Features>(
    Features.WeatherStation,
    builder.Configuration.GetSection("Features:WeatherStation"));

Следующий код отображает именованные параметры:

public class sealed Service
{
    private readonly Features _personalizeFeature;
    private readonly Features _weatherStationFeature;

    public Service(IOptionsSnapshot<Features> namedOptionsAccessor)
    {
        _personalizeFeature = namedOptionsAccessor.Get(Features.Personalize);
        _weatherStationFeature = namedOptionsAccessor.Get(Features.WeatherStation);
    }
}

Все параметры являются именованными экземплярами. Экземпляры IConfigureOptions<TOptions> считаются нацеленными на экземпляр Options.DefaultName, который имеет значение string.Empty. Интерфейс IConfigureNamedOptions<TOptions> также реализует интерфейс IConfigureOptions<TOptions>. Реализация IOptionsFactory<TOptions> по умолчанию содержит логику для надлежащего использования каждого экземпляра. Именованный параметр null предназначен для всех именованных экземпляров, а не для какого-то определенного. ConfigureAll и PostConfigureAll следуют этому соглашению.

API OptionsBuilder

OptionsBuilder<TOptions> используется для настройки экземпляров TOptions. OptionsBuilder упрощает создание именованных параметров, так как он является единственным параметром для первоначального вызова AddOptions<TOptions>(string optionsName) и не должен появляться во всех последующих вызовах. Проверка параметров и перегрузки ConfigureOptions, принимающие зависимости службы, доступны только через OptionsBuilder.

OptionsBuilder используется в разделе Проверка параметров.

Использование служб внедрения зависимостей для настройки параметров

Использовать службы, доступные в результате внедрения зависимостей при настройке параметров, можно двумя способами.

  • Передайте делегат конфигурации методу Configure в OptionsBuilder<TOptions>. OptionsBuilder<TOptions> предоставляет перегрузки метода Configure, которые позволяют использовать до пяти служб для настройки параметров:

    builder.Services
        .AddOptions<MyOptions>("optionalName")
        .Configure<ExampleService, ScopedService, MonitorService>(
            (options, es, ss, ms) =>
                options.Property = DoSomethingWith(es, ss, ms));
    
  • Создайте тип, реализующий IConfigureOptions<TOptions> или IConfigureNamedOptions<TOptions>, и зарегистрируйте этот тип как службу.

Рекомендуем передать делегат конфигурации методу Configure, так как создать службу сложнее. Создание типа эквивалентно тому, что делает платформа при вызове метода Configure. При вызове метода Configure регистрируется временный универсальный интерфейс IConfigureNamedOptions<TOptions>, имеющий конструктор, который принимает указанные универсальные типы службы.

Проверка параметров

Проверка параметров позволяет проверять значения параметров.

Рассмотрите следующий файл appsettings.json:

{
  "MyCustomSettingsSection": {
    "SiteTitle": "Amazing docs from Awesome people!",
    "Scale": 10,
    "VerbosityLevel": 32
  }
}

Следующий класс привязывается к разделу конфигурации "MyCustomSettingsSection" и применяет несколько правил DataAnnotations:

using System.ComponentModel.DataAnnotations;

namespace ConsoleJson.Example;

public sealed class SettingsOptions
{
    public const string ConfigurationSectionName = "MyCustomSettingsSection";

    [Required]
    [RegularExpression(@"^[a-zA-Z''-'\s]{1,40}$")]
    public required string SiteTitle { get; set; }

    [Required]
    [Range(0, 1_000,
        ErrorMessage = "Value for {0} must be between {1} and {2}.")]
    public required int Scale { get; set; }

    [Required]
    public required int VerbosityLevel { get; set; }
}

В предыдущем классе SettingsOptions свойство ConfigurationSectionName содержит имя раздела конфигурации, к которому необходимо выполнить привязку. В этом сценарии объект параметров предоставляет имя раздела конфигурации.

Совет

Имя раздела конфигурации не зависит от объекта конфигурации, к которому он привязан. Иными словами, раздел конфигурации с именем "FooBarOptions" может быть привязан к объекту параметров с именем ZedOptions. Несмотря на то что им можно присвоить одинаковые имена, это не обязательно и может привести к конфликтам имен.

Следующий код:

  • вызывает AddOptions, чтобы получить класс OptionsBuilder<TOptions>, который привязывается к классу SettingsOptions;
  • вызывает ValidateDataAnnotations для включения проверки с помощью DataAnnotations.
builder.Services
    .AddOptions<SettingsOptions>()
    .Bind(Configuration.GetSection(SettingsOptions.ConfigurationSectionName))
    .ValidateDataAnnotations();

Метод расширения ValidateDataAnnotations определен в пакете NuGet Microsoft.Extensions.Options.DataAnnotations.

В следующем коде отображаются значения конфигурации или отчеты об ошибках проверки:

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace ConsoleJson.Example;

public sealed class ValidationService
{
    private readonly ILogger<ValidationService> _logger;
    private readonly IOptions<SettingsOptions> _config;

    public ValidationService(
        ILogger<ValidationService> logger,
        IOptions<SettingsOptions> config)
    {
        _config = config;
        _logger = logger;

        try
        {
            SettingsOptions options = _config.Value;
        }
        catch (OptionsValidationException ex)
        {
            foreach (string failure in ex.Failures)
            {
                _logger.LogError("Validation error: {FailureMessage}", failure);
            }
        }
    }
}

Следующий код применяет более сложное правило проверки с использованием делегата:

builder.Services
    .AddOptions<SettingsOptions>()
    .Bind(Configuration.GetSection(SettingsOptions.ConfigurationSectionName))
    .ValidateDataAnnotations()
    .Validate(config =>
    {
        if (config.Scale != 0)
        {
            return config.VerbosityLevel > config.Scale;
        }

        return true;
    }, "VerbosityLevel must be > than Scale.");

Проверка выполняется во время выполнения, но ее можно настроить при запуске, вместо этого прицепозовав вызов:ValidateOnStart

builder.Services
    .AddOptions<SettingsOptions>()
    .Bind(Configuration.GetSection(SettingsOptions.ConfigurationSectionName))
    .ValidateDataAnnotations()
    .Validate(config =>
    {
        if (config.Scale != 0)
        {
            return config.VerbosityLevel > config.Scale;
        }

        return true;
    }, "VerbosityLevel must be > than Scale.")
    .ValidateOnStart();

Начиная с .NET 8, можно использовать альтернативный API, AddOptionsWithValidateOnStart<TOptions>(IServiceCollection, String)который включает проверку при запуске для определенного типа параметров:

builder.Services
    .AddOptionsWithValidateOnStart<SettingsOptions>()
    .Bind(Configuration.GetSection(SettingsOptions.ConfigurationSectionName))
    .ValidateDataAnnotations()
    .Validate(config =>
    {
        if (config.Scale != 0)
        {
            return config.VerbosityLevel > config.Scale;
        }

        return true;
    }, "VerbosityLevel must be > than Scale.");

IValidateOptions для сложной проверки

Следующий класс реализует IValidateOptions<TOptions>:

using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;

namespace ConsoleJson.Example;

sealed partial class ValidateSettingsOptions(
    IConfiguration config)
    : IValidateOptions<SettingsOptions>
{
    public SettingsOptions? Settings { get; private set; } =
        config.GetSection(SettingsOptions.ConfigurationSectionName)
              .Get<SettingsOptions>();

    public ValidateOptionsResult Validate(string? name, SettingsOptions options)
    {
        StringBuilder? failure = null;
    
        if (!ValidationRegex().IsMatch(options.SiteTitle))
        {
            (failure ??= new()).AppendLine($"{options.SiteTitle} doesn't match RegEx");
        }

        if (options.Scale is < 0 or > 1_000)
        {
            (failure ??= new()).AppendLine($"{options.Scale} isn't within Range 0 - 1000");
        }

        if (Settings is { Scale: 0 } && Settings.VerbosityLevel <= Settings.Scale)
        {
            (failure ??= new()).AppendLine("VerbosityLevel must be > than Scale.");
        }

        return failure is not null
            ? ValidateOptionsResult.Fail(failure.ToString())
            : ValidateOptionsResult.Success;
    }

    [GeneratedRegex("^[a-zA-Z''-'\\s]{1,40}$")]
    private static partial Regex ValidationRegex();
}

IValidateOptions позволяет переместить код проверки в класс.

Примечание.

Этот пример кода использует пакет NuGet Microsoft.Extensions.Configuration.Json.

С помощью предыдущего кода проверка включена при настройке служб со следующим кодом:

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

// Omitted for brevity...

builder.Services.Configure<SettingsOptions>(
    builder.Configuration.GetSection(
        SettingsOptions.ConfigurationSectionName));

builder.Services.TryAddEnumerable(
    ServiceDescriptor.Singleton
        <IValidateOptions<SettingsOptions>, ValidateSettingsOptions>());

Пост-конфигурация параметров

Задайте пост-конфигурацию с помощью IPostConfigureOptions<TOptions>. Процессы после завершения настройки выполняются после всех конфигураций IConfigureOptions<TOptions>. Это может быть полезно в сценариях, когда требуется переопределять конфигурацию.

builder.Services.PostConfigure<CustomOptions>(customOptions =>
{
    customOptions.Option1 = "post_configured_option1_value";
});

Для последующей настройки именованных параметров доступен метод PostConfigure.

builder.Services.PostConfigure<CustomOptions>("named_options_1", customOptions =>
{
    customOptions.Option1 = "post_configured_option1_value";
});

Для последующей настройки всех экземпляров конфигурации служит метод PostConfigureAll.

builder.Services.PostConfigureAll<CustomOptions>(customOptions =>
{
    customOptions.Option1 = "post_configured_option1_value";
});

См. также