前言
之前就写过 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 Secrets ) 和 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 对象.
通过 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>(); }
好, 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"); });
这样就行了.
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.