Skip to content
bitzorcas
中EN

Guide

Master Data Dictionary, Cache, and Localization

Real IDataDictionaryResolver contracts, queries, tenant cache, negative caching, warmup contributor, localization override, collision, and invalidation boundaries.

Last updated

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

yesnoyesno

group + code + culture

tenant cache key

cache hit?

case-insensitive map

active codes by Group

optional Code+Language texts

code exists?

display text

original code

Blank group/code throws ArgumentException; a miss returns the original code, not a failure.

3. Real contract

Single and list resolution
// ① 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

Batch projection
// ① 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

Current 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.

Group A / Class X / Code 01

map key 01

Group A / Class Y / Code 01

duplicate exception or wrong override

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

Terminal window
rg -n "GetOrCreateAsync|CacheScope.Tenant|RemoveByTagAsync|ToDictionary|entryCodes.Contains" src/Platform/MasterData -g '*.cs'
rg -n "UX_SysGeneralCode|SysGeneralCodeText" src/Platform/MasterData -g '*.cs'

Module overview · Testing and GA

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%