Skip to content

Nested Objects

Nested model-bound projections populate a single nullable child object on a read model. Use them when an event should set, update, or clear one scalar object inside the parent, rather than managing a collection of child items.

The current model-bound nested API is available in the .NET client. TypeScript currently rejects @nested during projection registration, and the Kotlin and Elixir clients do not expose an equivalent model-bound nested API.

Mark the nullable parent property as nested. The nested type declares the events that populate it and, optionally, the events that clear it.

Nested object lifecycle
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Keys;
using Cratis.Chronicle.Projections.ModelBound;
[EventType]
public record CommandSetForNestedBasic(string Name, string Schema);
[EventType]
public record CommandClearedForNestedBasic;
[FromEvent<CommandSetForNestedBasic>]
public record SliceWithNestedCommandBasic(
[Key] Guid Id,
string Name,
[Nested] CommandItemNestedBasic? Command);
[FromEvent<CommandSetForNestedBasic>]
[ClearWith<CommandClearedForNestedBasic>]
public record CommandItemNestedBasic(
string Name,
string Schema);

The lifecycle is:

  1. The first populate event creates the nested object.
  2. Later populate events update the existing nested object.
  3. A clear event sets the parent property back to null.

The nested marker belongs on the single nullable property that holds the child object.

Nested property on the parent
using Cratis.Chronicle.Projections.ModelBound;
public record ParentWithNestedProperty(
[Nested] NestedPropertyChild? Child);
public record NestedPropertyChild(
string Name,
string Description);

The nested type is scanned for its own model-bound projection annotations, including FromEvent, ClearWith, and property mappings.

Apply a clear annotation to the nested type when one event should remove the nested object from the parent.

Clear a nested object
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Projections.ModelBound;
[EventType]
public record CommandSetForNestedClear(string Name, string Schema);
[EventType]
public record CommandClearedForNestedClear;
[FromEvent<CommandSetForNestedClear>]
[ClearWith<CommandClearedForNestedClear>]
public record CommandItemNestedClear(
string Name,
string Schema);

Use multiple clear annotations when more than one event should clear the same nested object.

Clear a nested object from multiple events
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Projections.ModelBound;
[EventType]
public record CommandSetForNestedMultipleClear(string Name, string Schema);
[EventType]
public record CommandClearedForNestedMultipleClear;
[EventType]
public record SliceArchivedForNestedMultipleClear;
[FromEvent<CommandSetForNestedMultipleClear>]
[ClearWith<CommandClearedForNestedMultipleClear>]
[ClearWith<SliceArchivedForNestedMultipleClear>]
public record CommandItemNestedMultipleClear(
string Name,
string Schema);

A nested type can be populated or updated by several events. Matching property names are auto-mapped by default.

Update a nested object from multiple events
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Projections.ModelBound;
[EventType]
public record CommandSetForNestedMultipleFrom(string Name, string Schema);
[EventType]
public record CommandRenamedForNestedMultipleFrom(string Name);
[EventType]
public record CommandSchemaUpdatedForNestedMultipleFrom(string Schema);
[EventType]
public record CommandClearedForNestedMultipleFrom;
[FromEvent<CommandSetForNestedMultipleFrom>]
[FromEvent<CommandRenamedForNestedMultipleFrom>]
[FromEvent<CommandSchemaUpdatedForNestedMultipleFrom>]
[ClearWith<CommandClearedForNestedMultipleFrom>]
public record CommandItemNestedMultipleFrom(
string Name,
string Schema);

When event property names differ from nested object property names, add explicit property mappings to the nested type.

Explicit mappings on a nested type
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Projections.ModelBound;
[EventType]
public record CommandSetForNestedExplicit(string CommandName, string JsonSchema);
[EventType]
public record CommandSchemaUpdatedForNestedExplicit(string UpdatedSchema);
[EventType]
public record CommandClearedForNestedExplicit;
[FromEvent<CommandSetForNestedExplicit>]
[FromEvent<CommandSchemaUpdatedForNestedExplicit>]
[ClearWith<CommandClearedForNestedExplicit>]
public record CommandItemNestedExplicit(
[SetFrom<CommandSetForNestedExplicit>(nameof(CommandSetForNestedExplicit.CommandName))]
string Name,
[SetFrom<CommandSetForNestedExplicit>(nameof(CommandSetForNestedExplicit.JsonSchema))]
[SetFrom<CommandSchemaUpdatedForNestedExplicit>(nameof(CommandSchemaUpdatedForNestedExplicit.UpdatedSchema))]
string Schema);

AutoMap is enabled by default for nested types. Disable it on the nested type when every property should be mapped explicitly.

Disable AutoMap on a nested type
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Projections;
using Cratis.Chronicle.Projections.ModelBound;
[EventType]
public record CommandSetForNestedNoAutoMap(string CommandName, string Schema);
[EventType]
public record CommandClearedForNestedNoAutoMap;
[FromEvent<CommandSetForNestedNoAutoMap>]
[ClearWith<CommandClearedForNestedNoAutoMap>]
[NoAutoMap]
public record CommandItemNestedNoAutoMap(
[SetFrom<CommandSetForNestedNoAutoMap>(nameof(CommandSetForNestedNoAutoMap.CommandName))]
string Name,
[SetFrom<CommandSetForNestedNoAutoMap>(nameof(CommandSetForNestedNoAutoMap.Schema))]
string Schema);

A parent can hold more than one nested object. Each nested property points to a type with its own event lifecycle.

Multiple nested objects on one parent
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Projections.ModelBound;
[EventType]
public record CommandSetForNestedMultiple(string Name, string Schema);
[EventType]
public record CommandClearedForNestedMultiple;
[EventType]
public record ValidationConfiguredForNestedMultiple(string Rules, bool IsStrict);
[EventType]
public record ValidationRemovedForNestedMultiple;
public record SliceWithMultipleNestedObjects(
string Name,
[Nested] CommandItemNestedMultiple? Command,
[Nested] ValidationConfigNestedMultiple? Validation);
[FromEvent<CommandSetForNestedMultiple>]
[ClearWith<CommandClearedForNestedMultiple>]
public record CommandItemNestedMultiple(string Name, string Schema);
[FromEvent<ValidationConfiguredForNestedMultiple>]
[ClearWith<ValidationRemovedForNestedMultiple>]
public record ValidationConfigNestedMultiple(string Rules, bool IsStrict);

Nested objects can also appear inside child collection items.

Nested object inside child collection items
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Keys;
using Cratis.Chronicle.Projections.ModelBound;
[EventType]
public record TaskAddedForNestedChildren(Guid TaskId, string Title);
[EventType]
public record TaskAssignedForNestedChildren(Guid TaskId, string Name, string Email);
[EventType]
public record TaskUnassignedForNestedChildren(Guid TaskId);
public record ProjectWithNestedChildren(
[Key] Guid Id,
string Name,
[ChildrenFrom<TaskAddedForNestedChildren>(key: nameof(TaskAddedForNestedChildren.TaskId))]
IEnumerable<ProjectTaskWithNestedAssignee> Tasks);
public record ProjectTaskWithNestedAssignee(
[Key] Guid TaskId,
string Title,
[Nested] TaskAssigneeNestedChild? Assignee);
[FromEvent<TaskAssignedForNestedChildren>]
[ClearWith<TaskUnassignedForNestedChildren>]
public record TaskAssigneeNestedChild(
string Name,
string Email);

This example shows a parent read model with a command definition that can be set, renamed, updated, and cleared.

Complete nested object projection
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Keys;
using Cratis.Chronicle.Projections.ModelBound;
[EventType]
public record SliceCreatedForNestedComplete(string Name);
[EventType]
public record CommandSetForNestedComplete(
Guid CommandId,
string Name,
string Schema,
string Rules,
string StateSchema);
[EventType]
public record CommandRenamedForNestedComplete(Guid CommandId, string Name);
[EventType]
public record CommandDefinitionUpdatedForNestedComplete(
Guid CommandId,
string Schema,
string Rules,
string StateSchema);
[EventType]
public record CommandClearedForNestedComplete;
[FromEvent<SliceCreatedForNestedComplete>]
public record SliceNestedComplete(
[Key] Guid Id,
string Name,
[Nested] CommandItemNestedComplete? Command);
[FromEvent<CommandSetForNestedComplete>]
[FromEvent<CommandRenamedForNestedComplete>]
[FromEvent<CommandDefinitionUpdatedForNestedComplete>]
[ClearWith<CommandClearedForNestedComplete>]
public record CommandItemNestedComplete(
[SetFrom<CommandSetForNestedComplete>(nameof(CommandSetForNestedComplete.CommandId))]
Guid Id,
[SetFrom<CommandSetForNestedComplete>(nameof(CommandSetForNestedComplete.Name))]
[SetFrom<CommandRenamedForNestedComplete>(nameof(CommandRenamedForNestedComplete.Name))]
string Name,
[SetFrom<CommandSetForNestedComplete>(nameof(CommandSetForNestedComplete.Schema))]
[SetFrom<CommandDefinitionUpdatedForNestedComplete>(nameof(CommandDefinitionUpdatedForNestedComplete.Schema))]
string Schema,
[SetFrom<CommandSetForNestedComplete>(nameof(CommandSetForNestedComplete.Rules))]
[SetFrom<CommandDefinitionUpdatedForNestedComplete>(nameof(CommandDefinitionUpdatedForNestedComplete.Rules))]
string Rules,
[SetFrom<CommandSetForNestedComplete>(nameof(CommandSetForNestedComplete.StateSchema))]
[SetFrom<CommandDefinitionUpdatedForNestedComplete>(nameof(CommandDefinitionUpdatedForNestedComplete.StateSchema))]
string StateSchema);
EventEffect on the nested object
SliceCreatedForNestedCompleteCreates the parent read model; the nested command remains null.
CommandSetForNestedCompletePopulates the command object.
CommandRenamedForNestedCompleteUpdates the command name in place.
CommandDefinitionUpdatedForNestedCompleteUpdates command definition fields in place.
CommandClearedForNestedCompleteSets the command property to null.
AnnotationWorks on nested type
FromEventYes
ClearWithYes
SetFromYes
AddFrom / SubtractFromYes
SetFromContextYes
Increment / Decrement / CountYes
JoinYes
Nested recursivelyYes
ChildrenFrom inside nested objectsYes
NoAutoMapYes
  1. Declare the nested property as nullable because it starts empty and is populated by events.
  2. Keep the populate and clear annotations close to the nested type so its lifecycle is visible.
  3. Rely on AutoMap when event and nested property names match.
  4. Use a child collection instead when there can be multiple independent child objects.