Nguon: Microsoft Learn · .NET 8.0

Options pattern (Mẫu tùy chọn) trong ASP.NET Core

Nguồn: Options pattern in ASP.NET Core

Options pattern sử dụng các class để cung cấp quyền truy cập strongly typed (kiểu mạnh) vào các nhóm cài đặt liên quan. Khi configuration settings được cô lập theo kịch bản thành các class riêng biệt, ứng dụng tuân thủ hai nguyên tắc kỹ thuật phần mềm quan trọng:

Options cũng cung cấp cơ chế để xác thực dữ liệu cấu hình.

Bài viết này cung cấp thông tin về options pattern trong ASP.NET Core. Để biết thông tin về việc sử dụng options pattern trong console app, xem Options pattern in .NET.

Cách sử dụng options pattern

Xem xét dữ liệu cấu hình JSON sau từ file app settings, chứa dữ liệu liên quan đến tên và chức danh của nhân viên trong một vị trí của tổ chức:

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

Class options PositionOptions sau:

PositionOptions.cs:

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

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

Ví dụ sau:

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

ConfigurationBinder.Get bind và trả về kiểu được chỉ định. Ví dụ sau cho thấy cách sử dụng Get với class PositionOptions:

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>();
}

Tóm tắt sự khác biệt giữa ConfigurationBinder.BindConfigurationBinder.Get:

Bind options vào DI service container

Trong ví dụ sau, PositionOptions được thêm vào service container với Configure và được bind vào cấu hình.

Cấu hình JSON:

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

PositionOptions.cs:

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

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

Nơi đăng ký services cho dependency injection trong file Program:

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

Đọc position options:

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

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

Với code trên, các thay đổi đối với cấu hình JSON trong file app settings sau khi ứng dụng khởi động không được đọc. Để đọc thay đổi sau khi ứng dụng khởi động, sử dụng IOptionsSnapshot.

Options interfaces (Các interface Options)

IOptions&lt;TOptions&gt;:

IOptionsSnapshot&lt;TOptions&gt;:

IOptionsMonitor&lt;TOptions&gt;:

IOptionsFactory<TOptions> chịu trách nhiệm tạo các instance options mới.

Sử dụng IOptionsSnapshot để đọc dữ liệu được cập nhật

Sử dụng IOptionsSnapshot<TOptions>:

Ví dụ sử dụng IOptionsSnapshot<TOptions>:

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

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

Sử dụng IOptionsMonitor để đọc dữ liệu được cập nhật

IOptionsMonitor<TOptions> được sử dụng để lấy options và quản lý options notifications cho các instance TOptions.

Sự khác biệt giữa IOptionsMonitor<TOptions>IOptionsSnapshot là:

Ví dụ sử dụng IOptionsMonitor<TOptions>:

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

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

Chỉ định custom key name cho configuration property sử dụng ConfigurationKeyName

Theo mặc định, tên thuộc tính của class options được sử dụng làm key name trong configuration source. Khi tên khác nhau, bạn có thể sử dụng attribute [ConfigurationKeyName] để chỉ định key name trong configuration source.

Ví dụ, class options sau:

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

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

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

Các thuộc tính NameTitle của class được bind với PositionNamePositionTitle từ cấu hình JSON sau:

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

Named options support sử dụng IConfigureNamedOptions

Named options (tùy chọn được đặt tên):

Xem xét cấu hình JSON sau:

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

Thay vì tạo hai class để bind TopItem:MonthTopItem:Year, class sau được sử dụng cho mỗi section:

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

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

Cấu hình named options:

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

Hiển thị named options:

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

OptionsBuilder API

OptionsBuilder<TOptions> được sử dụng để cấu hình các instance TOptions. OptionsBuilder đơn giản hóa việc tạo named options vì nó chỉ là một tham số duy nhất đến lệnh gọi AddOptions<TOptions>(string optionsName) ban đầu.

Sử dụng DI services để cấu hình options

Services có thể được truy cập từ dependency injection khi cấu hình options theo hai cách:

Cách tiếp cận configuration delegate

Truyền configuration delegate vào Configure trên OptionsBuilder<TOptions>. OptionsBuilder<TOptions> cung cấp các overloads của Configure cho phép sử dụng tới năm services để cấu hình options:

csharp
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));

Cách tiếp cận configuration options service

Tạo kiểu implement IConfigureOptions<TOptions> hoặc IConfigureNamedOptions<TOptions> và đăng ký kiểu như một service.

Options validation (Xác thực tùy chọn)

Options validation cho phép xác thực các giá trị options.

Xem xét cấu hình JSON sau:

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

Class sau được sử dụng để bind với section cấu hình "KeyOptions" và áp dụng hai quy tắc data annotations, bao gồm regular expression và yêu cầu range:

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

Ví dụ sau:

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

Áp dụng quy tắc validation phức tạp hơn sử dụng delegate:

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

Xác thực options trong class dedicated với IValidateOptions<TOptions>

Implement IValidateOptions<TOptions> để xác thực options mà không cần duy trì các quy tắc validation với data annotations hoặc trong file Program của ứng dụng.

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

Validation được bật trong Program.cs:

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

builder.Services.AddSingleton<IValidateOptions<KeyOptions2>, 
    KeyOptionsValidation>();

Class-level validation với IValidatableObject

Options validation hỗ trợ IValidatableObject để thực hiện class-level validation của một class trong một class:

Chạy options validation khi ứng dụng khởi động với ValidateOnStart

Options validation chạy lần đầu tiên khi instance TOption được tạo. Để chạy options validation khi ứng dụng khởi động, gọi ValidateOnStart trong file Program nơi đăng ký services cho dependency injection:

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

Options post-configuration (Cấu hình sau tùy chọn)

PostConfigure có sẵn để khởi tạo named option cụ thể sau khi tất cả các cấu hình IConfigureOptions<TOptions> xảy ra.

csharp
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";
});

Sử dụng PostConfigureAll để khởi tạo tất cả các named instances của kiểu options được chỉ định:

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

Truy cập options trong request processing pipeline (đường dẫn xử lý yêu cầu)

Để truy cập IOptions<TOptions> hoặc IOptionsMonitor<TOptions> trong request processing pipeline, gọi GetRequiredService trên WebApplication.Services:

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