Skip to content
Bitzsoft.Integrationsbitzsoft.integrations

Concept

Options 配置与校验

Options 类的位置与命名、三种配置校验风格、ConfigProvider 抽象、配置节命名约定与启动期校验。

Last updated

Options 类是连接器配置的事实源。它定义厂商需要哪些参数(密钥、端点、超时),并通过校验机制在配置错误时尽早暴露。本节约定 Options 类放在哪里、如何命名、用哪种方式校验,以及配置节在 appsettings.json 中的键名约定。

Options 类的位置

Options 类始终放在厂商实现包,不放抽象包。这是三层包分离的核心推论:抽象层不知道任何厂商,自然不包含厂商特有的配置结构。

Bitzsoft.Integrations.Payment/ // 抽象包——无任何 Options
Bitzsoft.Integrations.Payment.Stripe/ // StripeOptions.cs ← 放这里
Bitzsoft.Integrations.Payment.Alipay/ // AlipayOptions.cs

命名约定

Options 类命名为 {Vendor}{Domain}Optionssealed 修饰:

厂商包Options 类名
Payment.StripeStripeOptions
Payment.AlipayAlipayOptions
TeamWork.DingTalkDingTalkOptions
Finance.Invoice.NuonuoNuonuoOptions
FileStorage.AwsAwsS3Options

字符串字段默认空字符串

所有 string 类型属性默认值用 string.Empty,不用 null。这避免 NullReferenceException,也让校验逻辑统一用 IsNullOrWhiteSpace 判断。

public sealed class StripeOptions
{
// ① 字符串字段默认 string.Empty
public string SecretKey { get; set; } = string.Empty;
public string WebhookSecret { get; set; } = string.Empty;
// ② 带业务默认值的字段
public string ApiBase { get; set; } = "https://api.stripe.com";
// ③ 非字符串类型按业务语义给默认值
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(30);
public string HttpClientName { get; set; } = nameof(StripePaymentProvider);
}

三种校验风格

全库存在三种 Options 校验风格,按演进顺序排列。

无校验Sms /Payment.Alipay配置错误在首次调用时才暴露DataAnnotationsBeisen特性内联 +ValidateDataAnnotationsIValidateOptionsTeamWork.DingTalk/ Finance独立验证器,表达力最强Options 校验风格演进

风格一:无校验(遗留)

Sms 域和早期 Payment.Alipay 不做任何配置校验。配置错误(如密钥为空)要到首次 API 调用时才以运行时异常暴露。

风格二:DataAnnotations + IValidatableObject

Beisen 域在 Options 类上用 [Required][Range] 特性声明约束,实现 IValidatableObject 做跨字段校验,DI 注册时调用 ValidateDataAnnotations()

BeisenOptions.cs
public class BeisenOptions : IValidatableObject
{
[Required(ErrorMessage = "AppKey 不能为空")]
public string AppKey { get; set; } = default!;
[Required(ErrorMessage = "AppSecret 不能为空")]
[JsonIgnore] // ① 敏感字段标记 JsonClone 防止泄露到日志
public string AppSecret { get; set; } = default!;
[Range(1000, 300000)]
public int TimeoutMs { get; set; } = 30_000;
// ② 跨字段 / 复杂规则用 IValidatableObject
public IEnumerable<ValidationResult> Validate(ValidationContext ctx)
{
if (!Uri.TryCreate(BaseUrl, UriKind.Absolute, out _))
yield return new ValidationResult("BaseUrl 不是有效的 URI", [nameof(BaseUrl)]);
}
}
// DI 注册
services.AddOptions<BeisenOptions>()
.Configure(configure)
.ValidateDataAnnotations() // ① 启用特性校验
.ValidateOnIntegrationStart(); // ② 启动期 fail-fast

风格三:IValidateOptions 独立验证器(推荐)

TeamWork.DingTalk 和 Finance 域把校验逻辑提取到独立的 IValidateOptions<T> 实现类,放在 Internal/ 下。这是当前推荐做法——表达力最强,校验逻辑与 Options 定义分离。

Internal/DingTalkOptionsValidator.cs
internal sealed class DingTalkOptionsValidator : IValidateOptions<DingTalkOptions>
{
public ValidateOptionsResult Validate(string? name, DingTalkOptions options)
{
// ① 必填项校验
if (string.IsNullOrWhiteSpace(options.AppKey))
return ValidateOptionsResult.Fail("钉钉 AppKey 不能为空");
if (string.IsNullOrWhiteSpace(options.AppSecret))
return ValidateOptionsResult.Fail("钉钉 AppSecret 不能为空");
// ② 数值范围校验
if (options.AgentId <= 0)
return ValidateOptionsResult.Fail("钉钉 AgentId 必须为正整数(工作通知需要)");
// ③ 复杂格式校验——HTTPS URL
if (!IsHttpsUrl(options.BaseUrl))
return ValidateOptionsResult.Fail($"钉钉 BaseUrl 必须为 HTTPS: {options.BaseUrl}");
return ValidateOptionsResult.Success;
}
private static bool IsHttpsUrl(string url)
=> Uri.TryCreate(url, UriKind.Absolute, out var uri) && uri.Scheme == Uri.UriSchemeHttps;
}
// DI 注册——注册为 Singleton,框架自动在解析 IOptions<T> 时调用
services.AddSingleton<IValidateOptions<DingTalkOptions>, DingTalkOptionsValidator>();

ConfigProvider 抽象

Payment 域的泛型 ConfigProvider

Payment 域在抽象包定义了 IPaymentConfigProvider<TOptions> 接口,把”配置从哪来”从 Provider 实现中解耦:

// Payment 抽象包
public interface IPaymentConfigProvider<TOptions> where TOptions : class
{
TOptions Current { get; } // ① 当前配置快照
string HttpClientName { get; } // ② 对应的命名 HttpClient
}

默认实现 StripeConfigProvider(internal)只是从 IOptions<StripeOptions> 读取的薄封装。消费者可注册自定义实现从数据库或配置中心加载配置:

// DI 注册——TryAddSingleton 允许消费者覆盖
services.TryAddSingleton<IStripeConfigProvider, StripeConfigProvider>();

appsettings.json 绑定

通过 services.Configure<T>(IConfigurationSection) 把配置节绑定到 Options:

public static IServiceCollection AddBitzsoftStripePayment(
this IServiceCollection services,
IConfiguration configuration,
string sectionKey = "Stripe")
{
// ① 从配置节绑定
services.Configure<StripeOptions>(configuration.GetSection(sectionKey));
// ...
}

配置节命名约定

配置节键名在全库不统一,这是历史遗留问题。下表列出当前各域的实际情况:

配置节键是否符合新约定
Payment.All"Payment"Payment:Alipay / Payment:Stripe
TeamWork.DingTalk"TeamWork:DingTalk"
Finance"Finance:Invoice"
Alipay(单包)"Alipay"✗ 缺 Domain 前缀
Stripe(单包)"Stripe"✗ 缺 Domain 前缀

新约定:{Domain}:{Vendor}

新包的配置节应使用 {Domain}:{Vendor} 格式,与聚合包的层级一致。聚合包从 {Domain} 根节下按 {Vendor} 分节:

{
"Payment": {
"Alipay": { "AppId": "...", "PrivateKey": "..." },
"WeChatPay": { "AppId": "...", "MchId": "..." },
"Stripe": { "SecretKey": "sk_live_..." }
},
"TeamWork": {
"DingTalk": { "AppKey": "...", "AppSecret": "..." }
}
}

ValidateOnIntegrationStart

跨 net5/net8/net10 多目标框架下,标准 ValidateOnStart() 仅在 ASP.NET Core 宿主启动时触发,对非 Web 宿主(如 Worker、控制台)无效。库提供了 ValidateOnIntegrationStart() 扩展,在任何宿主首次解析 IOptions<T> 时触发校验。

services.AddOptions<AwsS3Options>()
.Configure(configure)
.Validate(
options => !string.IsNullOrWhiteSpace(options.AccessKeyId)
&& !string.IsNullOrWhiteSpace(options.SecretAccessKey)
&& !string.IsNullOrWhiteSpace(options.DefaultBucketName),
"AWS S3 requires credentials, Region or ServiceURL, DefaultBucketName "
+ "and positive URL expiration values.")
.ValidateOnIntegrationStart(); // ① 跨 TFM 启动期校验

校验风格选择决策

仅必填 + 数值范围跨字段 / URL 格式 /条件必填

新增 Options 校验

校验规则复杂度?

DataAnnotations
[Required] + [Range]

IValidateOptions
独立验证器

Options 实现 IValidatableObject
补充跨字段规则

DI 注册 .ValidateDataAnnotations()

Internal/XxxOptionsValidator.cs

DI 注册 AddSingleton>

.ValidateOnIntegrationStart()

相关约定

100%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%