Options pattern (Mẫu tùy chọn) trong 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:
- Encapsulation (đóng gói): Các class phụ thuộc vào configuration settings chỉ phụ thuộc vào các cài đặt mà chúng sử dụng.
- Separation of Concerns (phân tách trách nhiệm): Settings cho các phần khác nhau của ứng dụng không phụ thuộc hoặc kết hợp với nhau.
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:
"Position": {
"Name": "Joe Smith",
"Title": "Editor"
}Class options PositionOptions sau:
- Là một POCO, một class .NET đơn giản với các thuộc tính. Class options không được là abstract class.
- Có các thuộc tính read-write public khớp với các mục tương ứng trong dữ liệu cấu hình.
- Không có trường (
Position) của nó bị bind. TrườngPositionđược sử dụng để tránh hardcoding chuỗi"Position"trong ứng dụng khi bind class với configuration provider.
PositionOptions.cs:
public class PositionOptions
{
public const string Position = "Position";
public string? Name { get; set; }
public string? Title { get; set; }
}Ví dụ sau:
- Gọi
ConfigurationBinder.Bindđể bind classPositionOptionsvào sectionPosition. - Hiển thị dữ liệu cấu hình
Position.
@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:
@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.Bind và ConfigurationBinder.Get:
Getthường thuận tiện hơn so với sử dụngBindvìGettạo và trả về instance mới của đối tượng, trong khiBindđiền thuộc tính của instance đối tượng hiện có.Bindcho phép khởi tạo một abstract class, trong khiGetchỉ có thể tạo instance non-abstract của kiểu options.
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:
"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; }
}Nơi đăng ký services cho dependency injection trong file Program:
builder.Services.Configure<PositionOptions>(
builder.Configuration.GetSection(PositionOptions.Position));Đọc position options:
@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)
- Không hỗ trợ:
- Đọc dữ liệu cấu hình sau khi ứng dụng khởi động.
- Named options.
- Được đăng ký là singleton service và có thể được inject vào bất kỳ service lifetime nào.
IOptionsSnapshot<TOptions>:
- Hữu ích trong các tình huống khi options cần được tính toán lại trên mỗi request.
- Được đăng ký là scoped service, vì vậy không thể được inject vào singleton service.
- Hỗ trợ named options.
IOptionsMonitor<TOptions>:
- Được sử dụng để lấy options và quản lý options notifications cho các instance
TOptions. - Được đăng ký là singleton service và có thể được inject vào bất kỳ service lifetime nào.
- Hỗ trợ:
- Change notifications.
- Named options.
- Reloadable configuration.
- Selective options invalidation (
IOptionsMonitorCache<TOptions>).
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>:
- Options được tính toán một lần mỗi request khi được truy cập và được cache trong suốt vòng đời của request.
- Các thay đổi đối với cấu hình được đọc sau khi ứng dụng khởi động khi sử dụng các configuration providers hỗ trợ đọc các giá trị cấu hình được cập nhật.
Ví dụ sử dụng IOptionsSnapshot<TOptions>:
@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> và IOptionsSnapshot là:
IOptionsMonitor<TOptions>là singleton service lấy các giá trị options hiện tại bất kỳ lúc nào, đặc biệt hữu ích trong các singleton dependencies.IOptionsSnapshot<TOptions>là scoped service và cung cấp snapshot của options tại thời điểm đối tượngIOptionsSnapshot<T>được xây dựng. Options snapshots được thiết kế để sử dụng với transient và scoped dependencies.
Ví dụ sử dụng IOptionsMonitor<TOptions>:
@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:
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 Name và Title của class được bind với PositionName và PositionTitle từ cấu hình JSON sau:
"PositionKeyName": {
"PositionName": "Carlos Diego",
"PositionTitle": "Director"
}Named options support sử dụng IConfigureNamedOptions
Named options (tùy chọn được đặt tên):
- Hữu ích khi nhiều configuration sections bind đến cùng các thuộc tính.
- Phân biệt chữ hoa/thường.
Xem xét cấu hình JSON sau:
"TopItem": {
"Month": {
"Name": "Green Widget",
"Model": "GW46"
},
"Year": {
"Name": "Orange Gadget",
"Model": "OG35"
}
}Thay vì tạo hai class để bind TopItem:Month và TopItem:Year, class sau được sử dụng cho mỗi section:
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:
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:
@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:
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:
"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:
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:
- Gọi
AddOptionsđể lấyOptionsBuilder<TOptions>bind với classKeyOptions. - Gọi
ValidateDataAnnotationsđể bật validation.
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:
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.
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:
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:
- Implement interface
IValidatableObjectvà phương thứcValidatecủa nó trong class. - Gọi
ValidateDataAnnotationstrong fileProgram.
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:
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.
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:
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:
var name = app.Services.GetRequiredService<IOptionsMonitor<PositionOptions>>()
.CurrentValue.Name;