Children Collections
Model-bound projections support child collections through the ChildrenFrom attribute, allowing you to build hierarchical read models with parent-child relationships.
Basic Children
Section titled “Basic Children”The ChildrenFrom attribute defines how child entities are added to a collection:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Keys;using Cratis.Chronicle.Projections.ModelBound;
[EventType]public record MbChildrenLineItemAdded( Guid ItemId, string ProductName, int Quantity, decimal Price);
public record MbChildrenOrder( [Key] Guid OrderId,
[ChildrenFrom<MbChildrenLineItemAdded>(key: nameof(MbChildrenLineItemAdded.ItemId))] IEnumerable<MbChildrenLineItem> Items);
public record MbChildrenLineItem( [Key] Guid Id, // Chronicle automatically discovers this as the key string ProductName, int Quantity, decimal Price);import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.ChildrenFromimport io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.readModels.ReadModel
@EventTypedata class MbChildrenLineItemAdded( val itemId: String, val productName: String, val quantity: Int, val price: Double)
@ReadModel@FromEvent(MbChildrenLineItemAdded::class)data class MbChildrenOrder( @ChildrenFrom(MbChildrenLineItemAdded::class, key = "itemId", identifiedBy = "itemId") val items: List<MbChildrenLineItem> = emptyList())
data class MbChildrenLineItem( val itemId: String = "", // Chronicle automatically discovers this as the key, via identifiedBy val productName: String = "", val quantity: Int = 0, val price: Double = 0.0)import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.ChildrenFrom;import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.readModels.ReadModel;
import java.util.Collections;import java.util.List;
@EventTyperecord MbChildrenLineItemAdded( String itemId, String productName, int quantity, double price) {}
@ReadModel@FromEvent(eventType = MbChildrenLineItemAdded.class)class MbChildrenOrder { @ChildrenFrom(eventType = MbChildrenLineItemAdded.class, key = "itemId", identifiedBy = "itemId") public List<MbChildrenLineItem> items = Collections.emptyList();}
class MbChildrenLineItem { public String itemId = ""; // Chronicle automatically discovers this as the key, via identifiedBy public String productName = ""; public int quantity = 0; public double price = 0.0;}Elixir does not support this workflow yet.import { childrenFrom, eventType, Guid, readModel } from '@cratis/chronicle';
@eventType()class MbChildrenLineItemAdded { itemId: Guid = Guid.empty; productName = ''; quantity = 0; price = 0;}
@readModel()class MbChildrenOrder { id: Guid = Guid.empty;
@childrenFrom(MbChildrenLineItemAdded, 'itemId') items: MbChildrenLineItem[] = [];}
// The `id` property is automatically discovered as the child's keyclass MbChildrenLineItem { id: Guid = Guid.empty; productName = ''; quantity = 0; price = 0;}In this example, the [Key] attribute on the LineItem.Id property is automatically discovered by Chronicle, so you don’t need to specify identifiedBy explicitly in the ChildrenFrom attribute.
Parameters
Section titled “Parameters”- key (optional): Property on the event that identifies the child. Defaults to
EventSourceId - identifiedBy (optional): Property on the child model that identifies it. If not specified, Chronicle will:
- Look for a property with the
[Key]attribute - Look for a property named
Id(case-insensitive) - Fall back to
EventSourceIdif neither is found
- Look for a property with the
- parentKey (optional): Property that identifies the parent. Defaults to
EventSourceId- Use this when the parent identifier is a property in the event content rather than the EventSourceId
- Example:
parentKey: nameof(LineItemAdded.OrderId)when OrderId is in the event
Auto-mapping is enabled by default. To disable it for a child model, apply the [NoAutoMap] attribute on the child type.
Note: With automatic key discovery, you typically don’t need to specify
identifiedByexplicitly. Just mark your child model’s key property with[Key]attribute, or name itId, and Chronicle will automatically discover it.
Auto-Mapping
Section titled “Auto-Mapping”By default, ChildrenFrom automatically maps properties from the event to the child model when property names match. This behavior is similar to the FromEvent attribute:
[EventType]public record MbChildrenAutoMapLineItemAdded( Guid ItemId, string ProductName, int Quantity, decimal Price);
public record MbChildrenAutoMapOrder( [Key] Guid OrderId,
[ChildrenFrom<MbChildrenAutoMapLineItemAdded>(key: nameof(MbChildrenAutoMapLineItemAdded.ItemId))] IEnumerable<MbChildrenAutoMapLineItem> Items);
public record MbChildrenAutoMapLineItem( [Key] Guid Id, string ProductName, // Automatically mapped from MbChildrenAutoMapLineItemAdded.ProductName int Quantity, // Automatically mapped from MbChildrenAutoMapLineItemAdded.Quantity decimal Price); // Automatically mapped from MbChildrenAutoMapLineItemAdded.Priceimport io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.ChildrenFromimport io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.readModels.ReadModel
@EventTypedata class MbChildrenAutoMapLineItemAdded( val itemId: String, val productName: String, val quantity: Int, val price: Double)
@ReadModel@FromEvent(MbChildrenAutoMapLineItemAdded::class)data class MbChildrenAutoMapOrder( @ChildrenFrom(MbChildrenAutoMapLineItemAdded::class, key = "itemId", identifiedBy = "itemId") val items: List<MbChildrenAutoMapLineItem> = emptyList())
data class MbChildrenAutoMapLineItem( val itemId: String = "", val productName: String = "", // Automatically mapped from MbChildrenAutoMapLineItemAdded.productName val quantity: Int = 0, // Automatically mapped from MbChildrenAutoMapLineItemAdded.quantity val price: Double = 0.0 // Automatically mapped from MbChildrenAutoMapLineItemAdded.price)import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.ChildrenFrom;import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.readModels.ReadModel;
import java.util.Collections;import java.util.List;
@EventTyperecord MbChildrenAutoMapLineItemAdded( String itemId, String productName, int quantity, double price) {}
@ReadModel@FromEvent(eventType = MbChildrenAutoMapLineItemAdded.class)class MbChildrenAutoMapOrder { @ChildrenFrom(eventType = MbChildrenAutoMapLineItemAdded.class, key = "itemId", identifiedBy = "itemId") public List<MbChildrenAutoMapLineItem> items = Collections.emptyList();}
class MbChildrenAutoMapLineItem { public String itemId = ""; public String productName = ""; // Automatically mapped from MbChildrenAutoMapLineItemAdded.productName public int quantity = 0; // Automatically mapped from MbChildrenAutoMapLineItemAdded.quantity public double price = 0.0; // Automatically mapped from MbChildrenAutoMapLineItemAdded.price}Elixir does not support this workflow yet.import { eventType } from '@cratis/chronicle';
@eventType()class MbChildrenAutoMapLineItemAdded { itemId: Guid = Guid.empty; productName = ''; quantity = 0; price = 0;}
@readModel()class MbChildrenAutoMapOrder { id: Guid = Guid.empty;
@childrenFrom(MbChildrenAutoMapLineItemAdded, 'itemId') items: MbChildrenAutoMapLineItem[] = [];}
class MbChildrenAutoMapLineItem { id: Guid = Guid.empty; productName = ''; // Automatically mapped from MbChildrenAutoMapLineItemAdded.productName quantity = 0; // Automatically mapped from MbChildrenAutoMapLineItemAdded.quantity price = 0; // Automatically mapped from MbChildrenAutoMapLineItemAdded.price}You can disable auto-mapping if you want to control property mapping explicitly:
using Cratis.Chronicle.Projections;
[EventType]public record MbChildrenNoAutoMapLineItemAdded( Guid ItemId, string ProductName, int Quantity, decimal Price);
public record MbChildrenNoAutoMapOrder( [Key] Guid OrderId,
[ChildrenFrom<MbChildrenNoAutoMapLineItemAdded>(key: nameof(MbChildrenNoAutoMapLineItemAdded.ItemId))] IEnumerable<MbChildrenNoAutoMapLineItem> Items);
[NoAutoMap]public record MbChildrenNoAutoMapLineItem( [Key] Guid Id,
// Now you must use SetFrom for each property [SetFrom<MbChildrenNoAutoMapLineItemAdded>(nameof(MbChildrenNoAutoMapLineItemAdded.ProductName))] string ProductName,
[SetFrom<MbChildrenNoAutoMapLineItemAdded>(nameof(MbChildrenNoAutoMapLineItemAdded.Quantity))] int Quantity,
[SetFrom<MbChildrenNoAutoMapLineItemAdded>(nameof(MbChildrenNoAutoMapLineItemAdded.Price))] decimal Price);import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.ChildrenFromimport io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.projections.NoAutoMapimport io.cratis.chronicle.projections.SetFromimport io.cratis.chronicle.readModels.ReadModel
@EventTypedata class MbChildrenNoAutoMapOrderPlaced(val orderId: String)
@EventTypedata class MbChildrenNoAutoMapLineItemAdded( val itemId: String, val productName: String, val quantity: Int, val price: Double)
@ReadModel@FromEvent(MbChildrenNoAutoMapOrderPlaced::class)data class MbChildrenNoAutoMapOrder( @ChildrenFrom(MbChildrenNoAutoMapLineItemAdded::class, key = "itemId") val items: List<MbChildrenNoAutoMapLineItem> = emptyList())
// Now you must use @SetFrom for each property@NoAutoMapdata class MbChildrenNoAutoMapLineItem( @SetFrom("productName", MbChildrenNoAutoMapLineItemAdded::class) val productName: String = "",
@SetFrom("quantity", MbChildrenNoAutoMapLineItemAdded::class) val quantity: Int = 0,
@SetFrom("price", MbChildrenNoAutoMapLineItemAdded::class) val price: Double = 0.0)import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.ChildrenFrom;import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.projections.NoAutoMap;import io.cratis.chronicle.projections.SetFrom;import io.cratis.chronicle.readModels.ReadModel;
import java.util.List;
@EventTyperecord MbChildrenNoAutoMapOrderPlaced(String orderId) {}
@EventTyperecord MbChildrenNoAutoMapLineItemAdded( String itemId, String productName, int quantity, double price) {}
@ReadModel@FromEvent(eventType = MbChildrenNoAutoMapOrderPlaced.class)record MbChildrenNoAutoMapOrder( @ChildrenFrom(eventType = MbChildrenNoAutoMapLineItemAdded.class, key = "itemId") List<MbChildrenNoAutoMapLineItem> items) {}
// Now you must use @SetFrom for each property@NoAutoMaprecord MbChildrenNoAutoMapLineItem( @SetFrom(propertyPath = "productName", eventType = MbChildrenNoAutoMapLineItemAdded.class) String productName,
@SetFrom(propertyPath = "quantity", eventType = MbChildrenNoAutoMapLineItemAdded.class) int quantity,
@SetFrom(propertyPath = "price", eventType = MbChildrenNoAutoMapLineItemAdded.class) double price) {}Elixir does not support this workflow yet.TypeScript does not support this workflow yet.Recursive Attribute Processing
Section titled “Recursive Attribute Processing”All projection attributes work recursively on child types. The child type’s properties are automatically scanned for projection attributes:
[EventType]public record MbChildrenCountersItemAddedToCart(Guid ItemId, string ProductName, decimal Price, int InitialQuantity);
[EventType]public record MbChildrenCountersQuantityIncreased(Guid ItemId);
[EventType]public record MbChildrenCountersQuantityDecreased(Guid ItemId);
public record MbChildrenCountersShoppingCart( [Key] Guid CartId,
[ChildrenFrom<MbChildrenCountersItemAddedToCart>( key: nameof(MbChildrenCountersItemAddedToCart.ItemId))] IEnumerable<MbChildrenCountersCartItem> Items);
// Child type with its own projection attributespublic record MbChildrenCountersCartItem( [Key] Guid Id,
[SetFrom<MbChildrenCountersItemAddedToCart>(nameof(MbChildrenCountersItemAddedToCart.ProductName))] string ProductName,
[SetFrom<MbChildrenCountersItemAddedToCart>(nameof(MbChildrenCountersItemAddedToCart.Price))] decimal Price,
[SetFrom<MbChildrenCountersItemAddedToCart>(nameof(MbChildrenCountersItemAddedToCart.InitialQuantity))] [Increment<MbChildrenCountersQuantityIncreased>] [Decrement<MbChildrenCountersQuantityDecreased>] int Quantity);import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.ChildrenFromimport io.cratis.chronicle.projections.Decrementimport io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.projections.Incrementimport io.cratis.chronicle.projections.SetFromimport io.cratis.chronicle.readModels.ReadModel
@EventTypedata class MbChildrenCountersItemAddedToCart( val itemId: String, val productName: String, val price: Double, val initialQuantity: Int)
@EventTypedata class MbChildrenCountersQuantityIncreased(val itemId: String)
@EventTypedata class MbChildrenCountersQuantityDecreased(val itemId: String)
@ReadModel@FromEvent(MbChildrenCountersItemAddedToCart::class)data class MbChildrenCountersShoppingCart( @ChildrenFrom(MbChildrenCountersItemAddedToCart::class, key = "itemId", identifiedBy = "itemId") @ChildrenFrom(MbChildrenCountersQuantityIncreased::class, key = "itemId", identifiedBy = "itemId") @ChildrenFrom(MbChildrenCountersQuantityDecreased::class, key = "itemId", identifiedBy = "itemId") val items: List<MbChildrenCountersCartItem> = emptyList())
// Child type with its own projection attributesdata class MbChildrenCountersCartItem( val itemId: String = "",
@SetFrom("productName", MbChildrenCountersItemAddedToCart::class) val productName: String = "",
@SetFrom("price", MbChildrenCountersItemAddedToCart::class) val price: Double = 0.0,
@SetFrom("initialQuantity", MbChildrenCountersItemAddedToCart::class) @Increment(MbChildrenCountersQuantityIncreased::class) @Decrement(MbChildrenCountersQuantityDecreased::class) val quantity: Int = 0)import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.ChildrenFrom;import io.cratis.chronicle.projections.Decrement;import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.projections.Increment;import io.cratis.chronicle.projections.SetFrom;import io.cratis.chronicle.readModels.ReadModel;
import java.util.Collections;import java.util.List;
@EventTyperecord MbChildrenCountersItemAddedToCart( String itemId, String productName, double price, int initialQuantity) {}
@EventTyperecord MbChildrenCountersQuantityIncreased(String itemId) {}
@EventTyperecord MbChildrenCountersQuantityDecreased(String itemId) {}
@ReadModel@FromEvent(eventType = MbChildrenCountersItemAddedToCart.class)class MbChildrenCountersShoppingCart { @ChildrenFrom(eventType = MbChildrenCountersItemAddedToCart.class, key = "itemId", identifiedBy = "itemId") @ChildrenFrom(eventType = MbChildrenCountersQuantityIncreased.class, key = "itemId", identifiedBy = "itemId") @ChildrenFrom(eventType = MbChildrenCountersQuantityDecreased.class, key = "itemId", identifiedBy = "itemId") public List<MbChildrenCountersCartItem> items = Collections.emptyList();}
// Child type with its own projection attributesclass MbChildrenCountersCartItem { public String itemId = "";
@SetFrom(propertyPath = "productName", eventType = MbChildrenCountersItemAddedToCart.class) public String productName = "";
@SetFrom(propertyPath = "price", eventType = MbChildrenCountersItemAddedToCart.class) public double price = 0.0;
@SetFrom(propertyPath = "initialQuantity", eventType = MbChildrenCountersItemAddedToCart.class) @Increment(eventType = MbChildrenCountersQuantityIncreased.class) @Decrement(eventType = MbChildrenCountersQuantityDecreased.class) public int quantity = 0;}Elixir does not support this workflow yet.import { childrenFrom, decrement, eventType, Guid, increment, readModel, setFrom } from '@cratis/chronicle';
@eventType()class MbChildrenCountersItemAddedToCart { itemId: Guid = Guid.empty; productName = ''; price = 0; initialQuantity = 0;}
@eventType()class MbChildrenCountersQuantityIncreased { itemId: Guid = Guid.empty;}
@eventType()class MbChildrenCountersQuantityDecreased { itemId: Guid = Guid.empty;}
@readModel()class MbChildrenCountersShoppingCart { id: Guid = Guid.empty;
@childrenFrom(MbChildrenCountersItemAddedToCart, 'itemId') items: MbChildrenCountersCartItem[] = [];}
// Child type with its own projection decoratorsclass MbChildrenCountersCartItem { id: Guid = Guid.empty;
@setFrom(MbChildrenCountersItemAddedToCart, 'productName') productName = '';
@setFrom(MbChildrenCountersItemAddedToCart, 'price') price = 0;
@setFrom(MbChildrenCountersItemAddedToCart, 'initialQuantity') @increment(MbChildrenCountersQuantityIncreased) @decrement(MbChildrenCountersQuantityDecreased) quantity = 0;}When an ItemAddedToCart event occurs:
- A new
CartItemis added to the collection - Properties are mapped from the event to the child
- The child’s own attributes are processed
When a QuantityIncreased event occurs later:
- The projection finds the matching child by ID
- Increments the
Quantityon that specific child
Class-Level FromEvent on Child Types
Section titled “Class-Level FromEvent on Child Types”Use a class-level FromEvent on the child type when later child events should auto-map into the existing child instance. If those events come from the child’s own event source, specify parentKey so Chronicle can still resolve the parent document:
[EventType]public record MbChildrenChildFromEventConfigurationAdded(Guid DashboardId, Guid ConfigurationId, string Name);
[EventType]public record MbChildrenChildFromEventConfigurationRenamed(Guid DashboardId, Guid Id, string Name);
public record MbChildrenChildFromEventDashboard( [Key] Guid Id, string Name,
[ChildrenFrom<MbChildrenChildFromEventConfigurationAdded>( key: nameof(MbChildrenChildFromEventConfigurationAdded.ConfigurationId), parentKey: nameof(MbChildrenChildFromEventConfigurationAdded.DashboardId))] IEnumerable<MbChildrenChildFromEventConfiguration> Configurations);
[FromEvent<MbChildrenChildFromEventConfigurationRenamed>(parentKey: nameof(MbChildrenChildFromEventConfigurationRenamed.DashboardId))]public record MbChildrenChildFromEventConfiguration( [Key] Guid Id, string Name);Kotlin does not support this workflow yet.Java does not support this workflow yet.Elixir does not support this workflow yet.import { childrenFrom, eventType, fromEvent, Guid, readModel } from '@cratis/chronicle';
@eventType()class MbChildrenChildFromEventConfigurationAdded { dashboardId: Guid = Guid.empty; configurationId: Guid = Guid.empty; name = '';}
@eventType()class MbChildrenChildFromEventConfigurationRenamed { dashboardId: Guid = Guid.empty; id: Guid = Guid.empty; name = '';}
@readModel()class MbChildrenChildFromEventDashboard { id: Guid = Guid.empty; name = '';
@childrenFrom(MbChildrenChildFromEventConfigurationAdded, 'configurationId', undefined, 'dashboardId') configurations: MbChildrenChildFromEventConfiguration[] = [];}
@fromEvent(MbChildrenChildFromEventConfigurationRenamed, { parentKey: 'dashboardId' })class MbChildrenChildFromEventConfiguration { id: Guid = Guid.empty; name = '';}ChildrenFrom handles how the child is first added to the collection. The child type’s own FromEvent handles later updates, and parentKey is the equivalent of .UsingParentKey(...) in a fluent child projection. You do not need identifiedBy here because Configuration.Id is marked with [Key].
Removing Children
Section titled “Removing Children”Use RemovedWith to remove children from collections. You can apply it either on the collection property or on the child type class:
Property-Level Removal
Section titled “Property-Level Removal”[EventType]public record MbChildrenRemovalPropertyLineItemAdded(Guid ItemId, string Description);
[EventType]public record MbChildrenRemovalPropertyLineItemRemoved(Guid ItemId);
public record MbChildrenRemovalPropertyOrder( [Key] Guid Id,
[ChildrenFrom<MbChildrenRemovalPropertyLineItemAdded>(key: nameof(MbChildrenRemovalPropertyLineItemAdded.ItemId))] [RemovedWith<MbChildrenRemovalPropertyLineItemRemoved>(key: nameof(MbChildrenRemovalPropertyLineItemRemoved.ItemId))] IEnumerable<MbChildrenRemovalPropertyOrderLine> Lines);
public record MbChildrenRemovalPropertyOrderLine( [Key] Guid Id, string Description);import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.ChildrenFromimport io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.projections.RemovedWithimport io.cratis.chronicle.projections.SetFromimport io.cratis.chronicle.readModels.ReadModel
@EventTypedata class MbChildrenRemovalPropertyLineItemAdded(val itemId: String, val description: String)
@EventTypedata class MbChildrenRemovalPropertyLineItemRemoved(val itemId: String)
@ReadModel@FromEvent(MbChildrenRemovalPropertyLineItemAdded::class)data class MbChildrenRemovalPropertyOrder( @ChildrenFrom(MbChildrenRemovalPropertyLineItemAdded::class, key = "itemId", identifiedBy = "itemId") @RemovedWith(MbChildrenRemovalPropertyLineItemRemoved::class, key = "itemId") val lines: List<MbChildrenRemovalPropertyOrderLine> = emptyList())
data class MbChildrenRemovalPropertyOrderLine( val itemId: String = "",
@SetFrom("description", MbChildrenRemovalPropertyLineItemAdded::class) val description: String = "")import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.ChildrenFrom;import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.projections.RemovedWith;import io.cratis.chronicle.projections.SetFrom;import io.cratis.chronicle.readModels.ReadModel;
import java.util.Collections;import java.util.List;
@EventTyperecord MbChildrenRemovalPropertyLineItemAdded(String itemId, String description) {}
@EventTyperecord MbChildrenRemovalPropertyLineItemRemoved(String itemId) {}
@ReadModel@FromEvent(eventType = MbChildrenRemovalPropertyLineItemAdded.class)class MbChildrenRemovalPropertyOrder { @ChildrenFrom(eventType = MbChildrenRemovalPropertyLineItemAdded.class, key = "itemId", identifiedBy = "itemId") @RemovedWith(eventType = MbChildrenRemovalPropertyLineItemRemoved.class, key = "itemId") public List<MbChildrenRemovalPropertyOrderLine> lines = Collections.emptyList();}
class MbChildrenRemovalPropertyOrderLine { public String itemId = "";
@SetFrom(propertyPath = "description", eventType = MbChildrenRemovalPropertyLineItemAdded.class) public String description = "";}Elixir does not support this workflow yet.import { childrenFrom, eventType, Guid, readModel, removedWith } from '@cratis/chronicle';
@eventType()class MbChildrenRemovalPropertyLineItemAdded { itemId: Guid = Guid.empty; description = '';}
@eventType()class MbChildrenRemovalPropertyLineItemRemoved { itemId: Guid = Guid.empty;}
@readModel()class MbChildrenRemovalPropertyOrder { id: Guid = Guid.empty;
@childrenFrom(MbChildrenRemovalPropertyLineItemAdded, 'itemId') @removedWith(MbChildrenRemovalPropertyLineItemRemoved, 'itemId') lines: MbChildrenRemovalPropertyOrderLine[] = [];}
class MbChildrenRemovalPropertyOrderLine { id: Guid = Guid.empty; description = '';}Class-Level Removal
Section titled “Class-Level Removal”Apply RemovedWith directly on the child type for better separation of concerns:
[EventType]public record MbChildrenRemovalClassLineItemAdded(Guid ItemId, string Description);
[EventType]public record MbChildrenRemovalClassLineItemRemoved(Guid OrderId, Guid ItemId);
public record MbChildrenRemovalClassOrder( [Key] Guid Id,
[ChildrenFrom<MbChildrenRemovalClassLineItemAdded>(key: nameof(MbChildrenRemovalClassLineItemAdded.ItemId))] IEnumerable<MbChildrenRemovalClassOrderLine> Lines);
[RemovedWith<MbChildrenRemovalClassLineItemRemoved>( key: nameof(MbChildrenRemovalClassLineItemRemoved.ItemId), parentKey: nameof(MbChildrenRemovalClassLineItemRemoved.OrderId))]public record MbChildrenRemovalClassOrderLine( [Key] Guid Id, string Description);Kotlin does not support this workflow yet.Java does not support this workflow yet.Elixir does not support this workflow yet.import { childrenFrom, eventType, Guid, readModel, removedWith } from '@cratis/chronicle';
@eventType()class MbChildrenRemovalClassLineItemAdded { itemId: Guid = Guid.empty; description = '';}
@eventType()class MbChildrenRemovalClassLineItemRemoved { orderId: Guid = Guid.empty; itemId: Guid = Guid.empty;}
@readModel()class MbChildrenRemovalClassOrder { id: Guid = Guid.empty;
@childrenFrom(MbChildrenRemovalClassLineItemAdded, 'itemId') lines: MbChildrenRemovalClassOrderLine[] = [];}
@removedWith(MbChildrenRemovalClassLineItemRemoved, 'itemId', 'orderId')class MbChildrenRemovalClassOrderLine { id: Guid = Guid.empty; description = '';}RemovedWithJoin
Section titled “RemovedWithJoin”For removal based on events from different streams (joins), use RemovedWithJoin on the collection property (it also works at the child type’s class level, the same way RemovedWith does above):
[EventType]public record MbChildrenRemovedFeatureActivated(Guid FeatureId, string Name);
[EventType]public record MbChildrenRemovedFeatureDeactivated(Guid FeatureId);
public record MbChildrenRemovedSubscription( [Key] Guid SubscriptionId,
[ChildrenFrom<MbChildrenRemovedFeatureActivated>(key: nameof(MbChildrenRemovedFeatureActivated.FeatureId))] [RemovedWithJoin<MbChildrenRemovedFeatureDeactivated>(key: nameof(MbChildrenRemovedFeatureDeactivated.FeatureId))] IEnumerable<MbChildrenRemovedFeature> Features);
public record MbChildrenRemovedFeature( [Key] Guid FeatureId, string Name);import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.ChildrenFromimport io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.projections.RemovedWithJoinimport io.cratis.chronicle.projections.SetFromimport io.cratis.chronicle.readModels.ReadModel
@EventTypedata class MbChildrenRemovedFeatureActivated(val featureId: String, val name: String)
@EventTypedata class MbChildrenRemovedFeatureDeactivated(val featureId: String)
@ReadModel@FromEvent(MbChildrenRemovedFeatureActivated::class)data class MbChildrenRemovedSubscription( @ChildrenFrom(MbChildrenRemovedFeatureActivated::class, key = "featureId", identifiedBy = "featureId") @RemovedWithJoin(MbChildrenRemovedFeatureDeactivated::class, key = "featureId") val features: List<MbChildrenRemovedFeature> = emptyList())
data class MbChildrenRemovedFeature( val featureId: String = "",
@SetFrom("name", MbChildrenRemovedFeatureActivated::class) val name: String = "")import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.ChildrenFrom;import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.projections.RemovedWithJoin;import io.cratis.chronicle.projections.SetFrom;import io.cratis.chronicle.readModels.ReadModel;
import java.util.Collections;import java.util.List;
@EventTyperecord MbChildrenRemovedFeatureActivated(String featureId, String name) {}
@EventTyperecord MbChildrenRemovedFeatureDeactivated(String featureId) {}
@ReadModel@FromEvent(eventType = MbChildrenRemovedFeatureActivated.class)class MbChildrenRemovedSubscription { @ChildrenFrom(eventType = MbChildrenRemovedFeatureActivated.class, key = "featureId", identifiedBy = "featureId") @RemovedWithJoin(eventType = MbChildrenRemovedFeatureDeactivated.class, key = "featureId") public List<MbChildrenRemovedFeature> features = Collections.emptyList();}
class MbChildrenRemovedFeature { public String featureId = "";
@SetFrom(propertyPath = "name", eventType = MbChildrenRemovedFeatureActivated.class) public String name = "";}Elixir does not support this workflow yet.import { childrenFrom, eventType, Guid, readModel, removedWithJoin } from '@cratis/chronicle';
@eventType()class MbChildrenRemovedFeatureActivated { featureId: Guid = Guid.empty; name = '';}
@eventType()class MbChildrenRemovedFeatureDeactivated { featureId: Guid = Guid.empty;}
@readModel()class MbChildrenRemovedSubscription { id: Guid = Guid.empty;
@childrenFrom(MbChildrenRemovedFeatureActivated, 'featureId', 'featureId') @removedWithJoin(MbChildrenRemovedFeatureDeactivated, 'featureId') features: MbChildrenRemovedFeature[] = [];}
class MbChildrenRemovedFeature { featureId: Guid = Guid.empty; name = '';}Note: For comprehensive documentation on removal options including removing root read models, see Removal.
Complete Example
Section titled “Complete Example”Here’s a comprehensive example showing children with full attribute support:
// Events[EventType]public record MbChildrenFullOrderCreated(string CustomerName);
[EventType]public record MbChildrenFullLineItemAdded( Guid ItemId, string ProductName, int InitialQuantity, decimal UnitPrice);
[EventType]public record MbChildrenFullQuantityAdjusted(Guid ItemId, int NewQuantity);
[EventType]public record MbChildrenFullLineItemRemoved(Guid ItemId);
// Read Modelspublic record MbChildrenFullOrder( [Key] Guid Id,
[SetFrom<MbChildrenFullOrderCreated>(nameof(MbChildrenFullOrderCreated.CustomerName))] string Customer,
[ChildrenFrom<MbChildrenFullLineItemAdded>(key: nameof(MbChildrenFullLineItemAdded.ItemId))] [RemovedWith<MbChildrenFullLineItemRemoved>(key: nameof(MbChildrenFullLineItemRemoved.ItemId))] IEnumerable<MbChildrenFullOrderLine> Lines);
public record MbChildrenFullOrderLine( [Key] Guid Id,
[SetFrom<MbChildrenFullLineItemAdded>(nameof(MbChildrenFullLineItemAdded.ProductName))] string Product,
[SetFrom<MbChildrenFullLineItemAdded>(nameof(MbChildrenFullLineItemAdded.InitialQuantity))] [SetFrom<MbChildrenFullQuantityAdjusted>(nameof(MbChildrenFullQuantityAdjusted.NewQuantity))] int Quantity,
[SetFrom<MbChildrenFullLineItemAdded>(nameof(MbChildrenFullLineItemAdded.UnitPrice))] decimal UnitPrice);import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.ChildrenFromimport io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.projections.RemovedWithimport io.cratis.chronicle.projections.SetFromimport io.cratis.chronicle.readModels.ReadModel
// Events@EventTypedata class MbChildrenFullOrderCreated(val customerName: String)
@EventTypedata class MbChildrenFullLineItemAdded( val itemId: String, val productName: String, val initialQuantity: Int, val unitPrice: Double)
@EventTypedata class MbChildrenFullQuantityAdjusted(val itemId: String, val newQuantity: Int)
@EventTypedata class MbChildrenFullLineItemRemoved(val itemId: String)
// Read Models@ReadModel@FromEvent(MbChildrenFullOrderCreated::class)data class MbChildrenFullOrder( @SetFrom("customerName", MbChildrenFullOrderCreated::class) val customer: String = "",
@ChildrenFrom(MbChildrenFullLineItemAdded::class, key = "itemId", identifiedBy = "itemId") @ChildrenFrom(MbChildrenFullQuantityAdjusted::class, key = "itemId", identifiedBy = "itemId") @RemovedWith(MbChildrenFullLineItemRemoved::class, key = "itemId") val lines: List<MbChildrenFullOrderLine> = emptyList())
data class MbChildrenFullOrderLine( val itemId: String = "",
@SetFrom("productName", MbChildrenFullLineItemAdded::class) val product: String = "",
@SetFrom("initialQuantity", MbChildrenFullLineItemAdded::class) @SetFrom("newQuantity", MbChildrenFullQuantityAdjusted::class) val quantity: Int = 0,
@SetFrom("unitPrice", MbChildrenFullLineItemAdded::class) val unitPrice: Double = 0.0)import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.ChildrenFrom;import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.projections.RemovedWith;import io.cratis.chronicle.projections.SetFrom;import io.cratis.chronicle.readModels.ReadModel;
import java.util.Collections;import java.util.List;
// Events@EventTyperecord MbChildrenFullOrderCreated(String customerName) {}
@EventTyperecord MbChildrenFullLineItemAdded( String itemId, String productName, int initialQuantity, double unitPrice) {}
@EventTyperecord MbChildrenFullQuantityAdjusted(String itemId, int newQuantity) {}
@EventTyperecord MbChildrenFullLineItemRemoved(String itemId) {}
// Read Models@ReadModel@FromEvent(eventType = MbChildrenFullOrderCreated.class)class MbChildrenFullOrder { @SetFrom(propertyPath = "customerName", eventType = MbChildrenFullOrderCreated.class) public String customer = "";
@ChildrenFrom(eventType = MbChildrenFullLineItemAdded.class, key = "itemId", identifiedBy = "itemId") @ChildrenFrom(eventType = MbChildrenFullQuantityAdjusted.class, key = "itemId", identifiedBy = "itemId") @RemovedWith(eventType = MbChildrenFullLineItemRemoved.class, key = "itemId") public List<MbChildrenFullOrderLine> lines = Collections.emptyList();}
class MbChildrenFullOrderLine { public String itemId = "";
@SetFrom(propertyPath = "productName", eventType = MbChildrenFullLineItemAdded.class) public String product = "";
@SetFrom(propertyPath = "initialQuantity", eventType = MbChildrenFullLineItemAdded.class) @SetFrom(propertyPath = "newQuantity", eventType = MbChildrenFullQuantityAdjusted.class) public int quantity = 0;
@SetFrom(propertyPath = "unitPrice", eventType = MbChildrenFullLineItemAdded.class) public double unitPrice = 0.0;}Elixir does not support this workflow yet.import { childrenFrom, eventType, Guid, readModel, removedWith, setFrom } from '@cratis/chronicle';
// Events@eventType()class MbChildrenFullOrderCreated { customerName = '';}
@eventType()class MbChildrenFullLineItemAdded { itemId: Guid = Guid.empty; productName = ''; initialQuantity = 0; unitPrice = 0;}
@eventType()class MbChildrenFullQuantityAdjusted { itemId: Guid = Guid.empty; newQuantity = 0;}
@eventType()class MbChildrenFullLineItemRemoved { itemId: Guid = Guid.empty;}
// Read Models@readModel()class MbChildrenFullOrder { id: Guid = Guid.empty;
@setFrom(MbChildrenFullOrderCreated, 'customerName') customer = '';
@childrenFrom(MbChildrenFullLineItemAdded, 'itemId') @removedWith(MbChildrenFullLineItemRemoved, 'itemId') lines: MbChildrenFullOrderLine[] = [];}
class MbChildrenFullOrderLine { id: Guid = Guid.empty;
@setFrom(MbChildrenFullLineItemAdded, 'productName') product = '';
@setFrom(MbChildrenFullLineItemAdded, 'initialQuantity') @setFrom(MbChildrenFullQuantityAdjusted, 'newQuantity') quantity = 0;
@setFrom(MbChildrenFullLineItemAdded, 'unitPrice') unitPrice = 0;}Event Processing Flow
Section titled “Event Processing Flow”- OrderCreated - Creates the parent Order with customer name
- LineItemAdded - Adds a new OrderLine to the collection with initial values
- QuantityAdjusted - Updates the Quantity on the matching OrderLine
- LineItemRemoved - Removes the OrderLine from the collection
Nested Children
Section titled “Nested Children”Children can have their own children, creating deeply nested structures. All projection attributes (joins, removal, counters, context mapping, etc.) work recursively at every level:
// Events[EventType]public record MbChildrenNestedOrganizationCreated(string Name);
[EventType]public record MbChildrenNestedDepartmentAdded(Guid Id, string Name);
[EventType]public record MbChildrenNestedDepartmentRenamed(Guid Id, string NewName);
[EventType]public record MbChildrenNestedTeamAdded(Guid Id, Guid DepartmentId, string Name);
[EventType]public record MbChildrenNestedTeamRenamed(Guid Id, string NewName);
// Read Models - all attributes work at every nesting levelpublic record MbChildrenNestedOrganization( [Key] Guid Id,
[SetFrom<MbChildrenNestedOrganizationCreated>] string Name,
[ChildrenFrom<MbChildrenNestedDepartmentAdded>( key: nameof(MbChildrenNestedDepartmentAdded.Id), identifiedBy: nameof(MbChildrenNestedDepartment.Id))] IEnumerable<MbChildrenNestedDepartment> Departments);
public record MbChildrenNestedDepartment( [Key] Guid Id,
[SetFrom<MbChildrenNestedDepartmentAdded>] [Join<MbChildrenNestedDepartmentRenamed>(eventPropertyName: nameof(MbChildrenNestedDepartmentRenamed.NewName))] // Joins work on children string Name,
[ChildrenFrom<MbChildrenNestedTeamAdded>( key: nameof(MbChildrenNestedTeamAdded.Id), identifiedBy: nameof(MbChildrenNestedTeam.Id), parentKey: nameof(MbChildrenNestedTeamAdded.DepartmentId))] // Nested children IEnumerable<MbChildrenNestedTeam> Teams);
public record MbChildrenNestedTeam( [Key] Guid Id,
[SetFrom<MbChildrenNestedTeamAdded>] [Join<MbChildrenNestedTeamRenamed>(eventPropertyName: nameof(MbChildrenNestedTeamRenamed.NewName))] // Joins work on nested children too string Name);Kotlin does not support this workflow yet.Java does not support this workflow yet.Elixir does not support this workflow yet.import { childrenFrom, eventType, Guid, join, readModel, setFrom } from '@cratis/chronicle';
// Events@eventType()class MbChildrenNestedOrganizationCreated { name = '';}
@eventType()class MbChildrenNestedDepartmentAdded { id: Guid = Guid.empty; name = '';}
@eventType()class MbChildrenNestedDepartmentRenamed { id: Guid = Guid.empty; newName = '';}
@eventType()class MbChildrenNestedTeamAdded { id: Guid = Guid.empty; departmentId: Guid = Guid.empty; name = '';}
@eventType()class MbChildrenNestedTeamRenamed { id: Guid = Guid.empty; newName = '';}
// Read Models - all decorators work at every nesting level@readModel()class MbChildrenNestedOrganization { id: Guid = Guid.empty;
@setFrom(MbChildrenNestedOrganizationCreated, 'name') name = '';
@childrenFrom(MbChildrenNestedDepartmentAdded, 'id', 'id') departments: MbChildrenNestedDepartment[] = [];}
class MbChildrenNestedDepartment { id: Guid = Guid.empty;
@setFrom(MbChildrenNestedDepartmentAdded, 'name') @join(MbChildrenNestedDepartmentRenamed, undefined, 'newName') // Joins work on children name = '';
@childrenFrom(MbChildrenNestedTeamAdded, 'id', 'id', 'departmentId') // Nested children teams: MbChildrenNestedTeam[] = [];}
class MbChildrenNestedTeam { id: Guid = Guid.empty;
@setFrom(MbChildrenNestedTeamAdded, 'name') @join(MbChildrenNestedTeamRenamed, undefined, 'newName') // Joins work on nested children too name = '';}What Works Recursively
Section titled “What Works Recursively”All projection attributes are fully supported on child types at any nesting level:
| Attribute | Works on Children |
|---|---|
SetFrom | ✓ |
AddFrom / SubtractFrom | ✓ |
SetFromContext | ✓ |
Join | ✓ |
Increment / Decrement / Count | ✓ |
ChildrenFrom (nested children) | ✓ |
RemovedWith / RemovedWithJoin | ✓ |
FromEvent (class-level) | ✓ |
This means you can build arbitrarily deep hierarchies with full projection capabilities at every level.
Collections with no children
Section titled “Collections with no children”Until a child event arrives the collection has no children, and Chronicle stores that as an absent field rather than
as [] — so that a parallel replay cannot erase a child a sibling event already added. How you declare the property
decides what you read back: non-nullable materializes as an empty collection, nullable stays null so you can tell
“no children yet” apart from “an empty list”. See Empty child collections.
Best Practices
Section titled “Best Practices”- Always use Key attribute on child types to identify them uniquely
- Leverage recursive attributes to build complex child projections without duplication
- Use RemovedWith to maintain collection integrity when items are removed
- Consider performance with large collections - deeply nested structures can impact query performance