Skip to content
bitzorcas
中EN

Reference

Master Data Records, Metadata, and ORM Boundary

Tables, tenant/soft-delete behavior, natural keys, compile-time metadata, IEntitySet, and dual-ORM ownership for nine CatalogRecords.

Last updated

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.

CatalogRecord + Bitz attributes

Metadata generator

Generated manifest

SqlSugar adapter

EF Core adapter

same physical table

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

TableKey/indexConcern
SysLanguageTenant + Code uniqueone-default rule is not modeled
SysCountryTenant + Alpha2 uniqueAlpha2 remains nullable
SysExchangeRateTenant + pair + ModifyTimee uniquemodel/CSV name mismatch
SysGeneralCodeGroupTenant + Code uniqueno hierarchy FK/cycle rule
SysGeneralCodeTenant + Code + Class uniqueresolver reads by Group
SysGeneralCodeTextTenant + Code + Class + Language indexindex is not unique
SysIndustrySettingTenant + ParentCode indexCode is not unique
SysPublicHolidayTenant + Country + Year indexseeder uses Country + DayDate
SysTranslationKey + Lang + Scope + Tenant + Office uniqueconsumed by I18n

Database constraints and seed matchers are not always equivalent.

4. Metadata declaration

Catalog record
// ① 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

Dictionary-row 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

noyes

database unique key

matches seed and resolver keys?

seed matcher

resolver map key

wrong overwrite / duplicate / map exception

stable catalog semantics

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

Reference-record checklist
// ① 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

Terminal window
# 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

Module overview · Seeding

100%

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