配置与 Options
假设开发环境每页显示 5 条 Todo,部署后要改成 20 条。这个值可以放在配置里,修改时就不必重新编译代码。
本章用 Options 模式(options pattern)把这组配置读进 TodoOptions 类,通过属性访问,并检查条数是否在允许范围内。
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.Options;
using Scalar.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();
builder.Services.AddOptions<TodoOptions>()
.BindConfiguration(TodoOptions.SectionName)
.ValidateDataAnnotations()
.ValidateOnStart();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapScalarApiReference();
}
app.MapGet("/settings", (IOptions<TodoOptions> options) =>
{
var settings = options.Value;
return new
{
settings.WelcomeMessage,
settings.MaxItems,
AdminKeyConfigured = !string.IsNullOrEmpty(settings.AdminKey),
};
});
app.Run();
public class TodoOptions
{
public const string SectionName = "Todo";
[Required]
public string WelcomeMessage { get; set; } = "";
[Range(1, 100)]
public int MaxItems { get; set; }
public string? AdminKey { get; set; }
}配置的值写在项目根目录的 appsettings.json 中,高亮部分是本节新增的 Todo 配置节:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Todo": {
"WelcomeMessage": "Welcome to the Todo API",
"MaxItems": 5
}
}运行与验证
先停止上一章的服务,从仓库根目录执行:
cd samples/12-configuration
dotnet runcurl http://localhost:5080/settings{"welcomeMessage":"Welcome to the Todo API (Development)","maxItems":5,"adminKeyConfigured":false}注意欢迎语后面多了"(Development)",而 appsettings.json 里并没有这几个字。它来自另一个文件:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"Todo": {
"WelcomeMessage": "Welcome to the Todo API (Development)"
}
}配置从哪里来
ASP.NET Core 的配置由多个配置源(configuration source)叠加而成。WebApplication.CreateBuilder 默认按以下顺序加载,后加载的覆盖先加载的:
| 顺序 | 配置源 | 典型用途 |
|---|---|---|
| 1 | appsettings.json | 所有环境共用的默认值,提交到代码仓库 |
| 2 | appsettings.{环境名}.json | 某个环境特有的值,例如 appsettings.Development.json |
| 3 | User Secrets(仅开发环境) | 开发者本机的密钥,不进代码仓库 |
| 4 | 环境变量 | 部署时由服务器、容器、云平台注入 |
| 5 | 命令行参数 | 临时覆盖,调试时方便 |
所以在开发环境中,WelcomeMessage 先从第 1 层读到"Welcome to the Todo API",又被第 2 层覆盖了;MaxItems 只在第 1 层出现,保持为 5。
这样可以把默认值留在文件里,部署时用环境变量覆盖,临时实验再用命令行覆盖。不必为了改一个值而复制整份配置文件。
用环境变量覆盖
下面每次切换启动参数,都先按 Ctrl+C 停止服务,重新启动后再用另一个终端请求 /settings。
环境变量用双下划线 __ 表示层级(因为有些系统的环境变量名不允许冒号):
Todo__MaxItems=20 dotnet run$env:Todo__MaxItems = "20"; dotnet run{"welcomeMessage":"Welcome to the Todo API (Development)","maxItems":20,"adminKeyConfigured":false}用命令行覆盖
命令行参数用冒号表示层级,写在 -- 之后。它的优先级比环境变量更高:
dotnet run -- --Todo:MaxItems=30{"welcomeMessage":"Welcome to the Todo API (Development)","maxItems":30,"adminKeyConfigured":false}即使同时设置了环境变量 Todo__MaxItems=20,结果也是 30。
提示
PowerShell 中设置的 $env: 变量会一直保留在当前终端窗口里,影响之后每一次 dotnet run。试验结束后用 Remove-Item Env:Todo__MaxItems 删除它。
绑定到强类型的类
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.Options;
using Scalar.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();
builder.Services.AddOptions<TodoOptions>()
.BindConfiguration(TodoOptions.SectionName)
.ValidateDataAnnotations()
.ValidateOnStart();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapScalarApiReference();
}
app.MapGet("/settings", (IOptions<TodoOptions> options) =>
{
var settings = options.Value;
return new
{
settings.WelcomeMessage,
settings.MaxItems,
AdminKeyConfigured = !string.IsNullOrEmpty(settings.AdminKey),
};
});
app.Run();
public class TodoOptions
{
public const string SectionName = "Todo";
[Required]
public string WelcomeMessage { get; set; } = "";
[Range(1, 100)]
public int MaxItems { get; set; }
public string? AdminKey { get; set; }
}第 34~45 行的 TodoOptions 是一个普通的类,属性名和 appsettings.json 中 Todo 节里的键一一对应。第 8~11 行做了三件事:
AddOptions<TodoOptions>():准备这个类型的 Options 服务,之后通过IOptions<TodoOptions>获取配置;BindConfiguration("Todo"):把配置中Todo节的值绑定到这个类的属性上。第 36 行的常量SectionName让配置节的名字只出现一次;ValidateDataAnnotations()和ValidateOnStart():用第 38、41 行的数据注解校验配置,并且在启动时就执行校验。
使用时,处理程序声明一个 IOptions<TodoOptions> 参数(第 21 行),通过 .Value 取得绑定好的对象。
配置只有一两项时,也可以直接读取 builder.Configuration["Todo:MaxItems"]。这里把相关配置放进一个类,是为了统一转换类型和检查范围,使用时写 settings.MaxItems 就能得到整数。
编辑器能检查 C# 属性名,但检查不了 JSON 里的键名。配置键拼错可能让属性保留默认值,因此仍需要校验。
在启动时发现配置错误
如果配置的值不合法,比如把 MaxItems 设为 0:
dotnet run -- --Todo:MaxItems=0应用根本不会启动:
fail: Microsoft.Extensions.Hosting.Internal.Host[11]
Hosting failed to start
Microsoft.Extensions.Options.OptionsValidationException: DataAnnotation validation failed for 'TodoOptions' members: 'MaxItems' with the error: 'The field MaxItems must be between 1 and 100.'.ValidateOnStart() 让配置错误在启动时暴露。去掉它,校验通常要等第一次读取 .Value 才执行,可能到某个请求进来时才发现配置有误。
技术细节
IOptions<T> 缓存配置对象,修改 JSON 后需要重启应用才能读到新值。需要动态更新时,还有 IOptionsSnapshot<T>(在每个作用域首次访问时创建快照)和 IOptionsMonitor<T>(支持配置变化通知);本章先使用 IOptions<T>。
FastAPI 对照
Options 模式相当于 pydantic-settings:用一个类声明配置项和类型,从文件和环境变量中读取,并做校验。FastAPI 中通常用 Depends(get_settings) 注入配置对象,这里则是注入 IOptions<TodoOptions>。
用 User Secrets 保存密钥
TodoOptions.AdminKey(第 44 行)模拟第三方服务的密钥。不要把真实密钥写进会提交到仓库的 appsettings.json。
开发时的密钥应该用 User Secrets(用户机密)保存。先在项目目录下初始化:
dotnet user-secrets init这个命令会在 .csproj 中加入一个 <UserSecretsId>,它是一个随机的 GUID,用来标识这个项目的机密存储。示例项目已经包含这一行,所以你可以跳过这一步。然后设置一个值:
dotnet user-secrets set "Todo:AdminKey" "s3cr3t-for-demo"Successfully saved Todo:AdminKey to the secret store.重新运行后,adminKeyConfigured 变成了 true:
{"welcomeMessage":"Welcome to the Todo API (Development)","maxItems":5,"adminKeyConfigured":true}机密保存在用户目录中,不随项目提交到 Git。用 dotnet user-secrets list 查看;实验后用 dotnet user-secrets remove "Todo:AdminKey" 删除这一项,不影响其他密钥。
/settings 只返回是否配置了密钥,方便验证来源;不会把服务端密钥返回给客户端。
注意
User Secrets 默认只在开发环境加载,而且以明文保存。本例切到生产环境后,如果没有从其他配置源提供 AdminKey,adminKeyConfigured 才是 false。部署时应从环境变量或密钥管理服务提供密钥。
总结
- 配置由多个源叠加:
appsettings.json→appsettings.{环境}.json→ User Secrets(仅开发)→ 环境变量 → 命令行,后面的覆盖前面的。 - 环境变量用
__表示层级(Todo__MaxItems),命令行用:(--Todo:MaxItems=30)。 - Options 模式:
AddOptions<T>().BindConfiguration("节名")把配置绑定到强类型的类,处理程序注入IOptions<T>使用。 - 用数据注解加
ValidateOnStart()校验配置,不合法的配置会让应用启动失败,而不是在运行中出错。 - 密钥不进代码仓库:开发时用 User Secrets,生产环境用环境变量或密钥管理服务。
