Поделиться через


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

Note

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

Warning

Эта версия ASP.NET Core больше не поддерживается. Дополнительные сведения см. в политике поддержки .NET и .NET Core. В текущей версии см. версию .NET 10 этой статьи.

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

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

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

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

Примеры в этой статье основаны на общем понимании внедрения служб в классы. Дополнительные сведения см. в статье Внедрение зависимостей в ASP.NET Core. Примеры основаны на BlazorRazor компонентах. Сведения о Razor примерах страниц см. в версии 7.0 этой статьи. В примерах в .NET 8 или более поздних версиях этой статьи используются основные конструкторы (первичные конструкторы (руководство по C#) и ссылочные типы, допускающие значение NULL (NRTs) с статическим анализом состояния .NET.

Использование шаблона параметров

Рассмотрим следующие данные конфигурации JSON из файла настроек приложения (например, appsettings.json), который содержит связанные данные об имени и должности сотрудника в должности в организации.

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

PositionOptions Следующий класс параметров:

  • Представляет собой POCO, простой класс .NET со свойствами. Класс options не должен быть абстрактным классом.
  • Имеет общедоступные свойства для чтения и записи, которые соответствуют записям в данных конфигурации.
  • Поле () . Поле Position используется для предотвращения жесткой привязки строки "Position" в приложении при привязке класса к поставщику конфигурации.

PositionOptions.cs:

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

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

Следующий пример:

  • Вызывает ConfigurationBinder.Bind, чтобы привязать класс PositionOptions к разделу Position.
  • Отображает данные конфигурации Position.

BasicOptions.razor:

@page "/basic-options"
@inject IConfiguration Config

Name: @positionOptions?.Name<br>
Title: @positionOptions?.Title

@code {
    private PositionOptions? positionOptions;

    protected override void OnInitialized()
    {
        positionOptions = new PositionOptions();
        Config.GetSection(PositionOptions.Position).Bind(positionOptions);
    }
}

BasicOptions.cshtml:

@page
@model RazorPagesSample.Pages.BasicOptionsModel
@{
}

BasicOptions.cshtml.cs:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace RazorPagesSample.Pages;

public class BasicOptionsModel : PageModel
{
    private readonly IConfiguration _config;

    public BasicOptionsModel(IConfiguration config)
    {
        _config = config;
    }

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

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

Выходные данные:

Name: Joe Smith
Title: Editor

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

Bind позволяет создать экземпляр абстрактного класса. Рассмотрим следующий пример, использующий абстрактный класс AbstractClassWithName.

NameTitleOptions.cs:

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

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

    public override string? Name { get; set; }
    public string? Title { get; set; }
    public int Age { get; set; } = age;
}

Конфигурация JSON:

"NameTitle": {
  "Name": "Sally Jones",
  "Title": "Writer"
}

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

AbstractClassOptions.razor:

@page "/abstract-class-options"
@inject IConfiguration Config

Name: @nameTitleOptions?.Name<br>
Title: @nameTitleOptions?.Title<br>
Age: @nameTitleOptions?.Age

@code {
    private NameTitleOptions? nameTitleOptions;

    protected override void OnInitialized()
    {
        nameTitleOptions = new NameTitleOptions(22);
        Config.GetSection(NameTitleOptions.NameTitle).Bind(nameTitleOptions);
    }
}

AbstractClassOptions.cshtml:

@page
@model RazorPagesSample.Pages.AbstractClassOptionsModel
@{
}

AbstractClassOptions.cshtml.cs:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace RazorPagesSample.Pages;

public class AbstractClassOptionsModel : PageModel
{
    private readonly IConfiguration _config;

    public AbstractClassOptionsModel(IConfiguration config)
    {
        _config = config;
    }

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

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

Выходные данные:

Name: Sally Jones
Title: Writer
Age: 22

ConfigurationBinder.Get привязывает и возвращает указанный тип. В следующем примере показано, как использовать Get с классом PositionOptions .

GetOptions.razor:

@page "/get-options"
@inject IConfiguration Config

Name: @positionOptions?.Name<br>
Title: @positionOptions?.Title

@code {
    private PositionOptions? positionOptions;

    protected override void OnInitialized() =>
        positionOptions = Config.GetSection(PositionOptions.Position)
            .Get<PositionOptions>();
}

GetOptions.cshtml:

@page
@model RazorPagesSample.Pages.GetOptionsModel
@{
}

GetOptions.cshtml.cs:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace RazorPagesSample.Pages;

public class GetOptionsModel : PageModel
{
    private readonly IConfiguration _config;

    public GetOptionsModel(IConfiguration config)
    {
        _config = config;
    }

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

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

Выходные данные:

Name: Joe Smith
Title: Editor

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

Сводка различий между ConfigurationBinder.Bind и ConfigurationBinder.Get:

  • Get обычно удобнее, чем использовать Bind , так как Get создает и возвращает новый экземпляр объекта, а Bind заполняет свойства существующего экземпляра объекта, который обычно устанавливается другой строкой кода.
  • Bind позволяет инстанцировать абстрактный класс, но Get может создать только неабстрактный экземпляр типа параметров.

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

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

Конфигурация JSON:

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

PositionOptions.cs:

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

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

Где службы регистрируются для внедрения зависимостей в файле приложения Program :

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

В следующем примере считываются параметры позиции.

DIOptions.razor:

@page "/di-options"
@using Microsoft.Extensions.Options
@inject IOptions<PositionOptions> Options

Name: @Options.Value.Name<br>
Title: @Options.Value.Title

DIOptions.cshtml:

@page
@model RazorPagesSample.Pages.DIOptionsModel
@{
}

DIOptions.cshtml.cs:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Options;

namespace RazorPagesSample.Pages;

public class DIOptionsModel : PageModel
{
    private readonly IOptions<PositionOptions> _options;

    public DIOptionsModel(IOptions<PositionOptions> options)
    {
        _options = options;
    }

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

Выходные данные:

Name: Joe Smith
Title: Editor

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

Интерфейсы опций

IOptions<TOptions>:

IOptionsSnapshot<TOptions>:

IOptionsMonitor<TOptions>:

Сценарии после настройки позволяют настраивать или изменять параметры после завершения настройки IConfigureOptions<TOptions> .

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

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

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

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

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

Среда выполнения ASP.NET Core использует OptionsCache<TOptions> для кэширования экземпляра параметров после его создания.

Конфигурация JSON:

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

PositionOptions.cs:

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

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

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

SnapshotOptions.razor:

@page "/snapshot-options"
@using Microsoft.Extensions.Options
@inject IOptionsSnapshot<PositionOptions> Options

Name: @Options.Value.Name<br>
Title: @Options.Value.Title

SnapshotOptions.cshtml:

@page
@model RazorPagesSample.Pages.SnapshotOptionsModel
@{
}

SnapshotOptions.cshtml.cs:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Options;

namespace RazorPagesSample.Pages;

public class SnapshotOptionsModel : PageModel
{
    private readonly IOptionsSnapshot<PositionOptions> _options;

    public SnapshotOptionsModel(IOptionsSnapshot<PositionOptions> options)
    {
        _options = options;
    }

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

Где службы регистрируются для внедрения зависимостей, в следующем примере регистрируется экземпляр конфигурации, PositionOptions который привязывается к:

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

Выходные данные:

Name: Joe Smith
Title: Editor

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

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

IOptionsMonitor<TOptions> используется для получения параметров и управления уведомлениями параметров для TOptions инстанций.

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

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

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

Конфигурация JSON:

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

PositionOptions.cs:

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

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

Где службы регистрируются для внедрения зависимостей, в следующем примере регистрируется экземпляр конфигурации, PositionOptions который привязывается к:

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

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

MonitorOptions.razor:

@page "/monitor-options"
@using Microsoft.Extensions.Options
@inject IOptionsMonitor<PositionOptions> Options

Name: @Options.CurrentValue.Name<br>
Title: @Options.CurrentValue.Title

MonitorOptions.cshtml:

@page
@model RazorPagesSample.Pages.MonitorOptionsModel
@{
}

MonitorOptions.cshtml.cs:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Options;

namespace RazorPagesSample.Pages;

public class MonitorOptionsModel : PageModel
{
    private readonly IOptionsMonitor<PositionOptions> _options;

    public MonitorOptionsModel(IOptionsMonitor<PositionOptions> options)
    {
        _options = options;
    }

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

Выходные данные:

Name: Joe Smith
Title: Editor

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

Указание пользовательского имени ключа для свойства конфигурации с помощью ConfigurationKeyName

По умолчанию имена свойств класса options используются в качестве имени ключа в источнике конфигурации. Если имя свойства равно Title, то имя ключа в конфигурации также равно Title.

Когда имена различаются, вы можете использовать атрибут[ConfigurationKeyName] для указания имени ключа в источнике конфигурации. С помощью этого метода можно сопоставить свойство в конфигурации с свойством в коде с другим именем. Это полезно, если имя свойства в источнике конфигурации не является допустимым идентификатором C# или если вы хотите использовать другое имя в коде.

Например, рассмотрим следующий класс параметров.

PositionKeyName.cs:

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

    [ConfigurationKeyName("PositionName")]
    public string? Name { get; set; }

    [ConfigurationKeyName("PositionTitle")]
    public string? Title { get; set; }
}

Свойства класса Name и Title привязаны к PositionName и PositionTitle из следующей конфигурации JSON.

"PositionKeyName": {
  "PositionName": "Carlos Diego",
  "PositionTitle": "Director"
}

PositionKeyNameOptions.razor:

@page "/position-key-name-options"
@inject IConfiguration Config

Name: @positionOptions?.Name<br>
Title: @positionOptions?.Title

@code {
    private PositionKeyName? positionOptions;

    protected override void OnInitialized() =>
        positionOptions = Config.GetSection(PositionKeyName.Position)
            .Get<PositionKeyName>();
}

PositionKeyName.cshtml:

@page
@model RazorPagesSample.Pages.PositionKeyNameModel
@{
}

PositionKeyName.cshtml.cs:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace RazorPagesSample.Pages;

public class PositionKeyNameModel : PageModel
{
    private readonly IConfiguration _config;

    public PositionKeyNameModel(IConfiguration config)
    {
        _config = config;
    }

    public ContentResult OnGet()
    {
        var positionOptions = _config.GetSection(PositionKeyName.Position)
            .Get<PositionKeyName>();

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

Выходные данные:

Name: Carlos Diego
Title: Director

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

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

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

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

Рассмотрим следующую конфигурацию JSON:

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

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

TopItemSettings.cs:

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

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

Где службы регистрируются для внедрения зависимостей, в следующем примере настраиваются именованные параметры:

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

В следующем примере отображаются именованные параметры:

NamedOptions.razor:

@page "/named-options"
@using Microsoft.Extensions.Options
@inject IOptionsSnapshot<TopItemSettings> Options

Month: Name: @monthTopItem?.Name Model: @monthTopItem?.Model<br>
Year: Name: @yearTopItem?.Name Model: @yearTopItem?.Model

@code {
    private TopItemSettings? monthTopItem;
    private TopItemSettings? yearTopItem;

    protected override void OnInitialized()
    {
        monthTopItem = Options.Get(TopItemSettings.Month);
        yearTopItem = Options.Get(TopItemSettings.Year);
    }
}

NamedOptions.cshtml:

@page
@model RazorPagesSample.Pages.NamedOptionsModel
@{
}

NamedOptions.cshtml.cs:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Options;

namespace RazorPagesSample.Pages;

public class NamedOptionsModel : PageModel
{
    private readonly IOptionsSnapshot<TopItemSettings> _options;

    public NamedOptionsModel(IOptionsSnapshot<TopItemSettings> options)
    {
        _options = options;
    }

    public ContentResult OnGet()
    {
        var monthTopItem = _options.Get(TopItemSettings.Month);
        var yearTopItem = _options.Get(TopItemSettings.Year);

        return Content(
            $"Month:Name {monthTopItem.Name}\n" +
            $"Month:Model {monthTopItem.Model}\n" +
            $"Year:Name {yearTopItem.Name}\n" +
            $"Year:Model {yearTopItem.Model}");
    }
}

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

Руководство по последующей настройке именованных параметров представлено в разделе "Параметры после настройки ".

OptionsBuilder API-интерфейс

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

OptionsBuilder<TOptions> показан в разделе проверки параметров .

Сведения о добавлении пользовательского репозитория см. в статье "Начало работы с API защиты данных" в ASP.NET Core.

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

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

Подход делегата конфигурации

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

builder.Services.AddOptions<PositionOptions>("optionalName")
    .Configure<Service1, Service2, Service3, Service4, Service5>(
        (o, s, s2, s3, s4, s5) => 
            o.Property = DoSomethingWith(s, s2, s3, s4, s5));
services.AddOptions<PositionOptions>("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>, имеющий конструктор, который принимает указанные типы служб.

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

Проверка параметров включает проверку значений параметров.

Рассмотрим следующую конфигурацию JSON:

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

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

KeyOptions.cs:

public class KeyOptions
{
    public const string Key = "KeyOptions";

    [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; }
}

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

builder.Services.AddOptions<KeyOptions>()
    .Bind(builder.Configuration.GetSection(KeyOptions.Key))
    .ValidateDataAnnotations();
services.AddOptions<KeyOptions>()
    .Bind(builder.Configuration.GetSection(KeyOptions.Key))
    .ValidateDataAnnotations();

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

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

builder.Services.AddOptions<KeyOptions>()
        .Bind(builder.Configuration.GetSection(KeyOptions.Key))
        .ValidateDataAnnotations()
    .Validate(options =>
    {
        return options.Key3 > options.Key2;
    }, "Key3 must be > than Key2");
services.AddOptions<KeyOptions>()
        .Bind(builder.Configuration.GetSection(KeyOptions.Key))
        .ValidateDataAnnotations()
    .Validate(options =>
    {
        return options.Key3 > options.Key2;
    }, "Key3 must be > than Key2");

В следующем примере показано, как регистрировать исключения проверки параметров и отображать OptionsValidationException.Message.

Note

Для демонстрации в следующем примере используется MarkupString, чтобы форматировать "сырой" HTML-код. Отрисовка необработанного HTML-кода, созданного из любого ненадежного источника, является угрозой безопасности, и ее всегда следует избегать. Дополнительные сведения см. в статье Компоненты Razor ASP.NET Core.

OptionsValidation1.razor:

@page "/options-validation-1"
@inject IOptionsSnapshot<KeyOptions> Options
@inject ILogger<OptionsValidation1> Logger

@if (message is not null)
{
    @((MarkupString)message)
}

@code {
    private string? message;

    protected override void OnInitialized()
    {
        try
        {
            var keyOptions = Options.Value;
        }
        catch (OptionsValidationException ex)
        {
            foreach (var failure in ex.Failures)
            {
                Logger.LogError(failure);
            }
        }

        try
        {
            message = 
                $"Key1: {Options.Value.Key1}<br>" +
                $"Key2: {Options.Value.Key2}<br>" +
                $"Key3: {Options.Value.Key3}";
        }
        catch (OptionsValidationException optValEx)
        {
            message = optValEx.Message;
        }
    }
}

В следующем примере показано, как регистрировать исключения, возникающие при проверке параметров, в конструкторе модели страницы и отображать OptionsValidationException.Message в методе OnGet этой страницы.

OptionsValidation1.cshtml:

@page
@model RazorPagesSample.Pages.OptionsValidation1Model
@{
}

OptionsValidation1.cshtml.cs:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Options;

namespace RazorPagesSample.Pages;

public class OptionsValidation1Model : PageModel
{
    private readonly IOptionsSnapshot<KeyOptions>? _options;

    public OptionsValidation1Model(IOptionsSnapshot<KeyOptions> options,
        ILogger<OptionsValidation1Model> logger)
    {
        _options = options;

        try
        {
            var keyOptions = _options?.Value;
        }
        catch (OptionsValidationException ex)
        {
            foreach (var failure in ex.Failures)
            {
                logger?.LogError("Validation: {Failure}", failure);
            }
        }
    }

    public ContentResult OnGet()
    {
        string message;

        try
        {
            message =
                $"Key1: {_options?.Value.Key1}\n" +
                $"Key2: {_options?.Value.Key2}\n" +
                $"Key3: {_options?.Value.Key3}";
        }
        catch (OptionsValidationException optValEx)
        {
            return Content(optValEx.Message);
        }

        return Content(message);
    }
}

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

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

В следующем примере содержимое страницы указывает, что значение Key2 не является диапазоном, если значение Key2 в файле параметров приложения изменяется на значение ниже нуля или более 1000:

DataAnnotation validation failed for 'KeyOptions' members: 'Key2' with the error: 'Value for Key2 must be between 0 and 1000.'.

Проверка параметров в выделенном классе с помощью IValidateOptions<TOptions>

Реализуйте IValidateOptions<TOptions> для проверки параметров без необходимости поддерживать правила проверки с использованием аннотаций данных или в файле приложения Program.

В следующем примере правила заметок данных и параметры делегирования проверки предыдущих примеров перемещаются в класс проверки. Класс модели Options (KeyOptions2) не содержит аннотаций данных.

Рассмотрим следующую конфигурацию JSON:

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

KeyOptions2.cs:

public class KeyOptions2
{
    public const string Key = "KeyOptions";

    public string? Key1 { get; set; }
    public int Key2 { get; set; }
    public int Key3 { get; set; }
}
public class KeyOptionsValidation : IValidateOptions<KeyOptions2>
{
    public ValidateOptionsResult Validate(string? name, KeyOptions2 options)
    {
        if (options == null)
        {
            return ValidateOptionsResult.Fail("KeyOptions not found.");
        }

        StringBuilder? validationResult = new();
        var rx = new Regex(@"^[a-zA-Z\s]{1,40}$");
        var match = rx.Match(options.Key1!);

        if (string.IsNullOrEmpty(match.Value))
        {
            validationResult.Append($"{options.Key1} doesn't match RegEx<br>");
        }

        if (options.Key2 < 0 || options.Key2 > 1000)
        {
            validationResult.Append($"{options.Key2} doesn't match Range 0 - 1000<br>");
        }

        if (options.Key3 < options.Key2)
        {
            validationResult.Append("Key3 must be > than Key2<br>");
        }

        if (validationResult.Length > 0)
        {
            return ValidateOptionsResult.Fail(validationResult.ToString());
        }

        return ValidateOptionsResult.Success;
    }
}
public class KeyOptionsValidation : IValidateOptions<KeyOptions2>
{
    public ValidateOptionsResult Validate(string? name, KeyOptions2 options)
    {
        if (options == null)
        {
            return ValidateOptionsResult.Fail("KeyOptions not found");
        }

        StringBuilder? validationResult = new();
        var rx = new Regex(@"^[a-zA-Z\s]{1,40}$");
        var match = rx.Match(options.Key1!);

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

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

        if (options.Key3 < options.Key2)
        {
            validationResult.Append("Key3 must be > than Key2\n");
        }

        if (validationResult.Length > 0)
        {
            return ValidateOptionsResult.Fail(validationResult.ToString());
        }

        return ValidateOptionsResult.Success;
    }
}

Где службы регистрируются для внедрения зависимостей и используют предыдущий код, проверка включена в Program.cs следующем примере:

builder.Services.Configure<KeyOptions2>(
    builder.Configuration.GetSection(KeyOptions2.Key));

builder.Services.AddSingleton<IValidateOptions<KeyOptions2>, 
    KeyOptionsValidation>();
services.Configure<KeyOptions>(
    builder.Configuration.GetSection(KeyOptions2.Key));

services.AddSingleton<IValidateOptions<KeyOptions2>, KeyOptionsValidation>();

В следующем примере показано, как регистрировать исключения проверки параметров и отображать OptionsValidationException.Message.

OptionsValidation2.razor:

@page "/options-validation-2"
@inject IOptionsSnapshot<KeyOptions2> Options
@inject ILogger<OptionsValidation2> Logger

@if (message is not null)
{
    @((MarkupString)message)
}

@code {
    private string? message;

    protected override void OnInitialized()
    {
        try
        {
            var keyOptions = Options.Value;
        }
        catch (OptionsValidationException ex)
        {
            foreach (var failure in ex.Failures)
            {
                Logger.LogError(failure);
            }
        }

        try
        {
            message = 
                $"Key1: {Options.Value.Key1}<br>" +
                $"Key2: {Options.Value.Key2}<br>" +
                $"Key3: {Options.Value.Key3}";
        }
        catch (OptionsValidationException optValEx)
        {
            message = optValEx.Message;
        }
    }
}

В следующем примере показано, как регистрировать исключения, возникающие при проверке параметров, в конструкторе модели страницы и отображать OptionsValidationException.Message в методе OnGet этой страницы.

OptionsValidation2.cshtml:

@page
@model RazorPagesSample.Pages.OptionsValidation2Model
@{
}

OptionsValidation2.cshtml.cs:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Options;

namespace RazorPagesSample.Pages;

public class OptionsValidation2Model : PageModel
{
    private readonly IOptionsSnapshot<KeyOptions2>? _options;

    public OptionsValidation2Model(IOptionsSnapshot<KeyOptions2> options,
        ILogger<OptionsValidation2Model> logger)
    {
        _options = options;

        try
        {
            var keyOptions = _options?.Value;
        }
        catch (OptionsValidationException ex)
        {
            foreach (var failure in ex.Failures)
            {
                logger?.LogError("Validation: {Failure}", failure);
            }
        }
    }

    public ContentResult OnGet()
    {
        string message;

        try
        {
            message =
                $"Key1: {_options?.Value.Key1}\n" +
                $"Key2: {_options?.Value.Key2}\n" +
                $"Key3: {_options?.Value.Key3}";
        }
        catch (OptionsValidationException optValEx)
        {
            return Content(optValEx.Message);
        }

        return Content(message);
    }
}

Проверка уровня класса с помощью IValidatableObject

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

Выполнение проверки параметров при запуске приложения ValidateOnStart

Проверка параметров выполняется при первом создании экземпляра TOption, то есть при первом доступе к конвейеру запроса IOptionsSnapshot<TOptions>.Value или при вызове IOptionsMonitor<TOptions>.Get(string). При каждом перезагрузе параметров проверка выполняется снова.

Чтобы выполнить проверку параметров при запуске приложения, вызовите ValidateOnStart, в Program файле, где службы регистрируются для внедрения зависимостей:

builder.Services.AddOptions<KeyOptions>()
    .Bind(builder.Configuration.GetSection(KeyOptions.Key))
    .ValidateDataAnnotations()
    .ValidateOnStart();

Параметры после настройки

При регистрации служб для внедрения зависимостей, PostConfigure доступен для инициализации определенного именованного параметра. В следующем примере только TopItem:Month:Name сконфигурировано дополнительно.

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>(TopItemSettings.Month, options =>
{
    options.Name = "Blue Gizmo";
});
services.Configure<TopItemSettings>(TopItemSettings.Month,
    builder.Configuration.GetSection("TopItem:Month"));
services.Configure<TopItemSettings>(TopItemSettings.Year,
    builder.Configuration.GetSection("TopItem:Year"));

services.PostConfigure<TopItemSettings>(TopItemSettings.Month, options =>
{
    options.Name = "Blue Gizmo";
});

Отображенный результат из примера именованных параметров, где после настройки изменён только TopItem:Month:Name.

Month: Name: Blue Gizmo Model: GW46
Year: Name: Orange Gadget Model: OG35

Для регистрации служб в системе внедрения зависимостей используйте PostConfigureAll для инициализации всех именованных экземпляров указанного типа параметров. В следующем примере для всех экземпляров TopItem.Name задано значение Blue Gizmo:

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

builder.Services.PostConfigureAll<TopItemSettings>(options =>
{
    options.Name = "Blue Gizmo";
});
services.Configure<TopItemSettings>(TopItemSettings.Month,
    builder.Configuration.GetSection("TopItem:Month"));
services.Configure<TopItemSettings>(TopItemSettings.Year,
    builder.Configuration.GetSection("TopItem:Year"));

services.PostConfigureAll<TopItemSettings>(options =>
{
    options.Name = "Blue Gizmo";
});

Отрисованные выходные данные из примера именованных параметров, где все TopItem:Name параметры настроены после настройки:

Month: Name: Blue Gizmo Model: GW46
Year: Name: Blue Gizmo Model: OG35

Параметры доступа в конвейере обработки запросов

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

var name = app.Services.GetRequiredService<IOptionsMonitor<PositionOptions>>()
    .CurrentValue.Name;

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

В следующем примере IOptionsMonitor<TOptions> внедряется в метод Startup.Configure, чтобы получить значения PositionOptions.

public void Configure(IApplicationBuilder app, 
    IOptionsMonitor<PositionOptions> options)

Доступ к параметрам в конвейере Startup.Configureобработки запросов:

var name = options.CurrentValue.Name;

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

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