系统参数(Aegis.SystemSettings)
Aegis.SystemSettings 用于按系统、院区和分组读取业务参数。业务代码可以使用 .NET 标准的 IOptionsMonitor<T> 强类型读取,也可以通过 ISystemSettings 按运行时坐标读取。
如何引入
安装 NuGet
dotnet add package Aegis.SystemSettings
启用组件
在宿主项目的 Component.deps.json 中,将 SystemSettings 加入 Services:
{
"Components": {
"Services": [
"SystemSettings"
],
"Middlewares": []
}
}
SystemSettings 不提供中间件,不需要放入 Middlewares。使用 ISystemSettings、SystemSettingsBindingAttribute 等公共类型的项目直接引用当前 NuGet 包即可。
配置当前系统编码
{
"SystemSettings": {
"SystemCode": "OP-Nurse"
}
}
SystemSettings:SystemCode 是必填项,需要与系统参数中登记的系统编码保持一致。
组件概览
| 字段 | 说明 |
|---|---|
| 组件名称 | 系统参数 |
| 真实类库 | Aegis.SystemSettings |
| 组件定位 | 按系统、院区和分组读取业务参数 |
| 引入方式 | 安装 NuGet,并在 Component.deps.json 中启用 SystemSettings |
| 默认参数来源 | 宿主已加载的 IConfiguration |
| 强类型入口 | IOptionsMonitor<T> |
| 动态入口 | ISystemSettings |
| 集中管理 | Redis 系统参数 |
什么时候要用它
适合以下场景:
- 参数属于可调整的业务规则,而不是代码常量。
- 同一套默认参数需要被不同院区局部覆盖。
- 业务代码希望使用强类型配置类,并在参数变化后读取新值。
- 需要在运行时切换系统、院区或分组。
以下内容继续使用其他配置能力:
- 数据库连接串、密钥和宿主端口等基础设施配置,应使用
appsettings.json、环境变量或专用密钥管理。 - 与当前系统无关的技术开关,不应为了集中展示而改造成业务参数。
最小可运行路径
第一步:在 appsettings.json 中准备参数
只使用当前包时,参数直接来自宿主已加载的 IConfiguration:
{
"SystemSettings": {
"SystemCode": "OP-Nurse",
"OP-Nurse": {
"_default": {
"Registration": {
"TimeoutSeconds": 60,
"AllowCrossSlot": false
}
},
"A": {
"Registration": {
"TimeoutSeconds": 30
}
}
}
}
}
_default 是全院区默认值,A 是 A 院区覆盖值。院区未配置的字段会继续使用默认值。
第二步:声明强类型配置
using Aegis.SystemSettings.Bindings;
[SystemSettingsBinding(GroupCode = "Registration")]
public sealed class RegistrationSettings
{
public int TimeoutSeconds { get; set; } = 60;
public bool AllowCrossSlot { get; set; }
}
GroupCode 需要与参数分组编码保持一致。未维护的字段保留 C# 属性初始值。
第三步:在业务服务中读取
using Microsoft.Extensions.Options;
public sealed class RegistrationService(
IOptionsMonitor<RegistrationSettings> settings)
{
public RegistrationSettings GetDefault()
{
return settings.CurrentValue;
}
public RegistrationSettings GetForHospital(string hospitalCode)
{
return settings.Get(hospitalCode);
}
}
CurrentValue 读取全院区默认值;Get(hospitalCode) 先加载默认值,再叠加指定院区的覆盖值。院区编码由业务上下文显式传入,组件不读取隐式请求上下文。
院区参数覆盖
取值顺序是“全院区默认值打底,指定院区局部覆盖”。
| 读取方式 | TimeoutSeconds | AllowCrossSlot |
|---|---|---|
settings.CurrentValue | 60 | false |
settings.Get("A") | 30 | false |
settings.Get("B") | 60 | false |
B 院区没有单独配置时,整个结果来自全院区默认值。
按运行时坐标读取
编译期无法确定分组或参数编码时,注入 ISystemSettings:
using Aegis.SystemSettings.Bindings;
public sealed class DynamicSettingsService(ISystemSettings settings)
{
public int GetDefaultTimeout()
{
return settings.Get("Registration", "TimeoutSeconds", 60);
}
public int GetHospitalTimeout(string hospitalCode)
{
return settings
.For(hospitalCode)
.Get("Registration", "TimeoutSeconds", 60);
}
public string GetRequiredValue(string groupCode, string key)
{
return settings.Require<string>(groupCode, key);
}
}
| API | 语义 |
|---|---|
Get<T>(groupCode, key, defaultValue) | 参数缺失或无法转换时返回默认值 |
Require<T>(groupCode, key) | 参数缺失或无法转换时抛出异常 |
For(hospitalCode) | 切换到当前系统的指定院区 |
ForSystem(systemCode, hospitalCode) | 切换到指定系统和院区 |
Group(groupCode).As<T>() | 将分组绑定为一次性强类型快照 |
Group(groupCode).As<T>() 返回调用时的快照,后续参数变更不会修改已返回的对象。
跨系统读取
配置类固定读取另一个系统时,在绑定特性中指定 SystemCode:
using Aegis.SystemSettings.Bindings;
[SystemSettingsBinding(
SystemCode = "Settle",
GroupCode = "Discount")]
public sealed class BillingDiscountSettings
{
public decimal SelfPayDiscountRate { get; set; }
}
系统编码由运行时参数决定时,使用 ISystemSettings:
var rate = settings
.ForSystem("Settle", hospitalCode)
.Get("Discount", "SelfPayDiscountRate", 1m);
一个强类型配置类只能对应一个系统和分组坐标。
显式注册配置类
配置类不适合添加 [SystemSettingsBinding] 特性时,可以在宿主中显式声明绑定:
services.AddSystemSettingsBinding<RegistrationSettings>(groupCode: "Registration");
指定另一个系统时:
services.AddSystemSettingsBinding<BillingDiscountSettings>(groupCode: "Discount", systemCode: "Settle");
同一类型不能同时使用特性绑定和显式绑定,冲突会在宿主启动时报错。
读取参数变化
IOptionsMonitor<T> 可以被任意生命周期的服务注入。参数来源发生变化后,默认值和已读取过的院区缓存会失效,下一次读取会重新绑定。
业务需要在变化时重建本地资源,可以订阅 OnChange:
using Microsoft.Extensions.Options;
public class RegistrationPolicyCache : IDisposable
{
private readonly IOptionsMonitor<RegistrationSettings> _settings;
private readonly IDisposable? _subscription;
public RegistrationPolicyCache(IOptionsMonitor<RegistrationSettings> settings)
{
_settings = settings;
_subscription = settings.OnChange((_, _) =>
{
RebuildPolicy(_settings.Get("A"));
});
}
public void Dispose()
{
_subscription?.Dispose();
}
private static void RebuildPolicy(RegistrationSettings settings)
{
// 根据最新参数重建本地策略
}
}
OnChange 表示已发生配置变化,不应依赖回调中的 name 一定是某个院区编码。需要指定院区的新值时,在回调中重新调用 Get(hospitalCode)。订阅返回的 IDisposable 应在服务停止时释放。
JSON 字符串参数
IConfiguration.Bind 不会将一个值中的 JSON 文本自动反序列化为列表或嵌套对象。参数值本身是 JSON 字符串时,先读取字符串,再显式反序列化:
using System.Text.Json;
var raw = settings
.For(hospitalCode)
.Get<string>("Registration", "ExcludedDepartments");
var departments = string.IsNullOrEmpty(raw)
? []
: JsonSerializer.Deserialize<List<string>>(raw) ?? [];
int、bool、decimal 和 string 等基本类型可以直接读取。
怎么选择参数来源
| 维度 | 默认实现 | Redis 实现 |
|---|---|---|
| 参数来源 | 宿主 IConfiguration | 管理后台发布并同步到 Redis |
| 参数更新 | 由宿主配置提供程序决定 | 发布后通知实例,并由周期校准恢复遗漏变更 |
| 集中管理 | 不提供 | 提供 |
| 业务读取 API | IOptionsMonitor<T> / ISystemSettings | 不变 |
参数需要在管理后台统一维护、多实例自动更新时,使用 Redis 系统参数。
接入后怎么确认生效
- 启动宿主,确认
ISystemSettings和已标注配置类的IOptionsMonitor<T>可以从 DI 解析。 - 读取
CurrentValue,确认与全院区默认值一致。 - 传入已配置的院区编码,确认只覆盖该院区已维护的字段。
- 读取不存在的参数,分别确认
Get返回默认值、Require抛出异常。
边界与限制
SystemSettings:SystemCode是当前进程的系统身份,区分大小写。- 院区编码、分组编码和参数编码由业务代码与参数来源约定,应保持稳定。
IOptionsMonitor<T>适合已知字段的强类型读取;ISystemSettings适合运行时坐标,不提供编译期字段检查。Group(groupCode).As<T>()是一次性快照,不会自动更新已返回的对象。- 一个配置类只能使用一种绑定声明,不能同时使用特性和显式注册。
- 将 JSON 存入单个参数值时,需要由业务代码显式反序列化。
- 当前包不提供参数管理页面或跨进程变更通知。
常见问题
为什么读到的都是配置类默认值
按顺序检查 SystemCode、GroupCode、院区编码和属性名。系统编码需要匹配 SystemSettings 下的系统节,分组编码需要匹配绑定声明,配置类属性名需要与参数编码对应。
业务层需要引用 Redis 扩展吗
不需要。业务层继续引用 Aegis.SystemSettings 并使用原有读取 API;Redis 扩展只安装在最终宿主。
参数变化后会修改已取得的配置对象吗
不会。IOptionsMonitor<T> 在下一次读取时返回重新绑定的对象;业务代码长期保留的旧对象和 Group(groupCode).As<T>() 已返回的快照不会就地变化。