前言
之前就寫過 Asp.net core 學習筆記 ( Configuration 配置 ) 只是有點亂, 這篇作為整理版.
項目中會有許許多多的 Config 要設定. 比較好的管理方式是把它們放到 json file 里. 這樣想修改時就不需要改動源碼, 改 json file 就行了.
ASP.NET Core 提供了一套管理 Config 的方式. 這篇主要就是介紹這個.
參考:
docs – Configuration in ASP.NET Core
appsetting.json
appsetting.json 就是 ASP.NET Core 的 config file, 我們可以把所有模塊用到的 config 都寫在里面.
真實情況大概長這樣

For 下面的測試, 我做一個簡單的就好了
{ "MyConfig": { "Child1": { "Key": "Value", "Secret": "secret" }, "Child2": { "Key": "Value" } } }
另外, 它還支持多環境 config

appsettings.json 是抽象, appsettings.Development.json 是具體, 具體可以 override 和 extend 抽象.
"MyConfig": { "Child2": { "Key": "New Value" }, "Child3": { "Key": "New Value" } }
注: Child2 的 Key 是 override, MyConfig.Child3 是 extend, 沒辦法 override 整個 MyConfig 對象的.
User Secrets
想深入了解請看這篇 ASP.NET Core – User Secret & Azure Key Vault .
有一些 config 比較敏感, 比如密碼. ASP.NET Core 提供了一個叫 User Secrets 的方案來解決這個問題.
上面例子中, MyConfig.Child.Secret 是敏感數據. 不應該直接把 value 寫到 json file 里面, 必須使用 User Secrets.
dotnet user-secrets init dotnet user-secrets set "MyConfig:Child1:Secret" "password"
注意, 它的分隔符是分號 ":" 而不是點 "." 哦, 如果是 Array 就寫號碼, 比如: MyConfig:Array:0
這個 password 會被存到另一個 local file, git checkin 只會把 appsetting.json checkin, User Secrets local file 則不會, 所以密碼只會留在電腦中.
項目發布時, 則會通過 Azure KeyVault 來充當這個 User Secrets, 所以在 Web Server appsetting.json 依然不會有任何敏感數據.
好了, 這樣我們的 config 定義就完成了. 接下來看看如何在項目中獲取這些 config.
Get Config Value
GetValue
program.cs
var builder = WebApplication.CreateBuilder(args); var configValue1 = builder.Configuration.GetValue<string>("MyConfig:Child1:Key"); var configValue2 = builder.Configuration.GetValue<string>("MyConfig:Child1:Secret");
.NET 6 以前, 想在 program.cs 或者 config 是很難的, 但是現在很簡單直觀了.
CreateBuilder 會把 appsettings.Development.json, appsettings.json, User Secrets 弄好好.
通過 builder.Configuration.GetValue("path") 就可以獲取到任何 value 了.
注意, path 的分隔符是分號 ":" 而不是點 "." 哦.
GetValue 一定要聲明類型, 如果不清楚類型可以這樣獲取
var value = builder.Configuration["MyConfig:Child1:Key"];
value 的值一定是 string?,
如果是 null 那么就是 empty string.
如果是 boolean 那么就是 "False" or "True"
如果是 object 或 array 那么是 null
注: 最好能清楚 config 結構和類型, 不然會很亂的
GetSection
獲取整個對象.
public class Child { public string Key { get; set; } = ""; } var childObject = builder.Configuration.GetSection("MyConfig:Child1").Get<Child>();
Section != Child 對象哦, 所以要記得 .Get() 才能獲取到 Child 對象.
在 Console 創建和獲取 Configuration
雖然 WebApplication.CreateBuilder 已經幫我們弄美美了, 但為了能理解多一點底層, 我們也看看 Console 的版本吧
創建項目和安裝各做 package
dotnet new console -o SecretConsole dotnet add package Microsoft.Extensions.Configuration dotnet add package Microsoft.Extensions.Configuration.FileExtensions dotnet add package Microsoft.Extensions.Configuration.Json dotnet add package Microsoft.Extensions.Configuration.UserSecrets dotnet add package Microsoft.Extensions.Configuration.Binder
program.cs
using System.Reflection; using Microsoft.Extensions.Configuration; var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); // Development // 創建 ConfigBuilder var configurationBuilder = new ConfigurationManager() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .AddJsonFile($"appsettings.{environment ?? ""}.json", optional: true, reloadOnChange: true); // Development 情況下用本地的 UserSecrets if (environment == "Development") { configurationBuilder = configurationBuilder.AddUserSecrets(Assembly.GetExecutingAssembly()); // configurationBuilder = configurationBuilder.AddUserSecrets<Program>(); // 或者用 Program class 也是一樣的 } // 到這里就和 WebApplication.CreateBuilder 的 builder.Configuration 同樣用法了 var configure = configurationBuilder.Build(); var value = configure.GetValue<string>("Account:Password"); Console.Write(value); // my password
通過 DI 獲取 Configuration
上面 program.cs 是通過 builder 獲取到 configuration. 想在 Razor Pages, Controllers, Services 獲取到 Configuration 就需要通過 DI
public class IndexModel : PageModel { public IndexModel( IConfiguration configuration ) { var value = configuration.GetValue<string>("MyConfig:Child1:Key"); } public void OnGet() { } }
注入 IConfiguration 就可以了.
Options
封裝的模塊通常不會直接通過 appsetting 獲取 configuration (唯一例外的是 Log).
絕大部分的模塊會通過 Options 來管理 "Config".
這些就是 options



我們先看看 Options 的玩法, 之后在看它如何和 configuration 一起工作.
Service Module
假設想封裝一個服務
service class
public class MyService { public string GetComputedValue() { return "value"; } }
provide
builder.Services.AddScoped<MyService>();
inject
public IndexModel( MyService myService ) { var result = myService.GetComputedValue(); }
升級為模塊
public static class IServiceCollectionExtensions { public static void AddMyModule(this IServiceCollection services) { services.AddScoped<MyService>(); } }
provide
builder.Services.AddMyModule();
Service Options
這時, 想加入一些 config, 在 provide 的時候設定.
public class MyServiceOptions { public string Value { get; set; } = ""; }
provider
builder.Services.AddMyModule(options => { options.Value = "my value"; });
module
public static class IServiceCollectionExtensions { public static void AddMyModule(this IServiceCollection services, Action<MyServiceOptions> optionsBuilder) { var options = new MyServiceOptions(); optionsBuilder(options); var value = options.Value; // my value services.AddScoped<MyService>(); } }
這時問題來了, MyService 和 MyServiceOptions 怎樣關聯起來呢?
既然是用 DI, MyService 依賴 MyServiceOptions, 那么顯然 MyServiceOptions 也必須 provide, 這樣才能被 MyService 注入.
ASP.NET Core 提供了一個 services.Configure 接口, 讓我們 provide 這個 options.
public static void AddMyModule(this IServiceCollection services, Action<MyServiceOptions> optionsBuilder) { services.Configure<MyServiceOptions>(optionsBuilder); services.AddScoped<MyService>(); }
Configure 是可以 call multiple times 的哦

好, provide 沒有問題了, 那怎么注入?
通過 IOptions<MyServiceOptions> 注入.
public class MyService { public MyService( IOptions<MyServiceOptions> myServiceOptions ) { var value = myServiceOptions.Value; } public string GetComputedValue() { return "value"; } }
Options Work with Configuration
上面 options 是通過 optionsBuilder 來設置的. 那怎樣讓它和 appsetting 掛鈎呢?
方法 1, 把 section 丟進去.
builder.Services.Configure<MyServiceOptions>(builder.Configuration.GetSection("MyServiceOptions"));
方法 2, 挨個挨個 set
builder.Services.Configure<MyServiceOptions>(options => { options.Value = builder.Configuration.GetValue<string>("MyServiceOptions:Value"); });
這樣就行了.
Named Options
我是在玩 Identity External Login 時發現的. GoogleOptions
[FromServices] IOptionsSnapshot<GoogleOptions> googleOptionsAccessor var googleOptions = googleOptionsAccessor.Get("Google").Scope.ToList();
參考: Docs – Named options support using IConfigureNamedOptions
public static void Main(string[] args) { var serviceCollection = new ServiceCollection(); // 做 2 次 Configure<ServiceOptions> 但是用放不同的名字 serviceCollection.Configure<ServiceOptions>("Option1", options => options.Name = "Derrick"); serviceCollection.Configure<ServiceOptions>("Option2", options => options.Name = "Alex"); var serviceProvider = serviceCollection.BuildServiceProvider(); // 獲取 options accessor 手法一樣 var serviceOptionsAccessor = serviceProvider.GetRequiredService<IOptionsSnapshot<ServiceOptions>>(); // 不同名字可以從 accessor 里拿到不同的 options Console.WriteLine(serviceOptionsAccessor.Get("Option1").Name); // Derrick Console.WriteLine(serviceOptionsAccessor.Get("Option2").Name); // Alex }
看注釋理解
Optional Options
假如我忘了在 program.cs register ServiceOptions。
// builder.Services.Configure<ServiceOptions>(options => options.Name = "test");
但我缺嘗試去 inject 它
public IndexModel( IOptionsSnapshot<ServiceOptions> serviceOptionsAccessor ) { var serviceOptions = serviceOptionsAccessor.Value; }
它是不會報錯了。它會 new ServiceOptions() 作為 default value。
如果你需要識別出是否有提供 ServiceOptions 會比較麻煩。
參考: Stack Overflow – How to make an IOptions section optional in .NET Core?
在 options 里多加一個 property 來表示。

IOptionsSnapshot vs IOptions vs IOptionsMonitor
上面給的例子是用 IOptions 來注入. 它有個缺點. 就是當 appsetting 修改了以后, 需要重啟 app 才能 update.
有時候這個是預期的效果, 但有時候會希望馬上更新. 於是有了另外 2 個 IOptions 變種.
參考:
IOptions、IOptionsMonitor、IOptionsSnapshot的區別

它們之間主要是生命周期不同.
IOptions 算是單列, optionsBuilder 只運行一次. 一直到 application 重啟,
IOptionsSnapshot 的生命周期是 scope (per request), 它把聲明周期從 app 縮小到每個 request.
每一次新的請求就會重跑 optionBuilder 拿到新的 Options 值, 需要注意的是 snapshot 的周期是 scope 也意味着它不能用在單列的 service 哦.
IOptionsMonitor 可以用在單列也可以不需要重啟 app, 因為它獲取的是 current value. 也就是每一次都拿最新的, 甚至在同一個 request 里面.
我個人的用法是盡可能就用 snapshot 然后少用單列 service.
