注:本文隸屬於《理解ASP.NET Core》系列文章,請檢視置頂部落格或點選此處檢視全文目錄
Options繫結
上期我們已經聊過了配置(IConfiguration),今天我們來聊一聊Options
,中文譯為“選項”,該功能用於實現以強型別的方式對程式配置資訊進行訪問。
既然是強型別的方式,那麼就需要定義一個Options類,該類:
- 推薦命名規則:
{Object}Options
- 特點:
- 非抽象類
- 必須包含公共無參的建構函式
- 類中的所有公共讀寫屬性都會與配置項進行繫結
- 欄位不會被繫結
接下來,為了便於理解,先舉個例子:
首先在 appsetting.json 中新增如下配置:
{
"Book": {
"Id": 1,
"Name": "三國演義",
"Author": "羅貫中"
}
}
然後定義Options類:
public class BookOptions
{
public const string Book = "Book";
public int Id { get; set; }
public string Name { get; set; }
public string Author { get; set; }
}
最後進行繫結(有Bind
和Get
兩種方式):
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
// 方式 1:
var bookOptions1 = new BookOptions();
Configuration.GetSection(BookOptions.Book).Bind(bookOptions1);
// 方式 2:
var bookOptions2 = Configuration.GetSection(BookOptions.Book).Get<BookOptions>();
}
}
其中,屬性Id
、Title
、Author
均會與配置進行繫結,但是欄位Book
並不會被繫結,該欄位只是用來讓我們避免在程式中使用“魔數”。另外,一定要確保配置項能夠轉換到其繫結的屬性型別(你該不會想把string
繫結到int
型別上吧)。
如果中文讀取出來是亂碼,那麼你可以按照.L的.net core 讀取appsettings.json 檔案中文亂碼的問題來配置一下。
當然,這樣寫程式碼還不夠完美,還是要將Options新增到依賴注入服務容器中,例如通過IServiceCollection
的擴充套件方法Configure
:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.Configure<BookOptions>(Configuration.GetSection(BookOptions.Book));
}
}
Options讀取
通過Options介面,我們可以讀取依賴注入容器中的Options。常用的有三個介面:
IOptions<TOptions>
IOptionsSnapshot<TOptions>
IOptionsMonitor<TOptions>
接下來,我們看看它們的區別。
IOptions
- 該介面物件例項生命週期為 Singleton,因此能夠將該介面注入到任何生命週期的服務中
- 當該介面被例項化後,其中的選項值將永遠保持不變,即使後續修改了與選項進行繫結的配置,也永遠讀取不到修改後的配置值
- 不支援命名選項(Named Options),這個下面會說
public class ValuesController : ControllerBase
{
private readonly BookOptions _bookOptions;
public ValuesController(IOptions<BookOptions> bookOptions)
{
// bookOptions.Value 始終是程式啟動時載入的配置,永遠不會改變
_bookOptions = bookOptions.Value;
}
}
IOptionsSnapshot
- 該介面被註冊為 Scoped,因此該介面無法注入到 Singleton 的服務中,只能注入到 Transient 和 Scoped 的服務中。
- 在作用域中,建立
IOptionsSnapshot<TOptions>
物件例項時,會從配置中讀取最新選項值作為快照,並在作用域中始終使用該快照。 - 支援命名選項
public class ValuesController : ControllerBase
{
private readonly BookOptions _bookOptions;
public ValuesController(IOptionsSnapshot<BookOptions> bookOptionsSnapshot)
{
// bookOptions.Value 是 Options 物件例項建立時讀取的配置快照
_bookOptions = bookOptionsSnapshot.Value;
}
}
IOptionsMonitor
- 該介面除了可以檢視
TOptions
的值,還可以監控TOptions
配置的更改。 - 該介面被註冊為 Singleton,因此能夠將該介面注入到任何生命週期的服務中
- 每次讀取選項值時,都是從配置中讀取最新選項值(具體讀取邏輯檢視下方三種介面對比測試)。
- 支援:
- 命名選項
- 重新載入配置(
CurrentValue
),並當配置發生更改時,進行通知(OnChange
) - 快取與快取失效 (
IOptionsMonitorCache<TOptions>
)
public class ValuesController : ControllerBase
{
private readonly IOptionsMonitor<BookOptions> _bookOptionsMonitor;
public ValuesController(IOptionsMonitor<BookOptions> bookOptionsMonitor)
{
// _bookOptionsMonitor.CurrentValue 的值始終是最新配置的值
_bookOptionsMonitor = bookOptionsMonitor;
}
}
三種介面對比測試
IOptions<TOptions>
就不說了,主要說一下IOptionsSnapshot<TOptions>
和IOptionsMonitor<TOptions>
的不同:
IOptionsSnapshot<TOptions>
註冊為 Scoped,在建立其例項時,會從配置中讀取最新選項值作為快照,並在作用域中使用該快照IOptionsMonitor<TOptions>
註冊為 Singleton,每次呼叫例項的 CurrentValue 時,會先檢查快取(IOptionsMonitorCache<TOptions>
)是否有值,如果有值,則直接用,如果沒有,則從配置中讀取最新選項值,並記入快取。當配置發生更改時,會將快取清空。
搞個測試小程式:
[ApiController]
[Route("[controller]")]
public class ValuesController : ControllerBase
{
private readonly IOptions<BookOptions> _bookOptions;
private readonly IOptionsSnapshot<BookOptions> _bookOptionsSnapshot;
private readonly IOptionsMonitor<BookOptions> _bookOptionsMonitor;
public ValuesController(
IOptions<BookOptions> bookOptions,
IOptionsSnapshot<BookOptions> bookOptionsSnapshot,
IOptionsMonitor<BookOptions> bookOptionsMonitor)
{
_bookOptions = bookOptions;
_bookOptionsSnapshot = bookOptionsSnapshot;
_bookOptionsMonitor = bookOptionsMonitor;
}
[HttpGet]
public dynamic Get()
{
var bookOptionsValue1 = _bookOptions.Value;
var bookOptionsSnapshotValue1 = _bookOptionsSnapshot.Value;
var bookOptionsMonitorValue1 = _bookOptionsMonitor.CurrentValue;
Console.WriteLine("請修改配置檔案 appsettings.json");
Task.Delay(TimeSpan.FromSeconds(10)).Wait();
var bookOptionsValue2 = _bookOptions.Value;
var bookOptionsSnapshotValue2 = _bookOptionsSnapshot.Value;
var bookOptionsMonitorValue2 = _bookOptionsMonitor.CurrentValue;
return new
{
bookOptionsValue1,
bookOptionsSnapshotValue1,
bookOptionsMonitorValue1,
bookOptionsValue2,
bookOptionsSnapshotValue2,
bookOptionsMonitorValue2
};
}
}
執行2次,並按照指示修改兩次配置檔案(初始是“三國演義”,第一次修改為“水滸傳”,第二次修改為“紅樓夢”)
- 第1次輸出:
{
"bookOptionsValue1": {
"id": 1,
"name": "三國演義",
"author": "羅貫中"
},
"bookOptionsSnapshotValue1": {
"id": 1,
"name": "三國演義",
"author": "羅貫中"
},
"bookOptionsMonitorValue1": {
"id": 1,
"name": "三國演義",
"author": "羅貫中"
},
"bookOptionsValue2": {
"id": 1,
"name": "三國演義",
"author": "羅貫中"
},
// 注意 OptionsSnapshot 的值在當前作用域內沒有進行更新
"bookOptionsSnapshotValue2": {
"id": 1,
"name": "三國演義",
"author": "羅貫中"
},
// 注意 OptionsMonitor 的值變成最新的
"bookOptionsMonitorValue2": {
"id": 1,
"name": "水滸傳",
"author": "施耐庵"
}
}
- 第2次輸出:
{
// Options 的值始終沒有變化
"bookOptionsValue1": {
"id": 1,
"name": "三國演義",
"author": "羅貫中"
},
// 注意 OptionsSnapshot 的值變成當前最新值了
"bookOptionsSnapshotValue1": {
"id": 1,
"name": "水滸傳",
"author": "施耐庵"
},
// 注意 OptionsMonitor 的值始終是最新的
"bookOptionsMonitorValue1": {
"id": 1,
"name": "水滸傳",
"author": "施耐庵"
},
// Options 的值始終沒有變化
"bookOptionsValue2": {
"id": 1,
"name": "三國演義",
"author": "羅貫中"
},
// 注意 OptionsSnapshot 的值在當前作用域內沒有進行更新
"bookOptionsSnapshotValue2": {
"id": 1,
"name": "水滸傳",
"author": "施耐庵"
},
// 注意 OptionsMonitor 的值始終是最新的
"bookOptionsMonitorValue2": {
"id": 1,
"name": "紅樓夢",
"author": "曹雪芹"
}
}
通過測試我相信你應該能深刻理解它們之間的區別了。
命名選項(Named Options)
上面我們提到了命名選項,命名選項常用於多個配置節點繫結同一屬性的情況,舉個例子你就明白了:
在 appsettings.json 中新增如下配置
{
"DateTime": {
"Beijing": {
"Year": 2021,
"Month": 1,
"Day":1,
"Hour":12,
"Minute":0,
"Second":0
},
"Tokyo": {
"Year": 2021,
"Month": 1,
"Day":1,
"Hour":13,
"Minute":0,
"Second":0
},
}
}
很顯然,雖然“Beijing”和“Tokyo”是兩個配置項,但是屬性都是一樣的,我們沒必要建立兩個Options類,只需要建立一個就好了:
public class DateTimeOptions
{
public const string Beijing = "Beijing";
public const string Tokyo = "Tokyo";
public int Year { get; set; }
public int Month { get; set; }
public int Day { get; set; }
public int Hour { get; set; }
public int Minute { get; set; }
public int Second { get; set; }
}
然後,通過對選項進行指定命名的方式,一個叫做“Beijing”,一個叫做“Tokyo”,將選項新增到DI容器中:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.Configure<BookOptions>(Configuration.GetSection(BookOptions.Book));
services.Configure<DateTimeOptions>(DateTimeOptions.Beijing, Configuration.GetSection($"DateTime:{DateTimeOptions.Beijing}"));
services.Configure<DateTimeOptions>(DateTimeOptions.Tokyo, Configuration.GetSection($"DateTime:{DateTimeOptions.Tokyo}"));
}
}
最後,通過建構函式的方式將選項注入到Controller中。需要注意的是,因為DateTimeOptions
類繫結了兩個選項類,所以當我們獲取時選項值時,需要指定選項的名字。
public class ValuesController : ControllerBase
{
private readonly DateTimeOptions _beijingDateTimeOptions;
private readonly DateTimeOptions _tockyoDateTimeOptions;
public ValuesController(IOptionsSnapshot<DateTimeOptions> dateTimeOptions)
{
_beijingDateTimeOptions = dateTimeOptions.Get(DateTimeOptions.Beijing);
_tockyoDateTimeOptions = dateTimeOptions.Get(DateTimeOptions.Tokyo);
}
}
程式執行後,你會發現變數 _beijingDateTimeOptions 繫結的配置是“Beijing”配置節點,變數 _tockyoDateTimeOptions 繫結的配置是“Tokyo” 配置節點,但它們繫結的都是同一個類DateTimeOptions
事實上,.NET Core 中所有 Options 都是命名選項,當沒有顯式指定名字時,使用的名字預設是
Options.DefaultName
,即string.Empty
。
使用 DI 服務配置選項
在某些場景下,選項的配置需要依賴DI中的服務,這時可以藉助OptionsBuilderConfigure
方法(注意這個Configure
不是上面提到的IServiceCollection
的擴充套件方法Configure
,這是兩個不同的方法),該方法支援最多5個服務來配置選項:
services.AddOptions<BookOptions>()
.Configure<Service1, Service2, Service3, Service4, Service5>((o, s, s2, s3, s4, s5) =>
{
o.Authors = DoSomethingWith(s, s2, s3, s4, s5);
});
Options 驗證
配置畢竟是我們手動進行文字輸入的,難免會出現錯誤,這種情況下,就需要使用程式來幫助進行校驗了。
DataAnnotations
Install-Package Microsoft.Extensions.Options.DataAnnotations
我們先升級一下BookOptions
,增加一些資料校驗:
public class BookOptions
{
public const string Book = "Book";
[Range(1,1000,
ErrorMessage = "必須 {1} <= {0} <= {2}")]
public int Id { get; set; }
[StringLength(10, MinimumLength = 1,
ErrorMessage = "必須 {2} <= {0} Length <= {1}")]
public string Name { get; set; }
public string Author { get; set; }
}
然後我們在新增到DI容器時,增加資料註解驗證:
public void ConfigureServices(IServiceCollection services)
{
services.AddOptions<BookOptions>()
.Bind(Configuration.GetSection(BookOptions.Book))
.ValidateDataAnnotations();
.Validate(options =>
{
// 校驗通過 return true
// 校驗失敗 return false
if (options.Author.Contains("A"))
{
return false;
}
return true;
});
}
ValidateDataAnnotations
會根據你新增的特性進行資料校驗,當特性無法實現想要的校驗邏輯時,則使用Validate
進行較為複雜的校驗,如果過於複雜,則就要用到IValidateOptions
了(實質上,Validate
方法內部也是通過注入一個IValidateOptions
例項來實現選項驗證的)。
IValidateOptions
通過實現IValidateOptions<TOptions>
介面,增加資料校驗規則,例如:
public class BookValidation : IValidateOptions<BookOptions>
{
public ValidateOptionsResult Validate(string name, BookOptions options)
{
var failures = new List<string>();
if(!(options.Id >= 1 && options.Id <= 1000))
{
failures.Add($"必須 1 <= {nameof(options.Id)} <= {1000}");
}
if(!(options.Name.Length >= 1 && options.Name.Length <= 10))
{
failures.Add($"必須 1 <= {nameof(options.Name)} <= 10");
}
if (failures.Any())
{
return ValidateOptionsResult.Fail(failures);
}
return ValidateOptionsResult.Success;
}
}
然後我們將其注入到DI容器 Singleton,這裡使用了TryAddEnumerable
擴充套件方法新增該服務,是因為我們可以注入多個針對同一Options的IValidateOptions
,這些IValidateOptions
例項都會被執行:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<BookOptions>(Configuration.GetSection(BookOptions.Book));
services.TryAddEnumerable(ServiceDescriptor.Singleton<IValidateOptions<BookOptions>, BookValidation>());
}
Options後期配置
介紹兩個方法,分別是PostConfigure
和PostConfigureAll
,他們用來對選項進行後期配置。
- 在所有的
OptionsServiceCollectionExtensions.Configure
方法執行後執行 - 與
Configure
和ConfigureAll
類似,PostConfigure
僅用於對指定名稱的選項進行後期配置(預設名稱為string.Empty
),PostConfigureAll
則用於對所有選項例項進行後期配置 - 每當選項更改時,均會觸發相應的方法
public void ConfigureServices(IServiceCollection services)
{
services.PostConfigure<DateTimeOptions>(options =>
{
Console.WriteLine($"我只對名稱為{Options.DefaultName}的{nameof(DateTimeOptions)}例項進行後期配置");
});
services.PostConfigure<DateTimeOptions>(DateTimeOptions.Beijing, options =>
{
Console.WriteLine($"我只對名稱為{DateTimeOptions.Beijing}的{nameof(DateTimeOptions)}例項進行後期配置");
});
services.PostConfigureAll<DateTimeOptions>(options =>
{
Console.WriteLine($"我對{nameof(DateTimeOptions)}的所有例項進行後期配置");
});
}
Options 體系
IConfigureOptions
該介面用於包裝對選項
的配置。預設實現為ConfigureOptions<TOptions>
。
public interface IConfigureOptions<in TOptions> where TOptions : class
{
void Configure(TOptions options);
}
ConfigureOptions
public class ConfigureOptions<TOptions> : IConfigureOptions<TOptions> where TOptions : class
{
public ConfigureOptions(Action<TOptions> action)
{
Action = action;
}
public Action<TOptions> Action { get; }
// 配置 TOptions 例項
public virtual void Configure(TOptions options)
{
Action?.Invoke(options);
}
}
ConfigureFromConfigurationOptions
該類通過繼承類ConfigureOptions<TOptions>
,對選項的配置進行了擴充套件,允許通過ConfigurationBinder.Bind
擴充套件方法將IConfiguration
例項繫結到選項上:
public class ConfigureFromConfigurationOptions<TOptions> : ConfigureOptions<TOptions>
where TOptions : class
{
public ConfigureFromConfigurationOptions(IConfiguration config)
: base(options => ConfigurationBinder.Bind(config, options))
{ }
}
IConfigureNamedOptions
該介面用於包裝對命名選項
的配置,該介面同時繼承了介面IConfigureOptions<TOptions>
的行為,預設實現為ConfigureNamedOptions<TOptions>
,另外為了實現“使用 DI 服務配置選項”的功能,還提供了一些泛型類過載。
public interface IConfigureNamedOptions<in TOptions> : IConfigureOptions<TOptions> where TOptions : class
{
void Configure(string name, TOptions options);
}
ConfigureNamedOptions
public class ConfigureNamedOptions<TOptions> : IConfigureNamedOptions<TOptions> where TOptions : class
{
public ConfigureNamedOptions(string name, Action<TOptions> action)
{
Name = name;
Action = action;
}
public string Name { get; }
public Action<TOptions> Action { get; }
public virtual void Configure(string name, TOptions options)
{
// Name == null 表示針對 TOptions 的所有例項進行配置
if (Name == null || name == Name)
{
Action?.Invoke(options);
}
}
public void Configure(TOptions options) => Configure(Options.DefaultName, options);
}
NamedConfigureFromConfigurationOptions
該類通過繼承類ConfigureNamedOptions<TOptions>
,對命名選項的配置進行了擴充套件,允許通過ConfigurationBinder.Bind
擴充套件方法將IConfiguration
例項繫結到命名選項上:
public class NamedConfigureFromConfigurationOptions<TOptions> : ConfigureNamedOptions<TOptions>
where TOptions : class
{
public NamedConfigureFromConfigurationOptions(string name, IConfiguration config)
: this(name, config, _ => { })
{ }
public NamedConfigureFromConfigurationOptions(string name, IConfiguration config, Action<BinderOptions> configureBinder)
: base(name, options => config.Bind(options, configureBinder))
{ }
}
IPostConfigureOptions
該介面用於包裝對命名選項
的後期配置,將在所有IConfigureOptions<TOptions>
執行完畢後才會執行,預設實現為PostConfigureOptions<TOptions>
,同樣的,為了實現“使用 DI 服務對選項進行後期配置”的功能,也提供了一些泛型類過載:
public interface IPostConfigureOptions<in TOptions> where TOptions : class
{
void PostConfigure(string name, TOptions options);
}
public class PostConfigureOptions<TOptions> : IPostConfigureOptions<TOptions> where TOptions : class
{
public PostConfigureOptions(string name, Action<TOptions> action)
{
Name = name;
Action = action;
}
public string Name { get; }
public Action<TOptions> Action { get; }
public virtual void PostConfigure(string name, TOptions options)
{
// Name == null 表示針對 TOptions 的所有例項進行後期配置
if (Name == null || name == Name)
{
Action?.Invoke(options);
}
}
}
AddOptions & AddOptions & OptionsBuilder
public static class OptionsServiceCollectionExtensions
{
// 該方法幫我們把一些常用的與 Options 相關的服務注入到 DI 容器
public static IServiceCollection AddOptions(this IServiceCollection services)
{
services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(OptionsManager<>)));
services.TryAdd(ServiceDescriptor.Scoped(typeof(IOptionsSnapshot<>), typeof(OptionsManager<>)));
services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitor<>), typeof(OptionsMonitor<>)));
services.TryAdd(ServiceDescriptor.Transient(typeof(IOptionsFactory<>), typeof(OptionsFactory<>)));
services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitorCache<>), typeof(OptionsCache<>)));
return services;
}
// 沒有指定 Options 名稱時,預設使用 Options.DefaultName
public static OptionsBuilder<TOptions> AddOptions<TOptions>(this IServiceCollection services) where TOptions : class
=> services.AddOptions<TOptions>(Options.Options.DefaultName);
// 由於後續還要對 TOptions 進行配置,所以返回一個 OptionsBuilder 出去
public static OptionsBuilder<TOptions> AddOptions<TOptions>(this IServiceCollection services, string name)
where TOptions : class
{
services.AddOptions();
return new OptionsBuilder<TOptions>(services, name);
}
}
那我們看看OptionsBuilder<TOptions>
可以配置哪些東西,由於該類中有大量過載方法,我只挑選最基礎的方法來看一看:
public class OptionsBuilder<TOptions> where TOptions : class
{
private const string DefaultValidationFailureMessage = "A validation error has occurred.";
// TOptions 例項的名字
public string Name { get; }
public IServiceCollection Services { get; }
public OptionsBuilder(IServiceCollection services, string name)
{
Services = services;
Name = name ?? Options.DefaultName;
}
// 選項配置
public virtual OptionsBuilder<TOptions> Configure(Action<TOptions> configureOptions)
{
Services.AddSingleton<IConfigureOptions<TOptions>>(new ConfigureNamedOptions<TOptions>(Name, configureOptions));
return this;
}
// 選項後期配置
public virtual OptionsBuilder<TOptions> PostConfigure(Action<TOptions> configureOptions)
{
Services.AddSingleton<IPostConfigureOptions<TOptions>>(new PostConfigureOptions<TOptions>(Name, configureOptions));
return this;
}
// 選項驗證
public virtual OptionsBuilder<TOptions> Validate(Func<TOptions, bool> validation)
=> Validate(validation: validation, failureMessage: DefaultValidationFailureMessage);
public virtual OptionsBuilder<TOptions> Validate(Func<TOptions, bool> validation, string failureMessage)
{
Services.AddSingleton<IValidateOptions<TOptions>>(new ValidateOptions<TOptions>(Name, validation, failureMessage));
return this;
}
}
OptionsServiceCollectionExtensions.Configure
OptionsServiceCollectionExtensions.Configure<TOptions>
實際上就是對選項的一般配置方式進行了封裝,免去了OptionsBuilder<TOptions>
:
public static class OptionsServiceCollectionExtensions
{
// 沒有指定 Options 名稱時,預設使用 Options.DefaultName
public static IServiceCollection Configure<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions) where TOptions : class
=> services.Configure(Options.Options.DefaultName, configureOptions);
// 等同於做了 AddOptions<TOptions> 和 OptionsBuilder<TOptions>.Configure 兩件事
public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, Action<TOptions> configureOptions)
where TOptions : class
{
services.AddOptions();
services.AddSingleton<IConfigureOptions<TOptions>>(new ConfigureNamedOptions<TOptions>(name, configureOptions));
return services;
}
// 由於 ConfigureAll 是針對 TOptions 的所有例項進行配置,所以不需要指定名字
public static IServiceCollection ConfigureAll<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions) where TOptions : class
=> services.Configure(name: null, configureOptions: configureOptions);
}
OptionsConfigurationServiceCollectionExtensions.Configure
請注意,該Configure<TOptions>
方法與上方提及的Configure<TOptions>
不是同一個。該擴充套件方法針對配置(IConfiguration)繫結到選項(Options)上進行了擴充套件
Install-Package Microsoft.Extensions.Options.ConfigurationExtensions
public static class OptionsConfigurationServiceCollectionExtensions
{
public static IServiceCollection Configure<TOptions>(this IServiceCollection services, IConfiguration config) where TOptions : class
=> services.Configure<TOptions>(Options.Options.DefaultName, config);
public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, IConfiguration config) where TOptions : class
=> services.Configure<TOptions>(name, config, _ => { });
public static IServiceCollection Configure<TOptions>(this IServiceCollection services, IConfiguration config, Action<BinderOptions> configureBinder)
where TOptions : class
=> services.Configure<TOptions>(Options.Options.DefaultName, config, configureBinder);
public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, IConfiguration config, Action<BinderOptions> configureBinder)
where TOptions : class
{
services.AddOptions();
services.AddSingleton<IOptionsChangeTokenSource<TOptions>>(new ConfigurationChangeTokenSource<TOptions>(name, config));
return services.AddSingleton<IConfigureOptions<TOptions>>(new NamedConfigureFromConfigurationOptions<TOptions>(name, config, configureBinder));
}
}
IOptionsFactory
IOptionsFactory<TOptions>
負責建立命名選項
例項,預設實現為OptionsFactory<TOptions>
:
public interface IOptionsFactory<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TOptions> where TOptions : class
{
TOptions Create(string name);
}
public class OptionsFactory<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions>
: IOptionsFactory<TOptions> where TOptions : class
{
private readonly IEnumerable<IConfigureOptions<TOptions>> _setups;
private readonly IEnumerable<IPostConfigureOptions<TOptions>> _postConfigures;
private readonly IEnumerable<IValidateOptions<TOptions>> _validations;
// 這裡通過依賴注入的的方式將與 TOptions 相關的配置、驗證服務列表解析出來
public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures)
: this(setups, postConfigures, validations: null)
{ }
public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures, IEnumerable<IValidateOptions<TOptions>> validations)
{
_setups = setups;
_postConfigures = postConfigures;
_validations = validations;
}
public TOptions Create(string name)
{
// 1. 建立並配置 Options
TOptions options = CreateInstance(name);
foreach (IConfigureOptions<TOptions> setup in _setups)
{
if (setup is IConfigureNamedOptions<TOptions> namedSetup)
{
namedSetup.Configure(name, options);
}
else if (name == Options.DefaultName)
{
setup.Configure(options);
}
}
// 2. 對 Options 進行後期配置
foreach (IPostConfigureOptions<TOptions> post in _postConfigures)
{
post.PostConfigure(name, options);
}
// 3. 執行 Options 校驗
if (_validations != null)
{
var failures = new List<string>();
foreach (IValidateOptions<TOptions> validate in _validations)
{
ValidateOptionsResult result = validate.Validate(name, options);
if (result.Failed)
{
failures.AddRange(result.Failures);
}
}
if (failures.Count > 0)
{
throw new OptionsValidationException(name, typeof(TOptions), failures);
}
}
return options;
}
protected virtual TOptions CreateInstance(string name)
{
return Activator.CreateInstance<TOptions>();
}
}
OptionsManager
通過AddOptions
擴充套件方法的實現,可以看到,IOptions<TOptions>
和IOptionsSnapshot<TOptions>
的實現都是OptionsManager<TOptions>
,只不過一個是 Singleton,一個是 Scoped。我們通過前面的分析也知道了,當源中的配置改變時,IOptions<TOptions>
始終維持初始值,IOptionsSnapshot<TOptions>
在每次請求時會讀取最新配置值,並在同一個請求中是不變的。接下來就來看看OptionsManager<TOptions>
是如何實現的:
public class OptionsManager<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions> :
IOptions<TOptions>,
IOptionsSnapshot<TOptions>
where TOptions : class
{
private readonly IOptionsFactory<TOptions> _factory;
// 將已建立的 TOptions 例項快取到該私有變數中
private readonly OptionsCache<TOptions> _cache = new OptionsCache<TOptions>();
public OptionsManager(IOptionsFactory<TOptions> factory)
{
_factory = factory;
}
public TOptions Value => Get(Options.DefaultName);
public virtual TOptions Get(string name)
{
name = name ?? Options.DefaultName;
// 若快取不存在,則通過工廠新建 Options 例項,否則直接讀取快取
return _cache.GetOrAdd(name, () => _factory.Create(name));
}
}
OptionsMonitor
同樣,通過前面的分析,我們知道OptionsMonitor<TOptions>
讀取的始終是配置的最新值,它的實現在OptionsManager<TOptions>
的基礎上,除了使用快取將建立的 Options 例項快取起來外,還增添了監聽機制,當配置發生更改時,會將快取移除。
public class OptionsMonitor<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions> :
IOptionsMonitor<TOptions>,
IDisposable
where TOptions : class
{
private readonly IOptionsMonitorCache<TOptions> _cache;
private readonly IOptionsFactory<TOptions> _factory;
private readonly IEnumerable<IOptionsChangeTokenSource<TOptions>> _sources;
private readonly List<IDisposable> _registrations = new List<IDisposable>();
internal event Action<TOptions, string> _onChange;
public OptionsMonitor(IOptionsFactory<TOptions> factory, IEnumerable<IOptionsChangeTokenSource<TOptions>> sources, IOptionsMonitorCache<TOptions> cache)
{
_factory = factory;
_sources = sources;
_cache = cache;
// 監聽更改
foreach (IOptionsChangeTokenSource<TOptions> source in _sources)
{
IDisposable registration = ChangeToken.OnChange(
() => source.GetChangeToken(),
(name) => InvokeChanged(name),
source.Name);
_registrations.Add(registration);
}
}
// 當發生更改時,移除快取
private void InvokeChanged(string name)
{
name = name ?? Options.DefaultName;
_cache.TryRemove(name);
TOptions options = Get(name);
if (_onChange != null)
{
_onChange.Invoke(options, name);
}
}
public TOptions CurrentValue => Get(Options.DefaultName);
public virtual TOptions Get(string name)
{
name = name ?? Options.DefaultName;
return _cache.GetOrAdd(name, () => _factory.Create(name));
}
// 通過該方法繫結 OnChange 事件
public IDisposable OnChange(Action<TOptions, string> listener)
{
var disposable = new ChangeTrackerDisposable(this, listener);
_onChange += disposable.OnChange;
return disposable;
}
public void Dispose()
{
// 移除所有 change token 的訂閱
foreach (IDisposable registration in _registrations)
{
registration.Dispose();
}
_registrations.Clear();
}
}
總結
- 所有選項均為命名選項,預設名稱為
Options.DefaultName
,即string.Empty
。 - 通過
ConfigurationBinder.Get
或ConfigurationBinder.Bind
手動獲取選項例項。 - 通過
Configure
方法進行選項配置:OptionsBuilder<TOptions>.Configure
:通過包含DI服務的委託來進行選項配置OptionsServiceCollectionExtensions.Configure<TOptions>
:通過簡單委託來進行選項配置OptionsConfigurationServiceCollectionExtensions.Configure<TOptions>
:直接將IConfiguration
例項繫結到選項上
- 通過
OptionsServiceCollectionExtensions.ConfigureAll<TOptions>
方法針對某個選項型別的所有例項(不同名稱)統一進行配置。 - 通過
PostConfigure
方法進行選項後期配置:OptionsBuilder<TOptions>.PostConfigure
:通過包含DI服務的委託來進行選項後期配置OptionsServiceCollectionExtensions.PostConfigure<TOptions>
:通過簡單委託來進行選項後期配置
- 通過
PostConfigureAll<TOptions>
方法針對某個選項型別的所有例項(不同名稱)統一進行配置。 - 通過
Validate
進行選項驗證:OptionsBuilderDataAnnotationsExtensions.ValidateDataAnnotations
:通過資料註解進行選項驗證OptionsBuilder<TOptions>.Validate
:通過委託進行選項驗證IValidateOptions<TOptions>
:通過實現該介面並注入實現來進行選項驗證
- 通過依賴注入讀取選項:
IOptions<TOptions>
:Singleton,值永遠是該介面被例項化時的選項配置初始值IOptionsSnapshot<TOptions>
:Scoped,每一次Http請求開始時會讀取選項配置的最新值,並在當前請求中保持不變IOptionsMonitor<TOptions>
:Singleton,每次讀取都是選項配置的最新值