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

Примечание.

Это не последняя версия этой статьи. В текущем выпуске см . версию .NET 8 этой статьи.

Внимание

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

В текущем выпуске см . версию .NET 8 этой статьи.

Рик Андерсон.

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

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

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

В этой статье приводятся сведения о шаблоне параметров в ASP.NET Core. Сведения об использовании шаблона параметров в консольных приложениях см. в разделе Шаблон параметров в .NET.

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

Предпочтительный способ чтения связанных значений конфигурации — использование шаблона параметров. Например, чтобы считать следующие значения конфигурации:

  "Position": {
    "Title": "Editor",
    "Name": "Joe Smith"
  }

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

public class PositionOptions
{
    public const string Position = "Position";

    public string Title { get; set; } = String.Empty;
    public string Name { get; set; } = String.Empty;
}

Класс параметров:

  • Должен быть не абстрактным.
  • Имеет общедоступные свойства записи для чтения и записи типа, имеющего соответствующие элементы в конфигурации, привязаны.
  • Имеет свойства чтения и записи, связанные с соответствующими записями в конфигурации.
  • Не привязаны его поля. В приведенном выше коде свойство Position не привязано. Поле Position используется так, что строку "Position" не требуется жестко кодировать в приложении при привязке класса к поставщику конфигурации.

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

  • Вызывает ConfigurationBinder.Bind для привязки класса PositionOptions к разделу Position.
  • Отображает данные конфигурации Position.
public class Test22Model : PageModel
{
    private readonly IConfiguration Configuration;

    public Test22Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {
        var positionOptions = new PositionOptions();
        Configuration.GetSection(PositionOptions.Position).Bind(positionOptions);

        return Content($"Title: {positionOptions.Title} \n" +
                       $"Name: {positionOptions.Name}");
    }
}

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

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

public class Test21Model : PageModel
{
    private readonly IConfiguration Configuration;
    public PositionOptions? positionOptions { get; private set; }

    public Test21Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {            
        positionOptions = Configuration.GetSection(PositionOptions.Position)
                                                     .Get<PositionOptions>();

        return Content($"Title: {positionOptions.Title} \n" +
                       $"Name: {positionOptions.Name}");
    }
}

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

Bind также позволяет сцепить абстрактный класс. Рассмотрим следующий код, использующий абстрактный класс SomethingWithAName:

namespace ConfigSample.Options;

public abstract class SomethingWithAName
{
    public abstract string? Name { get; set; }
}

public class NameTitleOptions(int age) : SomethingWithAName
{
    public const string NameTitle = "NameTitle";

    public override string? Name { get; set; }
    public string Title { get; set; } = string.Empty;

    public int Age { get; set; } = age;
}

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

public class Test33Model : PageModel
{
    private readonly IConfiguration Configuration;

    public Test33Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {
        var nameTitleOptions = new NameTitleOptions(22);
        Configuration.GetSection(NameTitleOptions.NameTitle).Bind(nameTitleOptions);

        return Content($"Title: {nameTitleOptions.Title} \n" +
                       $"Name: {nameTitleOptions.Name}  \n" +
                       $"Age: {nameTitleOptions.Age}"
                       );
    }
}

Bind Вызовы менее строги, чем вызовы:Get<>

  • Bind позволяет сцепить абстрактное.
  • Get<> должен создать сам экземпляр.

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

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

using ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<PositionOptions>(
    builder.Configuration.GetSection(PositionOptions.Position));

var app = builder.Build();

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

public class Test2Model : PageModel
{
    private readonly PositionOptions _options;

    public Test2Model(IOptions<PositionOptions> options)
    {
        _options = options.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Title: {_options.Title} \n" +
                       $"Name: {_options.Name}");
    }
}

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

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

IOptions<TOptions>:

IOptionsSnapshot<TOptions>:

IOptionsMonitor<TOptions>:

Сценарии пост-конфигурации позволяют задать или изменить параметры после выполнения всех действий настройки с помощью IConfigureOptions<TOptions>.

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

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

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

Использование среды IOptionsSnapshot<TOptions>:

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

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

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

public class TestSnapModel : PageModel
{
    private readonly MyOptions _snapshotOptions;

    public TestSnapModel(IOptionsSnapshot<MyOptions> snapshotOptionsAccessor)
    {
        _snapshotOptions = snapshotOptionsAccessor.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Option1: {_snapshotOptions.Option1} \n" +
                       $"Option2: {_snapshotOptions.Option2}");
    }
}

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

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<MyOptions>(
    builder.Configuration.GetSection("MyOptions"));

var app = builder.Build();

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

IOptionsMonitor

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

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<MyOptions>(
    builder.Configuration.GetSection("MyOptions"));

var app = builder.Build();

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

public class TestMonitorModel : PageModel
{
    private readonly IOptionsMonitor<MyOptions> _optionsDelegate;

    public TestMonitorModel(IOptionsMonitor<MyOptions> optionsDelegate )
    {
        _optionsDelegate = optionsDelegate;
    }

    public ContentResult OnGet()
    {
        return Content($"Option1: {_optionsDelegate.CurrentValue.Option1} \n" +
                       $"Option2: {_optionsDelegate.CurrentValue.Option2}");
    }
}

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

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

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

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

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

{
  "TopItem": {
    "Month": {
      "Name": "Green Widget",
      "Model": "GW46"
    },
    "Year": {
      "Name": "Orange Gadget",
      "Model": "OG35"
    }
  }
}

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

public class TopItemSettings
{
    public const string Month = "Month";
    public const string Year = "Year";

    public string Name { get; set; } = string.Empty;
    public string Model { get; set; } = string.Empty;
}

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

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
    builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
    builder.Configuration.GetSection("TopItem:Year"));

var app = builder.Build();

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

public class TestNOModel : PageModel
{
    private readonly TopItemSettings _monthTopItem;
    private readonly TopItemSettings _yearTopItem;

    public TestNOModel(IOptionsSnapshot<TopItemSettings> namedOptionsAccessor)
    {
        _monthTopItem = namedOptionsAccessor.Get(TopItemSettings.Month);
        _yearTopItem = namedOptionsAccessor.Get(TopItemSettings.Year);
    }

    public ContentResult OnGet()
    {
        return Content($"Month:Name {_monthTopItem.Name} \n" +
                       $"Month:Model {_monthTopItem.Model} \n\n" +
                       $"Year:Name {_yearTopItem.Name} \n" +
                       $"Year:Model {_yearTopItem.Model} \n"   );
    }
}

Все параметры являются именованными экземплярами. Экземпляры 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 используется в разделе Проверка параметров.

Дополнительные сведения о добавлении пользовательского репозитория см. в статье Использование AddOptions для настройки пользовательского репозитория.

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

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

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

    builder.Services.AddOptions<MyOptions>("optionalName")
        .Configure<Service1, Service2, Service3, Service4, Service5>(
            (o, s, s2, s3, s4, s5) => 
                o.Property = DoSomethingWith(s, s2, s3, s4, s5));
    
  • Создайте тип, реализующий IConfigureOptions<TOptions> или IConfigureNamedOptions<TOptions>, и зарегистрируйте этот тип как службу.

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

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

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

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

{
  "MyConfig": {
    "Key1": "My Key One",
    "Key2": 10,
    "Key3": 32
  }
}

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

public class MyConfigOptions
{
    public const string MyConfig = "MyConfig";

    [RegularExpression(@"^[a-zA-Z''-'\s]{1,40}$")]
    public string Key1 { get; set; }
    [Range(0, 1000,
        ErrorMessage = "Value for {0} must be between {1} and {2}.")]
    public int Key2 { get; set; }
    public int Key3 { get; set; }
}

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

  • вызывает AddOptions, чтобы получить класс OptionsBuilder<TOptions>, который привязывается к классу MyConfigOptions;
  • вызывает ValidateDataAnnotations для включения проверки с помощью DataAnnotations.
using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
            .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
            .ValidateDataAnnotations();

var app = builder.Build();

Метод расширения ValidateDataAnnotations определен в пакете NuGet Microsoft.Extensions.Options.DataAnnotations. Для веб-приложений, использующих пакет SDK Microsoft.NET.Sdk.Web, ссылка на этот пакет указывается неявным образом из общей платформы.

Следующий код отображает значения конфигурации или ошибки проверки:

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;
    private readonly IOptions<MyConfigOptions> _config;

    public HomeController(IOptions<MyConfigOptions> config,
                          ILogger<HomeController> logger)
    {
        _config = config;
        _logger = logger;

        try
        {
            var configValue = _config.Value;

        }
        catch (OptionsValidationException ex)
        {
            foreach (var failure in ex.Failures)
            {
                _logger.LogError(failure);
            }
        }
    }

    public ContentResult Index()
    {
        string msg;
        try
        {
            msg = $"Key1: {_config.Value.Key1} \n" +
                  $"Key2: {_config.Value.Key2} \n" +
                  $"Key3: {_config.Value.Key3}";
        }
        catch (OptionsValidationException optValEx)
        {
            return Content(optValEx.Message);
        }
        return Content(msg);
    }

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

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
            .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
            .ValidateDataAnnotations()
        .Validate(config =>
        {
            if (config.Key2 != 0)
            {
                return config.Key3 > config.Key2;
            }

            return true;
        }, "Key3 must be > than Key2.");   // Failure message.

var app = builder.Build();

IValidateOptions<TOptions> и IValidatableObject.

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

public class MyConfigValidation : IValidateOptions<MyConfigOptions>
{
    public MyConfigOptions _config { get; private set; }

    public  MyConfigValidation(IConfiguration config)
    {
        _config = config.GetSection(MyConfigOptions.MyConfig)
            .Get<MyConfigOptions>();
    }

    public ValidateOptionsResult Validate(string name, MyConfigOptions options)
    {
        string? vor = null;
        var rx = new Regex(@"^[a-zA-Z''-'\s]{1,40}$");
        var match = rx.Match(options.Key1!);

        if (string.IsNullOrEmpty(match.Value))
        {
            vor = $"{options.Key1} doesn't match RegEx \n";
        }

        if ( options.Key2 < 0 || options.Key2 > 1000)
        {
            vor = $"{options.Key2} doesn't match Range 0 - 1000 \n";
        }

        if (_config.Key2 != default)
        {
            if(_config.Key3 <= _config.Key2)
            {
                vor +=  "Key3 must be > than Key2.";
            }
        }

        if (vor != null)
        {
            return ValidateOptionsResult.Fail(vor);
        }

        return ValidateOptionsResult.Success;
    }
}

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

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

using Microsoft.Extensions.Options;
using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.Configure<MyConfigOptions>(builder.Configuration.GetSection(
                                        MyConfigOptions.MyConfig));

builder.Services.AddSingleton<IValidateOptions
                              <MyConfigOptions>, MyConfigValidation>();

var app = builder.Build();

Проверка параметров также поддерживает IValidatableObject. Для проверки на уровне класса внутри самого класса:

  • Реализуйте интерфейс IValidatableObject и его метод Validate внутри класса.
  • Вызовите ValidateDataAnnotations в Program.cs.

ValidateOnStart

Проверка параметров выполняется при первом создании реализации IOptions<TOptions>, IOptionsSnapshot<TOptions> или IOptionsMonitor<TOptions>. Для безотложной проверки при запуске приложения вызовите ValidateOnStart в Program.cs:

builder.Services.AddOptions<MyConfigOptions>()
    .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
    .ValidateDataAnnotations()
    .ValidateOnStart();

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

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

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
                .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));

builder.Services.PostConfigure<MyConfigOptions>(myOptions =>
{
    myOptions.Key1 = "post_configured_key1_value";
});

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

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
    builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
    builder.Configuration.GetSection("TopItem:Year"));

builder.Services.PostConfigure<TopItemSettings>("Month", myOptions =>
{
    myOptions.Name = "post_configured_name_value";
    myOptions.Model = "post_configured_model_value";
});

var app = builder.Build();

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

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
                .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));

builder.Services.PostConfigureAll<MyConfigOptions>(myOptions =>
{
    myOptions.Key1 = "post_configured_key1_value";
});

Параметры доступа в Program.cs

Чтобы получить доступ к IOptions<TOptions> или IOptionsMonitor<TOptions> в Program.cs, вызовите GetRequiredService в WebApplication.Services:

var app = builder.Build();

var option1 = app.Services.GetRequiredService<IOptionsMonitor<MyOptions>>()
    .CurrentValue.Option1;

Дополнительные ресурсы

Авторы: Кирк Ларкин (Kirk Larkin) и Рик Андерсон (Rick Anderson).

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

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

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

В этой статье приводятся сведения о шаблоне параметров в ASP.NET Core. Сведения об использовании шаблона параметров в консольных приложениях см. в разделе Шаблон параметров в .NET.

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

Предпочтительный способ чтения связанных значений конфигурации — использование шаблона параметров. Например, чтобы считать следующие значения конфигурации:

  "Position": {
    "Title": "Editor",
    "Name": "Joe Smith"
  }

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

public class PositionOptions
{
    public const string Position = "Position";

    public string Title { get; set; } = String.Empty;
    public string Name { get; set; } = String.Empty;
}

Класс параметров:

  • Должен быть неабстрактным с открытым конструктором без параметров.
  • Все открытые свойства чтения и записи типа привязаны.
  • Поля не привязаны. В приведенном выше коде свойство Position не привязано. Поле Position используется так, что строку "Position" не требуется жестко кодировать в приложении при привязке класса к поставщику конфигурации.

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

  • Вызывает ConfigurationBinder.Bind для привязки класса PositionOptions к разделу Position.
  • Отображает данные конфигурации Position.
public class Test22Model : PageModel
{
    private readonly IConfiguration Configuration;

    public Test22Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {
        var positionOptions = new PositionOptions();
        Configuration.GetSection(PositionOptions.Position).Bind(positionOptions);

        return Content($"Title: {positionOptions.Title} \n" +
                       $"Name: {positionOptions.Name}");
    }
}

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

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

public class Test21Model : PageModel
{
    private readonly IConfiguration Configuration;
    public PositionOptions? positionOptions { get; private set; }

    public Test21Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {            
        positionOptions = Configuration.GetSection(PositionOptions.Position)
                                                     .Get<PositionOptions>();

        return Content($"Title: {positionOptions.Title} \n" +
                       $"Name: {positionOptions.Name}");
    }
}

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

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

using ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<PositionOptions>(
    builder.Configuration.GetSection(PositionOptions.Position));

var app = builder.Build();

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

public class Test2Model : PageModel
{
    private readonly PositionOptions _options;

    public Test2Model(IOptions<PositionOptions> options)
    {
        _options = options.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Title: {_options.Title} \n" +
                       $"Name: {_options.Name}");
    }
}

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

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

IOptions<TOptions>:

IOptionsSnapshot<TOptions>:

IOptionsMonitor<TOptions>:

Сценарии пост-конфигурации позволяют задать или изменить параметры после выполнения всех действий настройки с помощью IConfigureOptions<TOptions>.

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

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

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

Использование среды IOptionsSnapshot<TOptions>:

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

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

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

public class TestSnapModel : PageModel
{
    private readonly MyOptions _snapshotOptions;

    public TestSnapModel(IOptionsSnapshot<MyOptions> snapshotOptionsAccessor)
    {
        _snapshotOptions = snapshotOptionsAccessor.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Option1: {_snapshotOptions.Option1} \n" +
                       $"Option2: {_snapshotOptions.Option2}");
    }
}

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

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<MyOptions>(
    builder.Configuration.GetSection("MyOptions"));

var app = builder.Build();

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

IOptionsMonitor

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

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<MyOptions>(
    builder.Configuration.GetSection("MyOptions"));

var app = builder.Build();

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

public class TestMonitorModel : PageModel
{
    private readonly IOptionsMonitor<MyOptions> _optionsDelegate;

    public TestMonitorModel(IOptionsMonitor<MyOptions> optionsDelegate )
    {
        _optionsDelegate = optionsDelegate;
    }

    public ContentResult OnGet()
    {
        return Content($"Option1: {_optionsDelegate.CurrentValue.Option1} \n" +
                       $"Option2: {_optionsDelegate.CurrentValue.Option2}");
    }
}

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

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

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

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

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

{
  "TopItem": {
    "Month": {
      "Name": "Green Widget",
      "Model": "GW46"
    },
    "Year": {
      "Name": "Orange Gadget",
      "Model": "OG35"
    }
  }
}

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

public class TopItemSettings
{
    public const string Month = "Month";
    public const string Year = "Year";

    public string Name { get; set; } = string.Empty;
    public string Model { get; set; } = string.Empty;
}

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

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
    builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
    builder.Configuration.GetSection("TopItem:Year"));

var app = builder.Build();

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

public class TestNOModel : PageModel
{
    private readonly TopItemSettings _monthTopItem;
    private readonly TopItemSettings _yearTopItem;

    public TestNOModel(IOptionsSnapshot<TopItemSettings> namedOptionsAccessor)
    {
        _monthTopItem = namedOptionsAccessor.Get(TopItemSettings.Month);
        _yearTopItem = namedOptionsAccessor.Get(TopItemSettings.Year);
    }

    public ContentResult OnGet()
    {
        return Content($"Month:Name {_monthTopItem.Name} \n" +
                       $"Month:Model {_monthTopItem.Model} \n\n" +
                       $"Year:Name {_yearTopItem.Name} \n" +
                       $"Year:Model {_yearTopItem.Model} \n"   );
    }
}

Все параметры являются именованными экземплярами. Экземпляры 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 используется в разделе Проверка параметров.

Дополнительные сведения о добавлении пользовательского репозитория см. в статье Использование AddOptions для настройки пользовательского репозитория.

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

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

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

    builder.Services.AddOptions<MyOptions>("optionalName")
        .Configure<Service1, Service2, Service3, Service4, Service5>(
            (o, s, s2, s3, s4, s5) => 
                o.Property = DoSomethingWith(s, s2, s3, s4, s5));
    
  • Создайте тип, реализующий IConfigureOptions<TOptions> или IConfigureNamedOptions<TOptions>, и зарегистрируйте этот тип как службу.

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

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

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

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

{
  "MyConfig": {
    "Key1": "My Key One",
    "Key2": 10,
    "Key3": 32
  }
}

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

public class MyConfigOptions
{
    public const string MyConfig = "MyConfig";

    [RegularExpression(@"^[a-zA-Z''-'\s]{1,40}$")]
    public string Key1 { get; set; }
    [Range(0, 1000,
        ErrorMessage = "Value for {0} must be between {1} and {2}.")]
    public int Key2 { get; set; }
    public int Key3 { get; set; }
}

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

  • вызывает AddOptions, чтобы получить класс OptionsBuilder<TOptions>, который привязывается к классу MyConfigOptions;
  • вызывает ValidateDataAnnotations для включения проверки с помощью DataAnnotations.
using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
            .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
            .ValidateDataAnnotations();

var app = builder.Build();

Метод расширения ValidateDataAnnotations определен в пакете NuGet Microsoft.Extensions.Options.DataAnnotations. Для веб-приложений, использующих пакет SDK Microsoft.NET.Sdk.Web, ссылка на этот пакет указывается неявным образом из общей платформы.

Следующий код отображает значения конфигурации или ошибки проверки:

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;
    private readonly IOptions<MyConfigOptions> _config;

    public HomeController(IOptions<MyConfigOptions> config,
                          ILogger<HomeController> logger)
    {
        _config = config;
        _logger = logger;

        try
        {
            var configValue = _config.Value;

        }
        catch (OptionsValidationException ex)
        {
            foreach (var failure in ex.Failures)
            {
                _logger.LogError(failure);
            }
        }
    }

    public ContentResult Index()
    {
        string msg;
        try
        {
            msg = $"Key1: {_config.Value.Key1} \n" +
                  $"Key2: {_config.Value.Key2} \n" +
                  $"Key3: {_config.Value.Key3}";
        }
        catch (OptionsValidationException optValEx)
        {
            return Content(optValEx.Message);
        }
        return Content(msg);
    }

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

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
            .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
            .ValidateDataAnnotations()
        .Validate(config =>
        {
            if (config.Key2 != 0)
            {
                return config.Key3 > config.Key2;
            }

            return true;
        }, "Key3 must be > than Key2.");   // Failure message.

var app = builder.Build();

IValidateOptions<TOptions> и IValidatableObject.

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

public class MyConfigValidation : IValidateOptions<MyConfigOptions>
{
    public MyConfigOptions _config { get; private set; }

    public  MyConfigValidation(IConfiguration config)
    {
        _config = config.GetSection(MyConfigOptions.MyConfig)
            .Get<MyConfigOptions>();
    }

    public ValidateOptionsResult Validate(string name, MyConfigOptions options)
    {
        string? vor = null;
        var rx = new Regex(@"^[a-zA-Z''-'\s]{1,40}$");
        var match = rx.Match(options.Key1!);

        if (string.IsNullOrEmpty(match.Value))
        {
            vor = $"{options.Key1} doesn't match RegEx \n";
        }

        if ( options.Key2 < 0 || options.Key2 > 1000)
        {
            vor = $"{options.Key2} doesn't match Range 0 - 1000 \n";
        }

        if (_config.Key2 != default)
        {
            if(_config.Key3 <= _config.Key2)
            {
                vor +=  "Key3 must be > than Key2.";
            }
        }

        if (vor != null)
        {
            return ValidateOptionsResult.Fail(vor);
        }

        return ValidateOptionsResult.Success;
    }
}

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

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

using Microsoft.Extensions.Options;
using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.Configure<MyConfigOptions>(builder.Configuration.GetSection(
                                        MyConfigOptions.MyConfig));

builder.Services.AddSingleton<IValidateOptions
                              <MyConfigOptions>, MyConfigValidation>();

var app = builder.Build();

Проверка параметров также поддерживает IValidatableObject. Для проверки на уровне класса внутри самого класса:

  • Реализуйте интерфейс IValidatableObject и его метод Validate внутри класса.
  • Вызовите ValidateDataAnnotations в Program.cs.

ValidateOnStart

Проверка параметров выполняется при первом создании реализации IOptions<TOptions>, IOptionsSnapshot<TOptions> или IOptionsMonitor<TOptions>. Для безотложной проверки при запуске приложения вызовите ValidateOnStart в Program.cs:

builder.Services.AddOptions<MyConfigOptions>()
    .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
    .ValidateDataAnnotations()
    .ValidateOnStart();

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

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

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
                .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));

builder.Services.PostConfigure<MyConfigOptions>(myOptions =>
{
    myOptions.Key1 = "post_configured_key1_value";
});

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

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
    builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
    builder.Configuration.GetSection("TopItem:Year"));

builder.Services.PostConfigure<TopItemSettings>("Month", myOptions =>
{
    myOptions.Name = "post_configured_name_value";
    myOptions.Model = "post_configured_model_value";
});

var app = builder.Build();

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

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
                .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));

builder.Services.PostConfigureAll<MyConfigOptions>(myOptions =>
{
    myOptions.Key1 = "post_configured_key1_value";
});

Параметры доступа в Program.cs

Чтобы получить доступ к IOptions<TOptions> или IOptionsMonitor<TOptions> в Program.cs, вызовите GetRequiredService в WebApplication.Services:

var app = builder.Build();

var option1 = app.Services.GetRequiredService<IOptionsMonitor<MyOptions>>()
    .CurrentValue.Option1;

Дополнительные ресурсы

Авторы: Кирк Ларкин (Kirk Larkin) и Рик Андерсон (Rick Anderson).

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

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

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

В этом разделе приводятся сведения о шаблоне параметров в ASP.NET Core. Сведения об использовании шаблона параметров в консольных приложениях см. в разделе Шаблон параметров в .NET.

Просмотреть или скачать образец кода (описание загрузки)

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

Предпочтительный способ чтения связанных значений конфигурации — использование шаблона параметров. Например, чтобы считать следующие значения конфигурации:

  "Position": {
    "Title": "Editor",
    "Name": "Joe Smith"
  }

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

public class PositionOptions
{
    public const string Position = "Position";

    public string Title { get; set; }
    public string Name { get; set; }
}

Класс параметров:

  • Должен быть неабстрактным с открытым конструктором без параметров.
  • Все открытые свойства чтения и записи типа привязаны.
  • Поля не привязаны. В приведенном выше коде свойство Position не привязано. Свойство Position используется так, что строку "Position" не требуется жестко кодировать в приложении при привязке класса к поставщику конфигурации.

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

  • Вызывает ConfigurationBinder.Bind для привязки класса PositionOptions к разделу Position.
  • Отображает данные конфигурации Position.
public class Test22Model : PageModel
{
    private readonly IConfiguration Configuration;

    public Test22Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {
        var positionOptions = new PositionOptions();
        Configuration.GetSection(PositionOptions.Position).Bind(positionOptions);

        return Content($"Title: {positionOptions.Title} \n" +
                       $"Name: {positionOptions.Name}");
    }
}

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

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

public class Test21Model : PageModel
{
    private readonly IConfiguration Configuration;
    public PositionOptions positionOptions { get; private set; }

    public Test21Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {            
        positionOptions = Configuration.GetSection(PositionOptions.Position)
                                                     .Get<PositionOptions>();

        return Content($"Title: {positionOptions.Title} \n" +
                       $"Name: {positionOptions.Name}");
    }
}

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

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

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<PositionOptions>(Configuration.GetSection(
                                        PositionOptions.Position));
    services.AddRazorPages();
}

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

public class Test2Model : PageModel
{
    private readonly PositionOptions _options;

    public Test2Model(IOptions<PositionOptions> options)
    {
        _options = options.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Title: {_options.Title} \n" +
                       $"Name: {_options.Name}");
    }
}

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

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

IOptions<TOptions>:

IOptionsSnapshot<TOptions>:

IOptionsMonitor<TOptions>:

Сценарии пост-конфигурации позволяют задать или изменить параметры после выполнения всех действий настройки с помощью IConfigureOptions<TOptions>.

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

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

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

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

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

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

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

public class TestSnapModel : PageModel
{
    private readonly MyOptions _snapshotOptions;

    public TestSnapModel(IOptionsSnapshot<MyOptions> snapshotOptionsAccessor)
    {
        _snapshotOptions = snapshotOptionsAccessor.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Option1: {_snapshotOptions.Option1} \n" +
                       $"Option2: {_snapshotOptions.Option2}");
    }
}

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

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<MyOptions>(Configuration.GetSection("MyOptions"));

    services.AddRazorPages();
}

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

IOptionsMonitor

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

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<MyOptions>(Configuration.GetSection("MyOptions"));

    services.AddRazorPages();
}

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

public class TestMonitorModel : PageModel
{
    private readonly IOptionsMonitor<MyOptions> _optionsDelegate;

    public TestMonitorModel(IOptionsMonitor<MyOptions> optionsDelegate )
    {
        _optionsDelegate = optionsDelegate;
    }

    public ContentResult OnGet()
    {
        return Content($"Option1: {_optionsDelegate.CurrentValue.Option1} \n" +
                       $"Option2: {_optionsDelegate.CurrentValue.Option2}");
    }
}

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

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

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

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

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

{
  "TopItem": {
    "Month": {
      "Name": "Green Widget",
      "Model": "GW46"
    },
    "Year": {
      "Name": "Orange Gadget",
      "Model": "OG35"
    }
  }
}

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

public class TopItemSettings
{
    public const string Month = "Month";
    public const string Year = "Year";

    public string Name { get; set; }
    public string Model { get; set; }
}

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

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<TopItemSettings>(TopItemSettings.Month,
                                       Configuration.GetSection("TopItem:Month"));
    services.Configure<TopItemSettings>(TopItemSettings.Year,
                                        Configuration.GetSection("TopItem:Year"));

    services.AddRazorPages();
}

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

public class TestNOModel : PageModel
{
    private readonly TopItemSettings _monthTopItem;
    private readonly TopItemSettings _yearTopItem;

    public TestNOModel(IOptionsSnapshot<TopItemSettings> namedOptionsAccessor)
    {
        _monthTopItem = namedOptionsAccessor.Get(TopItemSettings.Month);
        _yearTopItem = namedOptionsAccessor.Get(TopItemSettings.Year);
    }

    public ContentResult OnGet()
    {
        return Content($"Month:Name {_monthTopItem.Name} \n" +
                       $"Month:Model {_monthTopItem.Model} \n\n" +
                       $"Year:Name {_yearTopItem.Name} \n" +
                       $"Year:Model {_yearTopItem.Model} \n"   );
    }
}

Все параметры являются именованными экземплярами. Экземпляры 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 используется в разделе Проверка параметров.

Дополнительные сведения о добавлении пользовательского репозитория см. в статье Использование AddOptions для настройки пользовательского репозитория.

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

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

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

    services.AddOptions<MyOptions>("optionalName")
        .Configure<Service1, Service2, Service3, Service4, Service5>(
            (o, s, s2, s3, s4, s5) => 
                o.Property = DoSomethingWith(s, s2, s3, s4, s5));
    
  • Создайте тип, реализующий IConfigureOptions<TOptions> или IConfigureNamedOptions<TOptions>, и зарегистрируйте этот тип как службу.

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

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

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

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

{
  "MyConfig": {
    "Key1": "My Key One",
    "Key2": 10,
    "Key3": 32
  }
}

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

public class MyConfigOptions
{
    public const string MyConfig = "MyConfig";

    [RegularExpression(@"^[a-zA-Z''-'\s]{1,40}$")]
    public string Key1 { get; set; }
    [Range(0, 1000,
        ErrorMessage = "Value for {0} must be between {1} and {2}.")]
    public int Key2 { get; set; }
    public int Key3 { get; set; }
}

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

  • вызывает AddOptions, чтобы получить класс OptionsBuilder<TOptions>, который привязывается к классу MyConfigOptions;
  • вызывает ValidateDataAnnotations для включения проверки с помощью DataAnnotations.
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddOptions<MyConfigOptions>()
            .Bind(Configuration.GetSection(MyConfigOptions.MyConfig))
            .ValidateDataAnnotations();

        services.AddControllersWithViews();
    }

Метод расширения ValidateDataAnnotations определен в пакете NuGet Microsoft.Extensions.Options.DataAnnotations. Для веб-приложений, использующих пакет SDK Microsoft.NET.Sdk.Web, ссылка на этот пакет указывается неявным образом из общей платформы.

Следующий код отображает значения конфигурации или ошибки проверки:

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;
    private readonly IOptions<MyConfigOptions> _config;

    public HomeController(IOptions<MyConfigOptions> config,
                          ILogger<HomeController> logger)
    {
        _config = config;
        _logger = logger;

        try
        {
            var configValue = _config.Value;

        }
        catch (OptionsValidationException ex)
        {
            foreach (var failure in ex.Failures)
            {
                _logger.LogError(failure);
            }
        }
    }

    public ContentResult Index()
    {
        string msg;
        try
        {
             msg = $"Key1: {_config.Value.Key1} \n" +
                   $"Key2: {_config.Value.Key2} \n" +
                   $"Key3: {_config.Value.Key3}";
        }
        catch (OptionsValidationException optValEx)
        {
            return Content(optValEx.Message);
        }
        return Content(msg);
    }

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

public void ConfigureServices(IServiceCollection services)
{
    services.AddOptions<MyConfigOptions>()
        .Bind(Configuration.GetSection(MyConfigOptions.MyConfig))
        .ValidateDataAnnotations()
        .Validate(config =>
        {
            if (config.Key2 != 0)
            {
                return config.Key3 > config.Key2;
            }

            return true;
        }, "Key3 must be > than Key2.");   // Failure message.

    services.AddControllersWithViews();
}

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

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

public class MyConfigValidation : IValidateOptions<MyConfigOptions>
{
    public MyConfigOptions _config { get; private set; }

    public  MyConfigValidation(IConfiguration config)
    {
        _config = config.GetSection(MyConfigOptions.MyConfig)
            .Get<MyConfigOptions>();
    }

    public ValidateOptionsResult Validate(string name, MyConfigOptions options)
    {
        string vor=null;
        var rx = new Regex(@"^[a-zA-Z''-'\s]{1,40}$");
        var match = rx.Match(options.Key1);

        if (string.IsNullOrEmpty(match.Value))
        {
            vor = $"{options.Key1} doesn't match RegEx \n";
        }

        if ( options.Key2 < 0 || options.Key2 > 1000)
        {
            vor = $"{options.Key2} doesn't match Range 0 - 1000 \n";
        }

        if (_config.Key2 != default)
        {
            if(_config.Key3 <= _config.Key2)
            {
                vor +=  "Key3 must be > than Key2.";
            }
        }

        if (vor != null)
        {
            return ValidateOptionsResult.Fail(vor);
        }

        return ValidateOptionsResult.Success;
    }
}

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

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

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<MyConfigOptions>(Configuration.GetSection(
                                        MyConfigOptions.MyConfig));
    services.TryAddEnumerable(ServiceDescriptor.Singleton<IValidateOptions
                              <MyConfigOptions>, MyConfigValidation>());
    services.AddControllersWithViews();
}

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

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

services.PostConfigure<MyOptions>(myOptions =>
{
    myOptions.Option1 = "post_configured_option1_value";
});

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

services.PostConfigure<MyOptions>("named_options_1", myOptions =>
{
    myOptions.Option1 = "post_configured_option1_value";
});

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

services.PostConfigureAll<MyOptions>(myOptions =>
{
    myOptions.Option1 = "post_configured_option1_value";
});

Доступ к параметрам во время запуска

IOptions<TOptions> и IOptionsMonitor<TOptions> можно использовать в методе Startup.Configure, так как службы создаются до выполнения метода Configure.

public void Configure(IApplicationBuilder app, 
    IOptionsMonitor<MyOptions> optionsAccessor)
{
    var option1 = optionsAccessor.CurrentValue.Option1;
}

Не используйте IOptions<TOptions> или IOptionsMonitor<TOptions> в Startup.ConfigureServices. Из-за очередности регистрации служб состояние параметров может быть несогласованным.

Пакет NuGet Options.ConfigurationExtensions

Пакет Microsoft.Extensions.Options.ConfigurationExtensions неявно упоминается в приложениях ASP.NET Core.