注:本文隸屬於《理解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中的服務,這時可以借助OptionsBuilder
Configure
方法(注意這個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,每次讀取都是選項配置的最新值