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.
Basic Lifecycle
Section titled “Basic Lifecycle”Mark the nullable parent property as nested. The nested type declares the events that populate it and, optionally, the events that clear it.
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:
- The first populate event creates the nested object.
- Later populate events update the existing nested object.
- A clear event sets the parent property back to
null.
Parent Property
Section titled “Parent Property”The nested marker belongs on the single nullable property that holds the child object.
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.
Clearing Nested Objects
Section titled “Clearing Nested Objects”Apply a clear annotation to the nested type when one event should remove the nested object from the parent.
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.
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);Updating From Multiple Events
Section titled “Updating From Multiple Events”A nested type can be populated or updated by several events. Matching property names are auto-mapped by default.
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.
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);Auto-Mapping
Section titled “Auto-Mapping”AutoMap is enabled by default for nested types. Disable it on the nested type when every property should be mapped explicitly.
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);Multiple Nested Objects
Section titled “Multiple Nested Objects”A parent can hold more than one nested object. Each nested property points to a type with its own event lifecycle.
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 Inside Children
Section titled “Nested Objects Inside Children”Nested objects can also appear 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);Complete Example
Section titled “Complete Example”This example shows a parent read model with a command definition that can be set, renamed, updated, and cleared.
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);| Event | Effect on the nested object |
|---|---|
SliceCreatedForNestedComplete | Creates the parent read model; the nested command remains null. |
CommandSetForNestedComplete | Populates the command object. |
CommandRenamedForNestedComplete | Updates the command name in place. |
CommandDefinitionUpdatedForNestedComplete | Updates command definition fields in place. |
CommandClearedForNestedComplete | Sets the command property to null. |
Supported Nested Annotations
Section titled “Supported Nested Annotations”| Annotation | Works on nested type |
|---|---|
FromEvent | Yes |
ClearWith | Yes |
SetFrom | Yes |
AddFrom / SubtractFrom | Yes |
SetFromContext | Yes |
Increment / Decrement / Count | Yes |
Join | Yes |
Nested recursively | Yes |
ChildrenFrom inside nested objects | Yes |
NoAutoMap | Yes |
Best Practices
Section titled “Best Practices”- Declare the nested property as nullable because it starts empty and is populated by events.
- Keep the populate and clear annotations close to the nested type so its lifecycle is visible.
- Rely on AutoMap when event and nested property names match.
- Use a child collection instead when there can be multiple independent child objects.
See Also
Section titled “See Also”- Children Collections — arrays of items managed independently within a parent
- Basic Mapping — getting started with model-bound projections
- Removal — removing root read models on an event