ASP.NET Core 中的 Configuration 与 Options
适用范围:ASP.NET Core 8 及以上版本,示例采用
WebApplication.CreateBuilder最小托管模型。
1. 为什么需要 Configuration 与 Options
应用程序通常需要读取数据库连接串、第三方服务地址、超时时间、功能开关等运行参数。这些参数不应散落在代码中,因为它们会随着环境变化,却不应该迫使业务代码重新编译。
ASP.NET Core 将这个问题分成两层:
- Configuration 把 JSON 文件、环境变量、命令行参数等不同来源合并成一个统一的键值视图。
- Options 将某一组配置绑定成强类型对象,通过依赖注入交给业务组件使用,并提供校验、命名实例和变更监听等能力。
可以把二者的关系概括为:
配置源(JSON / 环境变量 / 命令行 / 密钥服务)
↓
IConfiguration
↓ 绑定
EmailOptions
↓ 注入
IOptions<T> / IOptionsSnapshot<T> / IOptionsMonitor<T>
对于只读取一两个框架级键值的启动代码,直接使用 IConfiguration 很方便;对于业务组件所依赖的一组配置,通常应优先采用 Options 模式。
2. Configuration 基础
2.1 默认配置源与覆盖规则
创建应用时,WebApplication.CreateBuilder(args) 已经注册了一组常用配置源:
var builder = WebApplication.CreateBuilder(args);
默认的应用配置按以下优先级排列,越靠前优先级越高:
- 命令行参数;
- 没有
ASPNETCORE_或DOTNET_前缀的环境变量; Development环境中的 User Secrets;appsettings.{Environment}.json;appsettings.json;- 宿主配置提供的后备值。
从实现角度看,配置提供程序按顺序加载,后加入的值会覆盖此前相同键的值。因此,同一个键既出现在 appsettings.json 又出现在环境变量中时,默认由环境变量获胜。这使得应用可以用 JSON 提供默认值,再由部署环境覆盖。
2.2 分层键
配置本质上是字符串键值对。JSON 的层级结构会被展开为使用冒号分隔的键:
{
"Email": {
"Host": "smtp.example.com",
"Port": 587,
"UseSsl": true
}
}
对应的键为:
Email:Host
Email:Port
Email:UseSsl
可以直接读取单个值:
string? host = builder.Configuration["Email:Host"];
int port = builder.Configuration.GetValue<int>("Email:Port");
string? connectionString =
builder.Configuration.GetConnectionString("DefaultConnection");
索引器返回字符串,键不存在时返回 null;GetValue<T> 会执行类型转换;GetConnectionString(name) 等价于读取 ConnectionStrings:{name}。
2.3 环境变量中的层级键
冒号在不同操作系统和 Shell 中兼容性不一致,因此环境变量应使用双下划线 __ 表示层级分隔符:
Email__Host=smtp.production.example.com
Email__Port=465
Email__UseSsl=true
运行时会自动把 __ 转换成 :。上述变量会覆盖 JSON 中的 Email 配置。Linux 环境变量名区分大小写,Windows 和 macOS 通常不区分,因此建议团队统一键名大小写。
2.4 按环境加载配置
常见的文件组织方式如下:
appsettings.json
appsettings.Development.json
appsettings.Staging.json
appsettings.Production.json
运行环境由 DOTNET_ENVIRONMENT 或 ASPNETCORE_ENVIRONMENT 指定;未设置时默认为 Production。在使用现代 WebApplication 模型时,若两者同时设置,DOTNET_ENVIRONMENT 具有更高优先级。
环境名在应用启动后不能动态修改。代码中可通过 builder.Environment 或 app.Environment 判断当前环境:
if (builder.Environment.IsDevelopment())
{
// 仅注册开发环境服务
}
2.5 添加自定义配置源
可以在默认配置之上继续添加来源:
builder.Configuration
.AddJsonFile(
"feature-flags.json",
optional: true,
reloadOnChange: true)
.AddEnvironmentVariables(prefix: "MYAPP_");
前缀会在读取时被移除。例如 MYAPP_Email__Host 会映射为 Email:Host。由于这两个来源是在默认来源之后添加的,它们对相同键具有更高优先级。
常见内置配置提供程序还包括 XML、INI、内存集合和 Key-per-file;云环境中也可以接入 Azure App Configuration、Azure Key Vault 或自定义提供程序。
3. Options 模式
3.1 定义强类型配置类
继续使用邮件服务作为例子:
public sealed class EmailOptions
{
public const string SectionName = "Email";
public required string Host { get; set; }
public int Port { get; set; }
public bool UseSsl { get; set; }
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(10);
}
对应的 appsettings.json:
{
"Email": {
"Host": "smtp.example.com",
"Port": 587,
"UseSsl": true,
"Timeout": "00:00:10"
}
}
建议做到:
- 一个 Options 类型只描述一个业务场景;
- 使用有意义的类型,例如
TimeSpan、Uri、枚举,而不是全部使用字符串; - 集中声明节名称,避免在多个文件中重复魔法字符串;
- 不在 Options 对象中存放业务行为。
3.2 绑定并注册
推荐通过 OptionsBuilder<T> 完成绑定和校验:
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddOptions<EmailOptions>()
.Bind(builder.Configuration.GetSection(EmailOptions.SectionName));
较简洁的等价写法是:
builder.Services.Configure<EmailOptions>(
builder.Configuration.GetSection(EmailOptions.SectionName));
Bind 会递归地把配置节绑定到可写属性。配置中多余的键默认会被忽略,缺失键也不一定报错,所以生产项目应配合校验使用。
也可以在启动阶段临时创建对象:
EmailOptions? email = builder.Configuration
.GetSection(EmailOptions.SectionName)
.Get<EmailOptions>();
但 Get<T> 得到的是普通对象,不会自动进入依赖注入容器,也没有 Options 的生命周期、命名实例、校验和变更监听机制,不应把它当作 Options 模式的完整替代品。
4. 三种 Options 接口如何选择
| 接口 | DI 生命周期 | 配置更新 | 命名选项 | 典型用途 |
|---|---|---|---|---|
IOptions<T> | Singleton | 不读取启动后的变化 | 不支持 | 配置固定、普通服务 |
IOptionsSnapshot<T> | Scoped | 每个作用域重新计算一次 | 支持 | Web 请求内保持一致、下个请求读取新值 |
IOptionsMonitor<T> | Singleton | 可读取当前值并订阅变化 | 支持 | 单例服务、后台服务、动态配置 |
4.1 IOptions<T>:稳定配置
public sealed class EmailSender(IOptions<EmailOptions> options)
{
private readonly EmailOptions _options = options.Value;
public Task SendAsync(string recipient, string body)
{
// 使用 _options.Host、_options.Port 等
return Task.CompletedTask;
}
}
它简单、开销低,适合数据库结构参数、启动后不允许改变的地址等稳定配置。
4.2 IOptionsSnapshot<T>:每个请求一个快照
public sealed class EmailSender(IOptionsSnapshot<EmailOptions> options)
{
public Task SendAsync(string recipient, string body)
{
EmailOptions current = options.Value;
return Task.CompletedTask;
}
}
在 Web 应用中,一个请求对应一个依赖注入作用域。同一请求内读取到的配置保持一致;支持重新加载的配置源发生变化后,后续请求可获得新值。
因为 IOptionsSnapshot<T> 是 Scoped 服务,不能注入 Singleton 服务。
4.3 IOptionsMonitor<T>:当前值与变更通知
public sealed class EmailWorker : IDisposable
{
private EmailOptions _current;
private readonly IDisposable? _subscription;
public EmailWorker(IOptionsMonitor<EmailOptions> monitor)
{
_current = monitor.CurrentValue;
_subscription = monitor.OnChange(updated => _current = updated);
}
public void Dispose() => _subscription?.Dispose();
}
IOptionsMonitor<T> 可安全注入 Singleton 或 BackgroundService。如果注册了 OnChange 回调,应保存并释放返回的订阅对象,避免不再使用的对象继续收到通知。
配置能否真正热更新取决于底层提供程序。JSON 等文件提供程序在启用 reloadOnChange 时支持变更通知;普通环境变量不会在进程运行期间自动重新读取。容器挂载卷或网络文件系统的文件事件有时不可靠,可按运行环境评估轮询文件监视器。
5. 配置校验:尽早失败
配置错误最好在应用启动时暴露,而不是等到第一封邮件发送时才失败。
5.1 Data Annotations 与自定义规则
using System.ComponentModel.DataAnnotations;
public sealed class EmailOptions
{
public const string SectionName = "Email";
[Required]
public string Host { get; set; } = string.Empty;
[Range(1, 65535)]
public int Port { get; set; }
public bool UseSsl { get; set; }
[Range(typeof(TimeSpan), "00:00:01", "00:05:00")]
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(10);
}
注册校验:
builder.Services
.AddOptions<EmailOptions>()
.Bind(builder.Configuration.GetSection(EmailOptions.SectionName))
.ValidateDataAnnotations()
.Validate(
options => !options.UseSsl || options.Port is 465 or 587,
"启用 SSL 时,端口必须为 465 或 587。")
.ValidateOnStart();
ValidateDataAnnotations 负责属性特性校验,Validate 适合简短的跨字段规则,ValidateOnStart 让错误在启动阶段立即暴露。
对于复杂规则,可以实现 IValidateOptions<T>:
public sealed class EmailOptionsValidator
: IValidateOptions<EmailOptions>
{
public ValidateOptionsResult Validate(
string? name,
EmailOptions options)
{
if (options.Host.EndsWith(".invalid", StringComparison.OrdinalIgnoreCase))
{
return ValidateOptionsResult.Fail("Email:Host 不是有效的服务地址。");
}
return ValidateOptionsResult.Success;
}
}
builder.Services.AddSingleton<
IValidateOptions<EmailOptions>,
EmailOptionsValidator>();
注意:Options 校验默认在选项首次创建时执行;配置热更新后,新值被 Options 系统创建时也会重新校验。ValidateOnStart 的意义是避免"某项配置一直没被访问,所以错误被隐藏"。
6. 一个推荐的完整写法
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.Options;
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddOptions<EmailOptions>()
.Bind(builder.Configuration.GetRequiredSection(EmailOptions.SectionName))
.ValidateDataAnnotations()
.Validate(
options => Uri.CheckHostName(options.Host) != UriHostNameType.Unknown,
"Email:Host 必须是有效的主机名。")
.ValidateOnStart();
builder.Services.AddSingleton<EmailSender>();
var app = builder.Build();
app.MapPost("/email", async (EmailRequest request, EmailSender sender) =>
{
await sender.SendAsync(request.Recipient, request.Body);
return Results.Accepted();
});
app.Run();
public sealed class EmailOptions
{
public const string SectionName = "Email";
[Required]
public string Host { get; set; } = string.Empty;
[Range(1, 65535)]
public int Port { get; set; }
public bool UseSsl { get; set; }
[Range(typeof(TimeSpan), "00:00:01", "00:05:00")]
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(10);
}
public sealed record EmailRequest(string Recipient, string Body);
public sealed class EmailSender(IOptions<EmailOptions> options)
{
private readonly EmailOptions _options = options.Value;
public Task SendAsync(string recipient, string body)
{
// 根据 _options 创建并调用邮件客户端。
return Task.CompletedTask;
}
}
这里使用 GetRequiredSection 检查配置节是否存在,使用 Options 校验检查字段是否合法,再通过 ValidateOnStart 将错误提前到应用启动阶段。对于启动后不需要变化的邮件配置,IOptions<EmailOptions> 是最直接的选择;若邮件配置需要热更新,可把消费者改为 IOptionsMonitor<EmailOptions>。