跳转到内容

配置与选项

框架怎么读配置、选项模式怎么用、配置节怎么命名、什么该放配置什么该写代码。

配置来源

标准 .NET 配置栈,优先级从低到高:

text
appsettings.json
  → appsettings.{Environment}.json
    → 环境变量
      → 命令行参数

层级用双下划线表示:XiHan:Data:SqlSugarCore:EnableDiffLogXiHan__Data__SqlSugarCore__EnableDiffLog

密钥走环境变量

JWT 签名密钥、数据库密码、第三方 AppSecret 一律用环境变量或密钥库注入,不要提交明文。

配置节命名约定

框架自身的配置节统一在 XiHan: 命名空间下,按「领域 → 子领域」分层:

配置节归属包
XiHan:Data:SqlSugarCoreData
XiHan:Caching:RedisCaching
XiHan:Authentication:Jwt / :PasswordHasher / :OAuthAuthentication
XiHan:Web:Api:Auth / :Cors / :OpenApiSecurityWeb.Api
XiHan:Web:RealTime:SignalRWeb.RealTime
XiHan:Tasks:ScheduledJobs / XiHan:BackgroundJobsTasks
XiHan:Workflow / XiHan:Workflow:WorkerWorkflow
XiHan:LocalizationLocalization
XiHan:ObjectStorageObjectStorage
XiHan:ObservabilityObservability
XiHan:DistributedIds:SnowflakeIdDistributedIds
XiHan:UpgradeUpgrade
XiHan:SearchEngines:ElasticsearchSearchEngines.Elasticsearch

你自己的业务包用自己的顶层命名空间(如 MyCompany:Billing),避免和框架撞节。

选项模式

定义

把配置节名钉成常量放在 Options 类里,代码里一律引用常量:

csharp
public class BillingOptions
{
    public const string SectionName = "MyCompany:Billing";

    public int InvoiceRetentionDays { get; set; } = 365;
    public bool EnableAutoArchive { get; set; }
}

为什么要 SectionName 常量

内联 "MyCompany:Billing" 字符串会散落在绑定处、测试里、文档里,改名时必漏。常量还能让「这个 Options 对应哪个节」一眼可见。

绑定

在模块的 ConfigureServices 里:

csharp
public override void ConfigureServices(ServiceConfigurationContext context)
{
    var configuration = context.Services.GetConfiguration();

    // 从配置节绑定
    Configure<BillingOptions>(configuration.GetSection(BillingOptions.SectionName));

    // 或直接赋值
    Configure<BillingOptions>(o => o.InvoiceRetentionDays = 180);
}

Configure<T>XiHanModule 上的便捷方法,等价于 services.Configure<T>(...)

消费

csharp
public class InvoiceService(IOptions<BillingOptions> options) : ITransientDependency
{
    private readonly BillingOptions _options = options.Value;
}
接口何时用
IOptions<T>默认选择。单例,应用生命周期内不变
IOptionsSnapshot<T>每个作用域重新计算(Scoped 服务里用)
IOptionsMonitor<T>需要热更新与变更通知(单例服务里用)

三个配置钩子的时序

XiHanModule 提供三个层次,对应模块生命周期的三个阶段:

方法时机用途
PreConfigure<T>PreConfigureServices先于其他模块预设,供后续模块读取
Configure<T>ConfigureServices常规配置
PostConfigure<T>PostConfigureServices所有模块配置完之后做最终覆盖

典型用法:

csharp
// 库作者:给使用方一个预埋钩子
public override void PreConfigureServices(ServiceConfigurationContext context)
{
    PreConfigure<BillingOptions>(o => o.EnableAutoArchive = true);
}

// 应用作者:不管中间谁改过,我最后说了算
public override void PostConfigureServices(ServiceConfigurationContext context)
{
    PostConfigure<BillingOptions>(o => o.InvoiceRetentionDays = 90);
}

不走配置文件的选项

有些选项刻意不从配置绑定,只能代码方式设置——因为它们要在装配期就确定,或者值是类型/委托而非标量。

最典型的是动态 APIDynamicApiOptions:框架 TryAddSingleton 一个实例并交给约定实现持有,你的模块在 ConfigureServices 里就地改它:

csharp
context.Services.ConfigureDynamicApiConventions(conventions =>
{
    conventions.HttpMethodConventions["Import"] = "POST";
});

遇到「这个选项怎么配置文件里绑不上」时,先看包文档里它是不是这一类。

什么该放配置

判断结论
启动期就要用、改了必须重启(连接串、密钥、端口)配置文件 / 环境变量
运行期由管理员调整、要按租户隔离(业务开关、阈值)数据库(自行实现配置源)
装配期确定的类型/委托/约定代码

框架的可替换点大多留了「配置源」接口(如 IAiProviderConfigStore),业务侧可以 Replace 成数据库实现——这就是「配置落库」的标准做法,见 扩展与二次开发

常见问题

现象原因
配置改了不生效用了 IOptions<T>(单例快照);要热更新改 IOptionsMonitor<T>
环境变量没覆盖掉 json层级分隔符要用双下划线 __
某个选项在 appsettings 里绑不上它可能是代码方式配置的(如 DynamicApiOptions),或该字段是第三方库的字段而非属性
PostConfigure 没生效检查是否有更晚的模块又改了一次——最后执行的赢

下一步

Released under The MIT License