The nine owner-local persistence records are reference-catalog rows, not domain aggregates. They preserve table compatibility while serving multiple ORMs through compile-time metadata.
1. Why CatalogRecord
The suffix distinguishes a reference row from the old one-table/one-Entity/mapper pattern. Each record carries [BitzTable], [BitzColumn], and [BitzIndex]; the generator emits the model at compile time.
MasterData itself references neither adapter.
2. Shared baseline
Every record inherits BizEntityBase, including ID, TenantId, OfficeId, audit, enabled, soft-delete, and internal flags. Every table enables tenant and soft-delete metadata.
Consequences:
- platform reference assets are still rows, commonly with TenantId=0;
- queries rely on adapters applying tenant/soft-delete filters and should not bypass
IEntitySet<T>.
3. Natural keys and indexes
| Table | Key/index | Concern |
|---|---|---|
| SysLanguage | Tenant + Code unique | one-default rule is not modeled |
| SysCountry | Tenant + Alpha2 unique | Alpha2 remains nullable |
| SysExchangeRate | Tenant + pair + ModifyTimee unique | model/CSV name mismatch |
| SysGeneralCodeGroup | Tenant + Code unique | no hierarchy FK/cycle rule |
| SysGeneralCode | Tenant + Code + Class unique | resolver reads by Group |
| SysGeneralCodeText | Tenant + Code + Class + Language index | index is not unique |
| SysIndustrySetting | Tenant + ParentCode index | Code is not unique |
| SysPublicHoliday | Tenant + Country + Year index | seeder uses Country + DayDate |
| SysTranslation | Key + Lang + Scope + Tenant + Office unique | consumed by I18n |
Database constraints and seed matchers are not always equivalent.
4. Metadata declaration
// ① Owner source declares table metadata for compile-time generation.[BitzTable("SysLanguage", IsTenant = true, IsSoftDelete = true)][BitzIndex("UX_SysLanguage_Tenant_Code", "TenantId", "Code", IsUnique = true)]public sealed class SysLanguageCatalogRecord : BizEntityBase{ // ② BCP-47 code is stable; display text may evolve. [BitzColumn(Length = 20, IsRequired = true)] public string Code { get; set; } = string.Empty;
[BitzColumn(Length = 120)] public string? DisplayName { get; set; }}The example trims non-essential members from the same source type.
5. ORM-neutral query
// ① Expressions must translate on every supported ORM.var entries = await codes.ListAsync( row => row.Group == groupKey && row.IsActive && !row.IsDeleted, cancellationToken);
// ② Sorting is currently in memory.var ordered = entries.OrderBy(row => row.Sort).ToList();return ordered;The explicit soft-delete predicate is defensive duplication, not proof of cross-tenant authorization.
6. Three kinds of uniqueness
SysGeneralCodeText is inconsistent: its index includes Class, while seed matching and resolver mapping use only Code+Language/Code.
7. SysTranslation and I18n
MasterData physically owns Language/Translation rows; I18n owns application contracts, repository, scoped overrides, and query API. I18n Infrastructure directly references MasterData Infrastructure because there is no MasterData Contracts project.
If independent packaging becomes necessary, extract a narrow owner-model contract package instead of copying table types.
8. Rules not implemented by records
- authority revision and effective period for country/currency/industry;
- rate source, quote time, precision, and inverse rules;
- working-day adjustments and holiday issuing authority;
- dictionary group FK, cycle, and orphan prevention;
- one default language per tenant;
- protected-row write prevention;
- publication approval and historical versions.
Column attributes cannot provide these business rules.
9. Deciding where a catalog belongs
Ask whether it is shared across modules, needs a stable code and authority, is tenant-overridable or global, needs I18n/effective dating/replay, and has a clear owner. Module-private status enums generally stay with the owning module.
10. New record skeleton
// ① Declare owner-local tenant and soft-delete semantics.[BitzTable("SysExampleCatalog", IsTenant = true, IsSoftDelete = true)][BitzIndex("UX_SysExample_Tenant_Code", "TenantId", "Code", IsUnique = true)]public sealed class SysExampleCatalogRecord : BizEntityBase{ // ② Required natural key follows the authoritative standard. [BitzColumn(Length = 32, IsRequired = true)] public string Code { get; set; } = string.Empty;
[BitzColumn(Length = 160, IsRequired = true)] public string DisplayName { get; set; } = string.Empty;}Then add metadata, dual-ORM parity, seed matching, asset integrity, and documentation gates.
11. Test evidence
Architecture tests keep legacy paths deleted, require nine records/eight assets, enforce ORM neutrality and EntitySet seeders, verify host composition/resolver dependencies, and inspect generated metadata. Cross-ORM suites register these types for parity and end-to-end seeding.
They do not prove catalog business rules, production freshness, or management authorization.
12. Inspection
# Tables and indexes.rg -n "BitzTable|BitzIndex|class .*CatalogRecord" src/Platform/MasterData -g '*.cs'
# No concrete ORM dependency should appear.rg -n "SqlSugar|EntityFrameworkCore|Infrastructure.EfCore" src/Platform/MasterData -g '*.cs' -g '*.csproj'
# New records must enter the generated-metadata gate.rg -n "Owner_Assembly_Should_Emit_Metadata" tests/BitzOrcas.Architecture.Tests/MasterDataInfrastructureArchitectureTests.cs