The child entity type.
The child entity type.
The parent entity.
The collection declaration.
ReadonlyOwnerThe entity this companion is attached to. Set by BaseEntity.RegisterCompanion.
Number of retained related records.
Deliberately delegates to Items rather than reading the backing array: for a 'lazy'
collection Items is what triggers population, so reading the raw array here would report 0
for a collection that has simply not been touched yet — and Count === 0 while
Items.length === 2 is the kind of inconsistency nobody debugs quickly. Same reason it picks
up a live cache view's refresh.
True when saving would produce work: any retained child is dirty or unsaved, or any removal is pending.
Whether reading Items right now will succeed — the guard for display-tier code.
A lazy collection's Items getter throws when its donor engine is not available,
deliberately: a silently empty array is how the bug this feature replaced went unnoticed for
years. That is the right default for business logic, but a template or widget rendering
during bootstrap (before anything has awaited the engine's Config()) wants "not yet",
not an aborted render — and the null-check templates reach for (@if (entity.Params && …))
cannot help, because the collection property itself is never null; it is the read that
throws.
@if (action.Params.IsAvailable) {
@for (p of action.Params.Items; track p.ID) { … }
}
This is a predicate, not a second way to read. There is exactly one accessor — Items —
so there is no null-versus-[] ambiguity for a caller to get wrong, and no quiet path that
can drift into business logic and re-create the silent-empty bug. true here means the very
next Items read is safe and already populated, because deciding the answer requires
consulting the donor, and consulting it is what fills the collection.
Never triggers a database load, and never throws.
Whether this collection has been populated from the database.
Whether this collection refuses mutation.
Defaults to false, except for a cache-sourced collection, which defaults to true
because its records are the engine's own shared instances. An explicit ReadOnly: false
still wins — and switches the cache path to copying, so the engine's objects stay untouched.
When this collection populates itself.
Stable identifier for this companion, unique within its owning entity.
This is the wire key: it appears in serialized payloads and is how the receiving tier finds the companion to deserialize into. Treat it as a published contract — renaming it breaks in-flight payloads and any persisted snapshot that captured them.
The OrderBy clause applied when loading, if declared.
The child field holding the foreign key back to the parent.
The child entity's name in MJ metadata.
What removal means for this collection.
Children removed since the last load or save, awaiting deletion on the next save.
Always empty when RemovalMode is 'orphan'.
Where this collection's records come from. Defaults to 'database'.
Iterates the retained records, so the collection works directly with for…of, spread and
array destructuring:
for (const param of action.Params) { … }
const all = [...action.Params];
const [first, ...rest] = action.Params;
This is the standard ES2015 iterable protocol — the same one Map, Set and NodeList
implement — deliberately chosen over extending Array. Subclassing Array would inherit
push, splice, sort and index assignment, every one of which bypasses the removal
tracking, foreign-key stamping and sequence renumbering this class exists to guarantee; and
Symbol.species would hand map/filter this constructor, which takes an owner and options
rather than a length. Iterability adds the ergonomics without any of that.
Use Items when you want the array itself — map, filter, find and indexing.
It is readonly, which is what keeps a caller from mutating around the collection's back.
An iterator over the retained records, in collection order.
Resets the companion to its post-save state — clearing pending removals, rebasing dirty tracking, and so on. Called after the graph commits successfully.
Appends an existing child entity to the collection.
The foreign key is not set here — it is stamped at save time, because when the parent is itself new its primary key does not exist yet. See ContributeSaveWork.
The child to append.
The same child, for chaining.
Rebinds the provider used by this companion and its child entities. Subclasses with child entities override this to propagate the provider.
Removes every child.
Contributes this companion's work to the owner's delete plan.
Called before the owner's own node, because children must generally be removed before the parent row they point at. Implementations that rely on database-level cascade delete should contribute nothing.
The plan being assembled for this unit of work.
Contributes work that must run after the owner's own delete node.
Related-record collections delete first (they point at the owner). Embedded records delete last (the owner points at them — the FK dies with the owner row, then the peer can go). Default is a no-op.
The plan being assembled; the owner's delete node already exists.
Contributes this companion's work to the owner's save plan.
Called after the owner's own node has been added, so implementations may assume the parent node exists and order their nodes relative to it. Add nothing when there is no work — an empty contribution keeps the save on the fast single-row path.
The plan being assembled for this unit of work.
Optionaloptions: EntitySaveOptionsThe caller's save options. Implementations that decide what counts as work
(skipping clean children, most importantly) must honor flags such as
IgnoreDirtyState that demand a full write-out.
Creates a new, empty child entity, appends it, and returns it.
Uses the owner's provider so the child resolves to the right registered subclass on whichever tier this runs — the server subclass on the server, the shared subclass in the browser.
The newly created child.
Restores this companion's state from a wire payload produced by Serialize on the other tier.
Implementations must be tolerant of payloads written by an older version of themselves — a companion is a wire contract, and rolling deploys mean both versions run at once.
The payload previously produced by Serialize.
Whether this payload is an inbound request or an authoritative result. See EntityCompanionDeserializeMode; the distinction is load-bearing.
Populates the collection from the database.
A no-op when the parent is unsaved (there is nothing to be a child of) or when
LoadMode is 'never'.
Reload even if already loaded, discarding any unsaved children and removals.
A failed load throws rather than yielding an empty collection. Silently returning no children makes a populated parent look empty, and anything derived from that — a reversal, a total, a validation decision — is then wrong in a way nothing downstream can detect. Only saves use the boolean-return convention.
Loading over UNSAVED WORK also throws, for the same reason. Add() and Create() do not mark
a collection loaded, so a collection that has only ever been appended to still has
loaded === false — and the early return above therefore does not protect it. A Load()
from anywhere (a lazy read, a refresh, a sibling component) would replace items wholesale
and take the caller's unsaved children with it, along with any queued deletions. Nothing
reports that; the screen simply shows fewer rows than the user typed.
Pass force to discard deliberately — that is what a refresh means, and saying so is cheap.
Populates this companion from the database, when it is configured to load eagerly.
Called by BaseEntity.Load() after the record's own fields are populated. Never called
from LoadFromData() — that is the row-materialization path for
RunView(ResultType:'entity_object'), so loading children there turns one view into an N+1
storm. Set-oriented eager loading is handled by RunView's batched child loading instead.
Optional_visited: Set<string>EntityName:PK tokens already on this load walk. Embedded records use it to fail a self-parented / cyclic inherit instead of recursing until the stack dies.
Removes a child by instance or index.
A child that was already persisted is queued for deletion when RemovalMode is
'delete'; an unsaved child is simply dropped, since there is nothing to delete.
When RemovalMode is 'refuse'.
Replaces an existing item in the collection with another item (e.g. replacing a generic base entity with its hydrated polymorphic IS-A leaf entity instance).
True if the item was found and replaced, false otherwise.
Produces this companion's JSON-safe state for transport, or null when it has nothing to
send.
Returning null keeps the companion out of the payload entirely, which matters: a save that
touches only header fields should not ship an empty children array and pay for it on every
request.
'request' (default) is the caller's intent — omit a clean saved
companion so a header-only edit does not ship it. 'result' is
post-save state the client must adopt so the next save does not
re-INSERT a peer the server already persisted.
The wire payload, or null to omit this companion.
Replaces the collection's contents with rows already fetched elsewhere.
Used by RunView's batched child loading, which issues one WHERE fk IN (...) for an entire
result set and distributes the rows — turning what would be N+1 queries into 1 + K.
The children belonging to this parent.
Attempts to populate this collection from a BaseEngine cache without touching the database.
Used by BaseEntity.LoadRelatedRecords() to resolve the free collections before batching
whatever is left into a database round trip.
True when the collection was populated from a cache; false when the caller must load it from the database.
Synchronous, in-memory validation contributed by this companion.
Runs as part of the owner's Validate(), before any write, over the companion's complete
state — including pending removals. That ordering is what lets cross-child invariants such as
"debits must equal credits" be enforced correctly rather than after half the graph has landed.
Push errors onto result.Errors and set result.Success = false to fail the save.
The accumulating validation result to contribute to.
Asynchronous validation contributed by this companion — anything that needs a round trip.
The accumulating validation result to contribute to.
Unlike an entity's own ValidateAsync(), this is not governed by
BaseEntity.DefaultSkipAsyncValidation. That flag exists so an entity can opt out of its own
expensive async rules; applying it to companions silently skipped cross-child invariants,
which is how OrderEntityServer.ValidateAsync came to be dead code on every save. Companion
validation runs whenever the companion is dirty.
A typed collection of child records that travels, validates and persists with its parent.
Obtain one via
BaseEntity.DeclareRelatedRecords()in a subclass constructor or field initialiser — do not construct it directly, or it will not be registered as a companion and will be silently ignored by load, validation and save.Example: Declaring a collection on a shared (client + server) entity subclass