Skip to content

CHR0023: Ambiguous parent key for [ChildrenFrom] collection

When a [ChildrenFrom] collection omits its parentKey argument, Chronicle infers the parent key by matching the parent read model’s identifier type against the child event’s properties (excluding the child key). When more than one property still matches that type, the inference is ambiguous — it would resolve by declaration order, which is fragile.

Specify the parentKey argument to make the relationship explicit.

Warning

using Cratis.Chronicle.Events;
using Cratis.Chronicle.Keys;
using Cratis.Chronicle.Projections.ModelBound;
public record Chr0023Ledger(
[Key] Guid Id,
// Warning CHR0023: Chr0023EntryRecorded has two Guid-typed properties (LedgerId and
// OffsetLedgerId), so Chronicle cannot infer which one links an entry to its parent ledger.
[ChildrenFrom<Chr0023EntryRecorded>(key: nameof(Chr0023EntryRecorded.EntryId))]
IEnumerable<Chr0023Entry> Entries);
[EventType]
public record Chr0023EntryRecorded(Guid EntryId, Guid LedgerId, Guid OffsetLedgerId, decimal Amount);
public record Chr0023Entry([Key] Guid EntryId, decimal Amount);
public record Chr0023LedgerFixed(
[Key] Guid Id,
// parentKey names the event property that links the child back to its parent, so the
// relationship is explicit and no longer depends on declaration order.
[ChildrenFrom<Chr0023EntryRecordedFixed>(
key: nameof(Chr0023EntryRecordedFixed.EntryId),
parentKey: nameof(Chr0023EntryRecordedFixed.LedgerId))]
IEnumerable<Chr0023EntryFixed> Entries);
[EventType]
public record Chr0023EntryRecordedFixed(Guid EntryId, Guid LedgerId, Guid OffsetLedgerId, decimal Amount);
public record Chr0023EntryFixed([Key] Guid EntryId, decimal Amount);

A [ChildrenFrom] collection projects child rows from an event stream and attaches them to a parent read model. Chronicle needs to know which event property carries the parent identifier so it can place each child under the right parent.

When parentKey is omitted, Chronicle infers it: it takes the parent read model’s identifier type and looks for a single event property of that type (the child key property is excluded, since one property can’t be both the child key and the parent key). If exactly one property matches, the inference is safe. If two or more match, the choice is ambiguous — the projection would silently pick one by declaration order, so reordering the event’s parameters, or adding another property of the same type later, could quietly re-point every child at a different parent.

Naming parentKey explicitly removes that fragility: the relationship is stated in the read model, survives refactoring of the event, and makes the intent obvious to readers.