本约定统一生成器管理的注册表的 key 选型与命名,确保消费方在跨项目、跨模块调用 key 时不漂移。它不是新功能,只是把现有 [RegisterFactory] / [RegisterStrategy] / [HandlerOrder] 的隐含规则显式化,供生成器产物({Contract}Keys)与人工 Builder 在同一语境下协作。
范围: 字符串 key(
[RegisterFactory("...")]/[RegisterStrategy("...")])。不覆盖 enum / Type / int 等强类型 key(当前生成器尚不支持;若引入会另开文档)。
| 维度 | 规则 | 说明 |
|---|---|---|
| 大小写 | 推荐 kebab-case 或 lower_snake_case |
DP025 字面量键校验对精确字面量做匹配;大小写不一致会被判为「未知 key」。kebab-case 视觉分组最稳 |
| 字符集 | 仅 [a-z0-9_-] |
避免空白、点、斜杠、反斜杠、Unicode(防止 JSON 序列化 / 文件名 / 环境变量传参时出错) |
| 前缀 | 推荐按业务域 / 子系统加 2-4 字符前缀 |
防止不同模块用同一个扁平 key 空间互踩(如 format vs payment 的 refund) |
| 不可变 | 一旦 [RegisterFactory] 标注,key 即成该实现的公共契约;改 key 等于改公共 API |
改 key 视作 breaking change,按 SemVer 处理 |
| 去重 | 同一契约下不可重复(DP020/DP003 等由生成器 Error 拦下) | Builder 手动 Register 时抛 ArgumentException |
例:
"format:json"、"image:convert"、"http-client-retry"、"auth-oauth2-callback"(前缀auth-区分其它子系统)。
当业务天然有两层结构(「按工具找能力」、「按租户找策略」、「按路由找处理器」),不要把两层拍扁成一个 key,也不要在生成器层做 native 二维 attribute — 而是用单层 key + Consumer 侧分组适配器(下文 § 4)。
[RegisterFactory<ITextFormatter>("format:json")]
public sealed class JsonTextFormatter : ITextFormatter { ... }
[RegisterFactory<ITextFormatter>("format:xml")]
public sealed class XmlTextFormatter : ITextFormatter { ... }
[RegisterFactory<IImageCapability>("image:convert")]
public sealed class ConvertCapability : IImageCapability { ... }- 单一契约
IAnyToolCapability(或等价根接口) - 单层 key,生成器照常工作(DP020 / DP025 校验照常生效)
- 消费方在
Internal/CapabilityOuterRegistry(类似形态)按:切前缀分组
| 备选 | 缺点 |
|---|---|
[RegisterFactory<Outer, Inner, T>] |
破坏现有 {Contract}Keys / {Contract}Registry 形态;所有 SG 路径要重写;DP026 占用 |
嵌套 IFactoryRegistry<string, IFactoryRegistry<string, T>> |
失去单一 key 字面量校验;DP025 无法作用在内层;Builder API 不一致 |
多契约注册([RegisterFactory<ITextFormatter>] [RegisterFactory<IImageCapability>] 写在同一类) |
一个实现承担多身份,与"一个类一个职责"冲突;SG 难以去重 |
底线: 库的二维概念不进入公共 API。Consumer 用单层 key + 分组适配器拼出二维;库侧零改动。
AnyTool 现有 PluginRegistry._capabilitiesByToolAndContract(二维 dict)用 toolId(外)+ capabilityId(内)。改用本约定:
| 维度 | 改前 | 改后 |
|---|---|---|
| 契约 | ITextFormatter / IImageCapability(两个根) |
IAnyToolCapability(一个根) |
| Key | toolId 与 capabilityId 各自一个字段 |
"format:json" / "image:convert" 单层 |
| 分组 | AnyTool 自己写二维 dict | SG 产 {Capability}Keys / {Capability}Registry,AnyTool 写 Internal/CapabilityOuterRegistry 按 : 切前缀 |
不强制形态,但强烈推荐放在 Internal/ 命名空间、标 internal,避免污染 Consumer 公共 API:
namespace AnyTool.Core.Plugins.Internal;
internal sealed class CapabilityOuterRegistry
{
private readonly IReadOnlyDictionary<string, IFactoryRegistry<string, IAnyToolCapability>> _byTool;
public CapabilityOuterRegistry(IFactoryRegistry<string, IAnyToolCapability> inner)
{
ArgumentNullException.ThrowIfNull(inner);
_byTool = inner.Keys
.GroupBy(ExtractToolId, StringComparer.OrdinalIgnoreCase)
.ToFrozenDictionary(
g => g.Key,
g => BuildSubRegistry(inner, g),
StringComparer.OrdinalIgnoreCase);
}
public IFactoryRegistry<string, IAnyToolCapability>? TryGet(string toolId) =>
_byTool.TryGetValue(toolId, out var sub) ? sub : null;
private static string ExtractToolId(string key)
{
var i = key.IndexOf(':');
return i < 0 ? key : key[..i];
}
private static IFactoryRegistry<string, IAnyToolCapability> BuildSubRegistry(
IFactoryRegistry<string, IAnyToolCapability> inner, IGrouping<string, string> keys)
{
var builder = new FactoryRegistryBuilder<string, IAnyToolCapability>();
foreach (var k in keys)
{
builder.Register(k, _ => inner.Create(k));
}
return builder.Build();
}
}要点:
- 接收已构造的
IFactoryRegistry,不参与 Builder 链 - 分组逻辑在 ctor 一次性完成;之后
TryGet是 O(1)(net8.0 上FrozenDictionary优化) - 不写 SG、不改库;
netstandard2.0退化为Dictionary
DP025 校验生成器管理的注册表(IStrategyRegistry / IFactoryRegistry)调用点的字面量字符串 key;常量字符串(由 {Contract}Keys 提供)始终放行,任意裸字符串若与已知 key 不匹配会触发 Info 级诊断并提供「最近键」CodeFix。
复合 key "format:json" 同样受 DP025 保护 — 它就是一个普通字符串,生成器把它放进 {Capability}Keys.FormatJson 常量,DP025 校验照常工作。外层前缀("format")是 Consumer 解析的细节,生成器/Analyzer 不可见。
| 特性 | key 维度 | 命名规则 |
|---|---|---|
[RegisterFactory] |
字符串(常带 : 分组) |
本约定 |
[RegisterStrategy] |
字符串(常带 : 分组) |
同上,DP006 + DP025 同样保护 |
[HandlerOrder] |
整数 Order(非字符串) | 见 ChainOfResponsibility.md;不适用本约定 |
[Decorator] / [CompositePart] |
整数 Order(同 Handler) | 同上 |
[GenerateSingleton] |
无 key(单例) | 不适用 |
- 不做原生二维 key attribute(
[RegisterFactory<Outer, Inner, T>])。见 § 2 表。 - 不做 enum key。当前生成器仅接受
string;若要 enum,需扩RegisterFactoryAttribute+ SG,经 F2/F3 评审。 - 不做运行时 key 重命名。改 key 即改公共 API,人工迁移。
- 不做自动前缀注入。前缀由 Consumer 显式写出,避免生成器越权改 key。
新增 / 修改 [RegisterFactory] / [RegisterStrategy] 标注时,过一遍:
- key 全小写 + 数字 +
-或_,无其它字符 - key 含业务前缀(跨模块时)
- 复合语义用
:分组(若有 outer/inner 概念) - 改 key = 改公共 API = PR 描述里点出 breaking
- DP025 不在调用点报「未知 key」(调用方用
{Contract}Keys.*常量) -
Directory.Build.propsTreatWarningsAsErrors=true通过(零警告) - 单测覆盖:重名 key 抛错 / key 大小写不敏感 / 复合 key 前缀分组正确
FactoryRegistry.md—[RegisterFactory]SG 产出与诊断Strategy.md—[RegisterStrategy]SG 产出与诊断AGENTS.md§ 诊断 ID 规范 — DP020–DP025 与 DP026 下一个可用 IDROADMAP.md— F1/F2/F3 路线图- #100 — AnyTool 二维 capability 讨论