---
title: 'CHR0023: Ambiguous parent key for [ChildrenFrom] collection'
---

import { Tabs, TabItem } from '@astrojs/starlight/components';

## Rule Description

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.

## Severity

Warning

## Example

### Violation

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
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);
```

</TabItem>
</Tabs>

### Fix

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
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);
```

</TabItem>
</Tabs>

## Why This Rule Exists

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.

## Related Rules

- [Model-bound children](/chronicle/projections/model-bound/children/) — the full `[ChildrenFrom]` reference.
