字典解析器是 MasterData 当前唯一面向应用的运行时服务。它把持久化 code 转为显示文本,但没有提供管理、发布或强类型目录 API。
1. 注册与依赖
AddBitzOrcasMasterDataPlatform 使用 TryAddScoped 注册 IDataDictionaryResolver → MasterDataDictionaryResolver。已有自定义注册不会被覆盖。
Resolver 依赖:
IEntitySet<SysGeneralCodeCatalogRecord>;IEntitySet<SysGeneralCodeTextCatalogRecord>;ICacheStore;ICacheKeyBuilder;ICurrentTenant(与BuildForTenant对齐显式租户键);- Logger。
类型标注 [BitzCacheArea(master-data)]。可选启动 / Host 重建由 MasterDataDictionaryCacheWarmupContributor 写入有限键空间(启用字典组 × 默认 culture);见缓存区域目录。
2. 单值解析
group/code 空白会抛 ArgumentException,未命中不是 Result failure,而是原 code。
3. 使用真实契约
// ① groupKey 对应 SysGeneralCode.Group,不是 Group 表的对象 ID。var statusText = await resolver.ResolveAsync( groupKey: "CASESTATUS", code: caseRecord.StatusCode, culture: "zh-CN", cancellationToken);
// ② 列表输入按逗号拆分、Trim、移除空项并保持顺序。var tags = await resolver.ResolveListAsync( groupKey: "CASETAG", commaSeparatedCodes: caseRecord.TagCodes, culture: "en-US", cancellationToken);调用方仍应返回 code,不能只返回解析后的自然语言文本。
4. 批量解析
ResolveBatchAsync 按 request.GroupKey 分组,每个组加载一次映射,再以 DictionaryResolveRequest 为键返回结果。
// ① 先收集一页结果所需的字典请求,避免逐字段调用。var requests = page.Items .SelectMany(item => item.RequiredDictionaryValues()) .Distinct() .ToArray();
// ② Resolver 内部按 group 加载;未命中仍返回原 code。var labels = await resolver.ResolveBatchAsync( requests, currentCulture.Name, cancellationToken);return page.Map(item => item.WithLabels(labels));空 requests 返回空字典;源码没有显式 null 参数防护。
5. ListEntries 的不同路径
ListEntriesAsync 直接查询 codes.ListAsync,按 Sort 内存排序,再查询本地化文本。它没有复用 30 分钟的 group cache。
这意味着 Resolve 与 ListEntries 在高并发下具有不同的数据库负载和一致性窗口。产品若用 ListEntries 渲染每次请求的下拉框,应增加缓存或调用方缓存,并保持租户/文化维度。
6. 缓存策略
// ① 正常字典组缓存 30 分钟,并加入 10% jitter。private static readonly CachePolicy DictionaryPolicy = new(TimeSpan.FromMinutes(30)){ // ② 缺失组使用 1 分钟 negative TTL,降低穿透。 NegativeTtl = TimeSpan.FromMinutes(1), JitterRatio = 0.10, AreaTag = "dictionary"};key 通过 CacheScope.Tenant 构造,包含 group 和 culture(null 变成 default)。这依赖正确的当前租户上下文。
7. 本地化覆盖
默认映射来自 GeneralCode 的 DisplayName。culture 非空且不等于小写精确值 default 时,再查询 Text 表,以 Code 对应 DisplayName 覆盖。
没有:
- culture 大小写/BCP-47 规范化;
zh-Hans-CN → zh-Hans → zh回退;- 默认语言目录回退;
- Class/Group 约束;
- 翻译状态/有效期。
这套字典本地化与 I18n 的 Translation scope fallback 是两条不同链路。
8. Code/Class 碰撞
GeneralCode 数据库唯一键是 Tenant+Code+Class,但 Resolver 最终映射键只有 Code。查询先按 Group 缩小范围,仍不能保证同组不同 Class 没有相同 Code。
Text 查询只按 entryCodes.Contains(text.Code) && text.Language == culture,忽略 Class、Group 和 tenant 显式字段。若返回同 Code 多行,ToDictionary(text => text.Code) 会抛异常。
GA 前必须统一数据库、seed 与 resolver 的复合身份。
9. 失效语义
InvalidateCacheAsync(groupKey) 无论是否传 group,都调用 RemoveByTagAsync("dictionary")。group 只用于日志。若 CacheStore tag 是全局范围,一次租户/组变更可能清除所有字典缓存;即使实现局部化,也不是精确 group 失效。
当前 MasterData 没有写入用例或集成事件触发该方法。谁修改数据、何时失效、跨实例如何传播尚未形成闭环。
10. 读取一致性
30 分钟 TTL 意味着数据库变更与显示之间可能有延迟。管理能力未来应采用:
- 事务内更新记录和 outbox;
- 提交后发布带 Tenant/Group/Version 的失效事件;
- 每实例幂等消费并精确失效;
- Resolve 返回可选 catalog version;
- 监控失效延迟与旧版本命中。
仅缩短 TTL 会增加数据库压力,不能替代一致性协议。
11. 安全与权限
字典读取本身不检查 MasterDataPermissions.DictionaryRead 或 Feature。多数显示解析可能作为内部服务无需逐次授权,但“列出全部字典”可能泄漏内部/受保护值,公开 Endpoint 仍需权限、Feature、IsInternal 过滤和速率限制。
当前 Resolver 过滤 IsActive/IsDeleted,却没有过滤继承的 IsEnabled/IsInternal,也没有检查 IsProtect。
12. 测试矩阵
- 命中、未命中返回原 code、空参数;
- 列表顺序、空项、重复 code;
- batch 跨 group 与重复 request;
- tenant cache key 隔离;
- default/null/大小写/父 culture;
- 同 group 跨 class 重复 code;
- text 重复与缺失;
- negative cache 与到期;
- 精确/全量/跨实例失效;
- 两个 ORM 的 Contains 表达式等价;
- IsInternal/IsEnabled/IsProtect 公开策略。
13. 核查命令
# 当前查询键和缓存策略。rg -n "GetOrCreateAsync|CacheScope.Tenant|RemoveByTagAsync|ToDictionary|entryCodes.Contains" src/Platform/MasterData -g '*.cs'
# 检查重复 Code/Class/Group 数据需要在导入验证器或数据库报告中执行。rg -n "UX_SysGeneralCode|SysGeneralCodeText" src/Platform/MasterData -g '*.cs'