Skip to main content

Creating a register

:::info Status Outline — expand with a full worked example (e.g. adding a new BalanceRegister<> and wiring a document to write it). :::

:::tip Use the shipped skill Prefer the create-register skill (in your scaffolded repo's .claude/skills/) over hand-rolling this — it covers all four register families in one skill: Balance, Turnover, and both document-bound and independent/periodic Info registers. :::

Registers are analytical storage with no UI/form of their own — they exist purely to be written by document behaviors and read by reports or the generated register-transactions viewer.

The three families

TypeStatusUse for
BalanceRegister<>Implemented, activeRunning totals (inventory balances, exchange rates)
Info-register family (InfoRegister/PeriodicInfoRegister/DocumentInfoRegister)Implemented, activePoint-in-time or periodic scalar facts tied to a document/date
TurnoverRegister<>Infrastructure exists, no live consumer yetPeriod-bucketed movement totals

Unlike form-bearing entities, a register has no *Dto/*Validator/generated UI of its own — just the *Base register type plus a Dimensions type implementing IRegisterDimensions<TSelf> (which the Equality generator fills in BuildEqualityPredicate() for).

Wiring a document to write it

Register writers are resolved only via ISubmitScope.GetWriter<TWriter>() inside OnSubmitAsync/OnDeleteAsync — never by constructor injection. See Overall architecture for why this chokepoint exists.

Gotchas worth knowing before you start

  • Concurrency uses a manually-incremented Version column with bounded (3-attempt) retry, not a lock-free atomic upsert — a deliberate scope-down (techdebt/TECH_DEBT.md TD-004), fine at current traffic levels but worth knowing if you're debugging a retry-exhausted failure under heavy concurrent writes to the same dimension row.
  • Use DimensionPredicateBuilder + the generated BuildEqualityPredicate() to filter to just the touched dimension rows — don't hand-roll a reflection-based filter.
  • Every Document needs IEntityWithRows even if it has no tabular lines, if it participates in the register-transactions viewer.

See also