关于 dotnet-csharp-configuration
面向 .NET 应用开发者的配置实践指南,介绍 Microsoft.Extensions.Configuration 与 Microsoft.Extensions.Options 的用法。内容涵盖配置来源优先级、选项绑定与验证、用户机密、环境配置及功能标志。
主要功能
- 01说明配置来源和覆盖优先级
- 02绑定并验证强类型选项
- 03比较三种 Options 接口的生命周期
- 04介绍用户机密和环境变量配置
- 05使用 Microsoft.FeatureManagement 管理功能标志
适用场景
- 01为服务配置并验证 SMTP 等选项
- 02按环境覆盖应用设置
- 03为端点设置功能标志和筛选器
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill dotnet-csharp-configuration安装后在 Claude Code 中输入/dotnet-csharp-configuration确认已加载。
详细介绍
dotnet-csharp-configuration
使用 Microsoft.Extensions.Configuration 和 Microsoft.Extensions.Options 为 .NET 应用配置的模式。涵盖 Options 模式(IOptions<T>、IOptionsMonitor<T>、IOptionsSnapshot<T>)、验证、用户机密、基于环境的配置,以及使用 Microsoft.FeatureManagement 的功能标志。
交叉引用:[skill:dotnet-csharp-dependency-injection],了解服务注册模式;[skill:dotnet-csharp-coding-standards],了解命名约定。
配置源与优先级
WebApplication.CreateBuilder 中的默认配置源(后者优先):
appsettings.jsonappsettings.{Environment}.json- 用户机密(仅限 Development)
- 环境变量
- 命令行参数
var builder = WebApplication.CreateBuilder(args);
// Sources above are loaded automatically. Add custom sources:
builder.Configuration.AddJsonFile("features.json", optional: true, reloadOnChange: true);
Options 模式
将配置节绑定到强类型类,并通过 DI 注入。
定义 Options 类
public sealed class SmtpOptions
{
public const string SectionName = "Smtp";
public string Host { get; set; } = "";
public int Port { get; set; } = 587;
public string FromAddress { get; set; } = "";
public bool UseSsl { get; set; } = true;
}
Options 类使用
{ get; set; }(而非init),因为配置绑定器和PostConfigure需要修改属性。必填字段应使用数据注释中的[Required]。
注册
builder.Services
.AddOptions<SmtpOptions>()
.BindConfiguration(SmtpOptions.SectionName)
.ValidateDataAnnotations()
.ValidateOnStart();
appsettings.json
{
"Smtp": {
"Host": "smtp.example.com",
"Port": 587,
"FromAddress": "[email protected]",
"UseSsl": true
}
}
Options 接口
| 接口 | 生命周期 | 重新加载行为 | 用例 |
|---|---|---|---|
IOptions<T> | Singleton | 启动后不会重新加载 | 静态配置,大多数服务 |
IOptionsSnapshot<T> | Scoped | 每个请求/作用域重新加载 | ASP.NET 中按请求提供的配置 |
IOptionsMonitor<T> | Singleton | 实时重新加载 + 更改通知 | 单例、后台服务 |
注入示例
// Static -- most common, singleton-safe
public sealed class EmailService(IOptions<SmtpOptions> options)
{
private readonly SmtpOptions _smtp = options.Value;
public Task SendAsync(string to, string subject, string body,
CancellationToken ct = default)
{
// Use _smtp.Host, _smtp.Port, etc.
return Task.CompletedTask;
}
}
// Live reload in singletons -- monitors config file changes
public sealed class FeatureService(IOptionsMonitor<FeatureOptions> monitor)
{
public bool IsEnabled(string feature)
=> monitor.CurrentValue.EnabledFeatures.Contains(feature);
}
// Per-request in scoped services -- reads latest config each request
public sealed class PricingService(IOptionsSnapshot<PricingOptions> snapshot)
{
public decimal GetMarkup() => snapshot.Value.MarkupPercent;
}
使用 IOptionsMonitor<T> 接收更改通知
public sealed class CacheService : IDisposable
{
private readonly IDisposable? _changeListener;
private CacheOptions _current;
public CacheService(IOptionsMonitor<CacheOptions> monitor)
{
_current = monitor.CurrentValue;
_changeListener = monitor.OnChange(updated =>
{
_current = updated;
// React to config change -- flush cache, resize pool, etc.
});
}
public void Dispose() => _changeListener?.Dispose();
}
Options 验证
数据注释
using System.ComponentModel.DataAnnotations;
public sealed class SmtpOptions
{
public const string SectionName = "Smtp";
[Required, MinLength(1)]
public string Host { get; set; } = "";
[Range(1, 65535)]
public int Port { get; set; } = 587;
[Required, EmailAddress]
public string FromAddress { get; set; } = "";
}
builder.Services
.AddOptions<SmtpOptions>()
.BindConfiguration(SmtpOptions.SectionName)
.ValidateDataAnnotations()
.ValidateOnStart(); // Fail fast at startup, not on first use
IValidateOptions<T>(复杂验证)
当验证逻辑需要检查多个属性或使用外部依赖项时使用。
public sealed class SmtpOptionsValidator : IValidateOptions<SmtpOptions>
{
public ValidateOptionsResult Validate(string? name, SmtpOptions options)
{
var failures = new List<string>();
if (options.UseSsl && options.Port == 25)
{
failures.Add("Port 25 does not support SSL. Use 465 or 587.");
}
if (string.IsNullOrWhiteSpace(options.Host))
{
failures.Add("SMTP host is required.");
}
return failures.Count > 0
? ValidateOptionsResult.Fail(failures)
: ValidateOptionsResult.Success;
}
}
// Register the validator
builder.Services.AddSingleton<IValidateOptions<SmtpOptions>, SmtpOptionsValidator>();
ValidateOnStart(快速失败)
始终使用 .ValidateOnStart(),以便在启动时而非首次解析时发现配置错误。如果不使用它,只有首次访问 IOptions<T>.Value 时,无效配置才会引发异常。
用户机密(Development)
在开发期间将敏感值存储在源代码管理之外。
# Initialize (once per project)
dotnet user-secrets init
# Set values
dotnet user-secrets set "Smtp:Host" "smtp.example.com"
dotnet user-secrets set "ConnectionStrings:Default" "Server=..."
# List all secrets
dotnet user-secrets list
# Clear all
dotnet user-secrets clear
用户机密存储在 ~/.microsoft/usersecrets/<UserSecretsId>/secrets.json 中,并会在 Development 环境中覆盖 appsettings.json 中的值。
关键规则:
- 生产环境中绝不要使用用户机密——应使用环境变量、Azure Key Vault 或其他密钥保管库提供程序
- 当
ASPNETCORE_ENVIRONMENT=Development时,会自动加载用户机密 - 对于非 Web 主机,需显式添加:
builder.Configuration.AddUserSecrets<Program>()
基于环境的配置
环境变量
// Hierarchical keys use __ (double underscore) as separator
// Environment variable: Smtp__Host=smtp.prod.com
// Maps to: configuration["Smtp:Host"]
按环境划分的文件
appsettings.json # Base (all environments)
appsettings.Development.json # Overrides for dev
appsettings.Staging.json # Overrides for staging
appsettings.Production.json # Overrides for prod
// Set environment via ASPNETCORE_ENVIRONMENT or DOTNET_ENVIRONMENT
// Defaults to "Production" if not set
var env = builder.Environment.EnvironmentName; // "Development", "Staging", "Production"
条件式服务注册
if (builder.Environment.IsDevelopment())
{
builder.Services.AddSingleton<IEmailSender, ConsoleEmailSender>();
}
else
{
builder.Services.AddSingleton<IEmailSender, SmtpEmailSender>();
}
使用 Microsoft.FeatureManagement 实现功能标志
Microsoft.FeatureManagement.AspNetCore 提供结构化的功能标志支持,包括筛选器、目标定位和渐进式发布。
设置
dotnet add package Microsoft.FeatureManagement.AspNetCore
builder.Services.AddFeatureManagement();
配置
{
"FeatureManagement": {
"NewDashboard": true,
"BetaSearch": {
"EnabledFor": [
{
"Name": "Percentage",
"Parameters": { "Value": 50 }
}
]
},
"DarkMode": {
"EnabledFor": [
{
"Name": "Targeting",
"Parameters": {
"Audience": {
"Users": [ "[email protected]" ],
"Groups": [
{ "Name": "Beta", "RolloutPercentage": 100 }
],
"DefaultRolloutPercentage": 0
}
}
}
]
}
}
}
在代码中使用
// Inject IFeatureManager
public sealed class DashboardController(IFeatureManager featureManager) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> Get(CancellationToken ct = default)
{
if (await featureManager.IsEnabledAsync("NewDashboard"))
{
return Ok(new { version = "v2", dashboard = "new" });
}
return Ok(new { version = "v1", dashboard = "legacy" });
}
}
功能门控特性
// Entire endpoint gated on feature flag
[FeatureGate("BetaSearch")]
[HttpGet("search")]
public async Task<IActionResult> Search(string query, CancellationToken ct = default)
{
var results = await _searchService.SearchAsync(query, ct);
return Ok(results);
}
功能筛选器
| 筛选器 | 用途 |
|---|---|
Percentage | 为 N% 的请求启用(随机) |
TimeWindow | 在开始/结束日期之间启用 |
Targeting | 为特定用户、组或发布百分比启用 |
| 自定义 | 实现 IFeatureFilter 以编写特定于领域的逻辑 |
自定义功能筛选器
[FilterAlias("Browser")]
public sealed class BrowserFeatureFilter(IHttpContextAccessor accessor) : IFeatureFilter
{
public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext context)
{
var userAgent = accessor.HttpContext?.Request.Headers.UserAgent.ToString() ?? "";
var settings = context.Parameters.Get<BrowserFilterSettings>();
return Task.FromResult(
settings?.AllowedBrowsers?.Any(b =>
userAgent.Contains(b, StringComparison.OrdinalIgnoreCase)) ?? false);
}
}
public sealed class BrowserFilterSettings
{
public string[] AllowedBrowsers { get; init; } = [];
}
// Register
builder.Services.AddFeatureManagement()
.AddFeatureFilter<BrowserFeatureFilter>();
命名 Options
当需要同一 Options 类型的多个实例时(例如,多个 API 客户端),请使用命名 Options。
// Registration with names
builder.Services
.AddOptions<ApiClientOptions>("GitHub")
.BindConfiguration("ApiClients:GitHub");
builder.Services
.AddOptions<ApiClientOptions>("Jira")
.BindConfiguration("ApiClients:Jira");
// Resolution via IOptionsSnapshot<T> or IOptionsMonitor<T>
public sealed class ApiClientFactory(IOptionsSnapshot<ApiClientOptions> snapshot)
{
public HttpClient CreateFor(string name)
{
var options = snapshot.Get(name); // "GitHub" or "Jira"
return new HttpClient { BaseAddress = new Uri(options.BaseUrl) };
}
}
后期配置
在处理完所有配置源后应用默认值或覆盖值。
builder.Services.PostConfigure<SmtpOptions>(options =>
{
// Ensure a default port if none specified
if (options.Port == 0)
{
options.Port = options.UseSsl ? 465 : 25;
}
});
测试配置
[Fact]
public void SmtpOptions_Validates_InvalidPort()
{
var options = new SmtpOptions
{
Host = "smtp.example.com",
FromAddress = "[email protected]",
Port = 25,
UseSsl = true
};
var validator = new SmtpOptionsValidator();
var result = validator.Validate(null, options);
Assert.True(result.Failed);
Assert.Contains("Port 25 does not support SSL", result.FailureMessage);
}
[Fact]
public void Configuration_BindsCorrectly()
{
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["Smtp:Host"] = "smtp.test.com",
["Smtp:Port"] = "465",
["Smtp:FromAddress"] = "[email protected]",
})
.Build();
var options = new SmtpOptions();
config.GetSection("Smtp").Bind(options);
Assert.Equal("smtp.test.com", options.Host);
Assert.Equal(465, options.Port);
}