跳到主要内容

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);

默认的应用配置按以下优先级排列,越靠前优先级越高:

  1. 命令行参数;
  2. 没有 ASPNETCORE_DOTNET_ 前缀的环境变量;
  3. Development 环境中的 User Secrets;
  4. appsettings.{Environment}.json
  5. appsettings.json
  6. 宿主配置提供的后备值。

从实现角度看,配置提供程序按顺序加载,后加入的值会覆盖此前相同键的值。因此,同一个键既出现在 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");

索引器返回字符串,键不存在时返回 nullGetValue<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_ENVIRONMENTASPNETCORE_ENVIRONMENT 指定;未设置时默认为 Production。在使用现代 WebApplication 模型时,若两者同时设置,DOTNET_ENVIRONMENT 具有更高优先级。

环境名在应用启动后不能动态修改。代码中可通过 builder.Environmentapp.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 类型只描述一个业务场景;
  • 使用有意义的类型,例如 TimeSpanUri、枚举,而不是全部使用字符串;
  • 集中声明节名称,避免在多个文件中重复魔法字符串;
  • 不在 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>