v0.14.0 — everything that governs an entity, under the entity
Everything that governs an entity, under the entity.
0.13.0 gave each entity the columns it persists and stopped one door short. What is written on a property travelled with the property — a folded Number row correctly said required — and what is written on the class did not. So an entity's section could call a column required and, three headings later, document no rule requiring it: two halves of the same page disagreeing, with the property half telling the truth.
The rules were never missing from the application, only from the place a reader looks. A RuleCriteria on an audit base is enforced every time anything in the application is saved. An [Appearance] greys a field on every screen below it. An association gives every descendant a collection that really is populated. All three were documented under the base alone — which, on a real application, means documented nowhere anybody reads.
Receipt now reads:
### Relationships
- **AuditEntries** -> `AuditEntry` (One to many) (composition/aggregation) — inherited from `AuditedObject`
### Validation Rules
- **RuleCriteria** `Audit_ChangedNotBeforeCreated`: `ChangedOn >= CreatedOn` — “A record cannot be changed before it was created.” — inherited from `AuditedObject`
- **RuleRequiredField** in `CreatedBy` `Audit_CreatedByRequired` — inherited from `AuditedObject`
### Appearance Rules
- **Audit_ReadOnlyOnceVersioned** — when `RowVersion > 0`: enabled=false — inherited from `AuditedObject`
Marking is not decoration. InheritedFrom had been recorded on properties since 0.13.0 and displayed by nothing, so folding rules on top of that would have traded one defect for a quieter one: every rule visible, and no way to tell which of them changing would change the whole application.
On scale. #14 filed half of itself as genuinely debatable, and it turned out to be a question about scale rather than about relationships. An index, a count, a diagram and a search answer what does this application declare; an entity's section answers what governs this entity. Following the fold everywhere would have made every total a measurement of the class hierarchy — one rule on a base shared by two hundred entities, reported two hundred times. Each folded declaration carries the class that wrote it, and each rendering chooses.
Found while probing this. The four-argument rule attribute was misread. [RuleCriteria("id", DefaultContexts.Save, "Total >= 0", "A sale total cannot be negative.")] put the message in the field holding what the rule enforces and left the message field empty. Every fixture in the suite passed its message as CustomMessageTemplate =, so all 299 tests agreed with the wrong answer. Rules now also keep their identifier and their validation contexts, which were read as arg0/arg1 and printed to the published documentation that way.
Thanks again to @MBrekhof, who filed #14 separately rather than growing the pull request it was found in.
A minor rather than a patch: ExtractedValidationRule gains Id and Contexts; ExtractedValidationRule, ExtractedAppearanceRule and ExtractedRelationship gain InheritedFrom and Clone(). Additive, and invisible to the CLI and the MCP server.
318 tests. Full notes in the changelog.