- 状态:已接受
- 日期:2026-07-24
- 取代:ADR-0003
背景
.All 聚合包允许同时配置多个厂商,但旧注册使用 TryAddSingleton<TCapability, TProvider>:
- 直接注入共享接口时只有首个 Provider 可见;
IEnumerable<T>也不能恢复被TryAdd丢弃的实现;- 运行时按租户、地区或业务线选择 Provider 没有统一入口;
- Singleton Provider 会捕获 typed/factory HttpClient,形成错误生命周期;
- 包存在、接口实现和真实可用能力之间没有可查询的事实源。
.NET 8 keyed services 能解决部分问题,但本仓库的长期兼容基线包含 net5.0,公共契约不能依赖 keyed DI。
决策
引入目标化 net5.0;net8.0;net10.0 的 Bitzsoft.Integrations.Core:
IntegrationProviderDescriptor是 Provider ID、领域、厂商、API、鉴权、地区、环境、稳定性、限制和真实能力的事实源;IIntegrationProviderCatalog枚举当前容器中所有已配置 Provider;IIntegrationProviderResolver<TCapability>按大小写不敏感的稳定 Provider ID 解析能力;- Provider 跨领域唯一键为
{Domain}:{ProviderId}; - resolver 使用 scoped 字典 Adapter,不依赖 .NET 8 keyed services;
- HTTP Provider 与其客户端默认使用 Transient;只有明确管理有状态长连接并自行保证并发安全的 Adapter 才保留 Singleton;
- 单 Provider 应用仍可直接注入能力接口,作为兼容别名;多 Provider 应用必须使用 resolver;
.All包必须有测试证明每个配置 Provider 可枚举、可按 ID 解析,且只暴露真实能力。
Provider ID 是持久配置值,发布后不得因显示名称或类名变化而修改。
备选方案
- 继续由消费方使用
IEnumerable<T>自行查找:无法解决TryAdd已丢弃实现的问题,也没有统一元数据和错误语义。 - 只使用 .NET 8 keyed services:会破坏 net5.0 兼容。
- 每个领域定义独立 Factory:可以工作,但重复注册、查找、异常和测试逻辑。
- 移除直接接口注入:语义最严格,但会立即破坏现有单 Provider 应用。
后果
正面:
- 多厂商配置不再依赖注册顺序;
- 能力声明可以与运行时解析及支持矩阵校验;
- net5.0、net8.0、net10.0 使用同一公共 API;
- internal 构造函数可通过包内显式工厂安全注册;
- 生命周期可被 DI 集成测试验证。
代价:
- 多 Provider 消费方需要注入 resolver;
- 兼容期内直接能力接口仍代表单 Provider 用法,不能表达路由意图;
- 现有宽接口尚需逐步拆为更细能力,否则 Descriptor 只能记录限制,无法彻底阻止调用厂商不支持的方法。
迁移
旧代码(只配置一个 Provider)可以保持:
public sealed class SingleProviderService(IPaymentProvider payment) { }多 Provider 代码改为:
public sealed class PaymentRouter( IIntegrationProviderResolver<IPaymentProvider> providers){ public IPaymentProvider Resolve(string providerId) => providers.GetRequired(providerId);}撤销条件
只有在 net5.0 兼容策略被新的 ADR 正式结束,且 keyed services 能同时覆盖 Catalog、能力元数据和兼容迁移时,才重新评估底层实现;公共 resolver 契约应尽量保持。