Options 类是连接器配置的事实源。它定义厂商需要哪些参数(密钥、端点、超时),并通过校验机制在配置错误时尽早暴露。本节约定 Options 类放在哪里、如何命名、用哪种方式校验,以及配置节在 appsettings.json 中的键名约定。
Options 类的位置
Options 类始终放在厂商实现包,不放抽象包。这是三层包分离的核心推论:抽象层不知道任何厂商,自然不包含厂商特有的配置结构。
Bitzsoft.Integrations.Payment/ // 抽象包——无任何 OptionsBitzsoft.Integrations.Payment.Stripe/ // StripeOptions.cs ← 放这里Bitzsoft.Integrations.Payment.Alipay/ // AlipayOptions.cs命名约定
Options 类命名为 {Vendor}{Domain}Options,sealed 修饰:
| 厂商包 | Options 类名 |
|---|---|
| Payment.Stripe | StripeOptions |
| Payment.Alipay | AlipayOptions |
| TeamWork.DingTalk | DingTalkOptions |
| Finance.Invoice.Nuonuo | NuonuoOptions |
| FileStorage.Aws | AwsS3Options |
字符串字段默认空字符串
所有 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 不做任何配置校验。配置错误(如密钥为空)要到首次 API 调用时才以运行时异常暴露。
风格二:DataAnnotations + IValidatableObject
Beisen 域在 Options 类上用 [Required]、[Range] 特性声明约束,实现 IValidatableObject 做跨字段校验,DI 注册时调用 ValidateDataAnnotations()。
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 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 启动期校验