The dictionary resolver is MasterData’s only current application-facing runtime service. It converts persisted codes to display text; it does not provide management, publication, or strongly typed catalog APIs.
1. Registration and dependencies
AddBitzOrcasMasterDataPlatform uses TryAddScoped for IDataDictionaryResolver → MasterDataDictionaryResolver; a prior custom resolver wins.
Dependencies: two owner-local IEntitySet<T> instances, ICacheStore, ICacheKeyBuilder, ICurrentTenant (aligned with BuildForTenant for explicit tenant keys), and a logger.
The type is marked [BitzCacheArea(master-data)]. Optional startup / Host rebuild is filled by MasterDataDictionaryCacheWarmupContributor for the finite key space (enabled dictionary groups × default culture). See the cache area catalog.
2. Single-value resolution
Blank group/code throws ArgumentException; a miss returns the original code, not a failure.
3. Real contract
// ① groupKey is SysGeneralCode.Group, not a group-row object ID.var statusText = await resolver.ResolveAsync( groupKey: "CASESTATUS", code: caseRecord.StatusCode, culture: "zh-CN", cancellationToken);
// ② List input splits commas, trims, removes empties, and keeps order.var tags = await resolver.ResolveListAsync( groupKey: "CASETAG", commaSeparatedCodes: caseRecord.TagCodes, culture: "en-US", cancellationToken);Return the code alongside localized text.
4. Batch resolution
// ① Collect one page's requests to avoid field-by-field calls.var requests = page.Items .SelectMany(item => item.RequiredDictionaryValues()) .Distinct() .ToArray();
// ② The resolver groups by GroupKey; misses remain raw codes.var labels = await resolver.ResolveBatchAsync( requests, currentCulture.Name, cancellationToken);return page.Map(item => item.WithLabels(labels));An empty list returns an empty map; source has no explicit null guard.
5. ListEntries follows another path
ListEntriesAsync queries codes directly, sorts in memory, and fetches localized text. It bypasses the 30-minute group cache, creating a different load and consistency window from Resolve.
Cache dropdown lists at a tenant/culture-aware owner boundary if used per request.
6. Cache policy
// ① Existing groups live for 30 minutes with 10% jitter.private static readonly CachePolicy DictionaryPolicy = new(TimeSpan.FromMinutes(30)){ // ② Missing groups use a one-minute negative TTL. NegativeTtl = TimeSpan.FromMinutes(1), JitterRatio = 0.10, AreaTag = "dictionary"};The CacheScope.Tenant key includes group and culture; null culture becomes default. Correct tenant context remains a prerequisite.
7. Localization override
Default values come from GeneralCode.DisplayName. A non-empty culture other than exact lowercase default queries texts and overrides by Code.
There is no culture normalization, parent-culture fallback, default-language lookup, Class/Group constraint, or translation status/effective period. This differs from I18n’s scoped Translation fallback.
8. Code/Class collision
GeneralCode uniqueness is Tenant+Code+Class, but the final map key is Code. Group filtering does not prove code uniqueness across classes within the group.
Text queries use Code+Language only. Duplicate returned codes make ToDictionary throw.
Align database, seed, and resolver identity before GA.
9. Invalidation semantics
InvalidateCacheAsync(groupKey) always calls RemoveByTagAsync("dictionary"); groupKey only changes the log. Depending on cache implementation, one change may clear every tenant/group or at least far more than requested.
No current MasterData write workflow or integration event invokes this method, so mutation-to-invalidation is not closed.
10. Read consistency
A future management workflow should update plus outbox transactionally, publish Tenant/Group/Version, consume idempotently on every instance, invalidate precisely, and observe propagation/version lag. A shorter TTL alone is not a consistency protocol.
11. Security and permissions
Resolver calls do not enforce DictionaryRead or the feature. Internal display resolution may not need per-call authorization, but a list-all endpoint should enforce feature/permission, IsInternal policy, and rate limits.
Current filters include IsActive/IsDeleted but not inherited IsEnabled/IsInternal or IsProtect.
12. Test matrix
- hit, miss-as-code, and blank input;
- list order, empty values, duplicate codes;
- batch across groups and repeated requests;
- tenant cache-key isolation;
- null/default/case/parent culture;
- same-group cross-class duplicate code;
- duplicate and missing text;
- negative cache and expiry;
- precise/global/multi-instance invalidation;
- Contains translation on both ORMs;
- public policy for internal/disabled/protected rows.
13. Inspection
rg -n "GetOrCreateAsync|CacheScope.Tenant|RemoveByTagAsync|ToDictionary|entryCodes.Contains" src/Platform/MasterData -g '*.cs'rg -n "UX_SysGeneralCode|SysGeneralCodeText" src/Platform/MasterData -g '*.cs'