Projection with children
Projections can manage hierarchical data by defining child collections. This allows you to build read models that contain arrays or lists of related data.
Defining a projection with children
Section titled “Defining a projection with children”Use the child-collection builder to select the collection property, declare how each child is identified, and map the events that add, update, or remove items.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Projections;
[EventType]public record GroupCreatedForChildren(string Name, string Description);
[EventType]public record UserAddedToGroupForChildren(string UserId, string Role);
[EventType]public record UserRoleChangedForChildren(string UserId, string Role);
[EventType]public record UserRemovedFromGroupForChildren(string UserId);
public record GroupForChildren( string Name, string Description, IEnumerable<GroupMemberForChildren> Members);
public record GroupMemberForChildren( string UserId, string Role);
public class GroupProjectionForChildren : IProjectionFor<GroupForChildren>{ public void Define(IProjectionBuilderFor<GroupForChildren> builder) => builder .From<GroupCreatedForChildren>() .Children(m => m.Members, children => children .IdentifiedBy(m => m.UserId) .From<UserAddedToGroupForChildren>(b => b .UsingKey(e => e.UserId)) .From<UserRoleChangedForChildren>(b => b .UsingKey(e => e.UserId)) .RemovedWith<UserRemovedFromGroupForChildren>(b => b .UsingKey(e => e.UserId)));}import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.IProjectionBuilderForimport io.cratis.chronicle.projections.IProjectionFor
@EventTypedata class GroupCreatedForChildren(val name: String, val description: String)
@EventTypedata class UserAddedToGroupForChildren(val userId: String, val role: String)
@EventTypedata class UserRoleChangedForChildren(val userId: String, val role: String)
data class GroupForChildren( val name: String = "", val description: String = "", val members: List<GroupMemberForChildren> = emptyList())
data class GroupMemberForChildren( val userId: String = "", val role: String = "")
class GroupProjectionForChildren : IProjectionFor<GroupForChildren> { override fun define(builder: IProjectionBuilderFor<GroupForChildren>) { builder .from(GroupCreatedForChildren::class) .children(GroupForChildren::members, GroupMemberForChildren::class) { children -> children .identifiedBy("userId") .from(UserAddedToGroupForChildren::class) { it.usingKey("userId") } .from(UserRoleChangedForChildren::class) { it.usingKey("userId") } } }}import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.IProjectionBuilderFor;import io.cratis.chronicle.projections.IProjectionFor;
import java.util.List;
@EventTyperecord GroupCreatedForChildren(String name, String description) {}
@EventTyperecord UserAddedToGroupForChildren(String userId, String role) {}
@EventTyperecord UserRoleChangedForChildren(String userId, String role) {}
record GroupForChildren(String name, String description, List<GroupMemberForChildren> members) {}
record GroupMemberForChildren(String userId, String role) {}
class GroupProjectionForChildren implements IProjectionFor<GroupForChildren> { @Override public void define(IProjectionBuilderFor<GroupForChildren> builder) { builder .from(GroupCreatedForChildren.class) .children("members", GroupMemberForChildren.class, children -> { children .identifiedBy("userId") .from(UserAddedToGroupForChildren.class, fb -> { fb.usingKey("userId"); }) .from(UserRoleChangedForChildren.class, fb -> { fb.usingKey("userId"); }); }); }}Elixir does not support this workflow yet.import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle';
@eventType()class GroupCreatedForChildren { constructor(readonly name: string, readonly description: string) {}}
@eventType()class UserAddedToGroupForChildren { constructor(readonly userId: string, readonly role: string) {}}
@eventType()class UserRoleChangedForChildren { constructor(readonly userId: string, readonly role: string) {}}
@eventType()class UserRemovedFromGroupForChildren { constructor(readonly userId: string) {}}
class GroupMemberForChildren { userId = ''; role = '';}
class GroupForChildren { name = ''; description = ''; members: GroupMemberForChildren[] = [];}
@projection()class GroupProjectionForChildren implements IProjectionFor<GroupForChildren> { define(builder: IProjectionBuilderFor<GroupForChildren>): void { builder .from(GroupCreatedForChildren) .children<GroupMemberForChildren>(m => m.members, children => children .identifiedBy(m => m.userId) .from(UserAddedToGroupForChildren, b => b .usingKey(e => e.userId)) .from(UserRoleChangedForChildren, b => b .usingKey(e => e.userId)) .removedWith(UserRemovedFromGroupForChildren, b => b .usingKey(e => e.userId))); }}Read model with children
Section titled “Read model with children”The read model includes a collection property for the children:
public record GroupWithMembers( string Name, string Description, IEnumerable<GroupMember> Members);
public record GroupMember( string UserId, string Role);data class GroupWithMembers( val name: String = "", val description: String = "", val members: List<GroupMember> = emptyList())
data class GroupMember( val userId: String = "", val role: String = "")import java.util.List;
record GroupWithMembers(String name, String description, List<GroupMember> members) {}
record GroupMember(String userId, String role) {}Elixir does not support this workflow yet.class GroupMember { userId = ''; role = '';}
class GroupWithMembers { name = ''; description = ''; members: GroupMember[] = [];}Event definitions
Section titled “Event definitions”Events that affect children use keys to identify which child to update:
using Cratis.Chronicle.Events;
[EventType]public record GroupCreatedForChildEvents(string Name, string Description);
[EventType]public record UserAddedToGroupForChildEvents(string UserId, string Role);
[EventType]public record UserRoleChangedForChildEvents(string UserId, string Role);
[EventType]public record UserRemovedFromGroupForChildEvents(string UserId);import io.cratis.chronicle.events.EventType
@EventTypedata class GroupCreatedForChildEvents(val name: String, val description: String)
@EventTypedata class UserAddedToGroupForChildEvents(val userId: String, val role: String)
@EventTypedata class UserRoleChangedForChildEvents(val userId: String, val role: String)
@EventTypedata class UserRemovedFromGroupForChildEvents(val userId: String)import io.cratis.chronicle.events.EventType;
@EventTyperecord GroupCreatedForChildEvents(String name, String description) {}
@EventTyperecord UserAddedToGroupForChildEvents(String userId, String role) {}
@EventTyperecord UserRoleChangedForChildEvents(String userId, String role) {}
@EventTyperecord UserRemovedFromGroupForChildEvents(String userId) {}Elixir does not support this workflow yet.import { eventType } from '@cratis/chronicle';
@eventType()class GroupCreatedForChildEvents { constructor(readonly name: string, readonly description: string) {}}
@eventType()class UserAddedToGroupForChildEvents { constructor(readonly userId: string, readonly role: string) {}}
@eventType()class UserRoleChangedForChildEvents { constructor(readonly userId: string, readonly role: string) {}}
@eventType()class UserRemovedFromGroupForChildEvents { constructor(readonly userId: string) {}}How children work
Section titled “How children work”- Root events (
GroupCreated) update properties on the main read model - Child events (
UserAddedToGroup,UserRoleChanged) are routed to child items IdentifiedBy()specifies how to identify child items (byUserIdin this example)UsingKey()tells the projection which property contains the child identifier- Child items are created, updated, or remain unchanged based on the events
Parent key resolution
Section titled “Parent key resolution”By default, when a child event is processed, the framework uses the EventSourceId to identify the parent. This works well when the event is appended with the parent’s identifier as the EventSourceId.
Default behavior (EventSourceId as parent key)
Section titled “Default behavior (EventSourceId as parent key)”In most scenarios, you don’t need to specify the parent key explicitly:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Projections;
[EventType]public record GroupCreatedWithDefaultParentKey(string Name);
[EventType]public record UserAddedWithDefaultParentKey(string UserId, string Role);
public record GroupWithDefaultParentKey( string Name, IEnumerable<GroupMemberWithDefaultParentKey> Members);
public record GroupMemberWithDefaultParentKey( string UserId, string Role);
public class GroupWithDefaultParentKeyProjection : IProjectionFor<GroupWithDefaultParentKey>{ public void Define(IProjectionBuilderFor<GroupWithDefaultParentKey> builder) => builder .From<GroupCreatedWithDefaultParentKey>() .Children(m => m.Members, children => children .IdentifiedBy(m => m.UserId) .From<UserAddedWithDefaultParentKey>(b => b .UsingKey(e => e.UserId)));}import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.IProjectionBuilderForimport io.cratis.chronicle.projections.IProjectionFor
@EventTypedata class GroupCreatedWithDefaultParentKey(val name: String)
@EventTypedata class UserAddedWithDefaultParentKey(val userId: String, val role: String)
data class GroupWithDefaultParentKey( val name: String = "", val members: List<GroupMemberWithDefaultParentKey> = emptyList())
data class GroupMemberWithDefaultParentKey( val userId: String = "", val role: String = "")
class GroupWithDefaultParentKeyProjection : IProjectionFor<GroupWithDefaultParentKey> { override fun define(builder: IProjectionBuilderFor<GroupWithDefaultParentKey>) { builder .from(GroupCreatedWithDefaultParentKey::class) .children(GroupWithDefaultParentKey::members, GroupMemberWithDefaultParentKey::class) { children -> children .identifiedBy("userId") .from(UserAddedWithDefaultParentKey::class) { it.usingKey("userId") } } }}import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.IProjectionBuilderFor;import io.cratis.chronicle.projections.IProjectionFor;
import java.util.List;
@EventTyperecord GroupCreatedWithDefaultParentKey(String name) {}
record GroupWithDefaultParentKey(String name, List<GroupMemberWithDefaultParentKey> members) {}
record GroupMemberWithDefaultParentKey(String userId, String role) {}
class GroupWithDefaultParentKeyProjection implements IProjectionFor<GroupWithDefaultParentKey> { @Override public void define(IProjectionBuilderFor<GroupWithDefaultParentKey> builder) { builder .from(GroupCreatedWithDefaultParentKey.class) .children("members", GroupMemberWithDefaultParentKey.class, children -> { children .identifiedBy("userId") .from(UserAddedWithDefaultParentKey.class, fb -> { fb.usingKey("userId"); }); }); }}Elixir does not support this workflow yet.import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle';
@eventType()class GroupCreatedWithDefaultParentKey { constructor(readonly name: string) {}}
@eventType()class UserAddedWithDefaultParentKey { constructor(readonly userId: string, readonly role: string) {}}
class GroupMemberWithDefaultParentKey { userId = ''; role = '';}
class GroupWithDefaultParentKey { name = ''; members: GroupMemberWithDefaultParentKey[] = [];}
@projection()class GroupWithDefaultParentKeyProjection implements IProjectionFor<GroupWithDefaultParentKey> { define(builder: IProjectionBuilderFor<GroupWithDefaultParentKey>): void { builder .from(GroupCreatedWithDefaultParentKey) .children<GroupMemberWithDefaultParentKey>(m => m.members, children => children .identifiedBy(m => m.userId) .from(UserAddedWithDefaultParentKey, b => b .usingKey(e => e.userId))); }}When you append the event:
using Cratis.Chronicle;using Cratis.Chronicle.Events;using Cratis.Chronicle.EventSequences;
public class GroupMembershipWithDefaultParentKey(IEventStore eventStore){ public Task AddUserToGroup(EventSourceId groupId, string userId, string role) => eventStore.EventLog.Append(groupId, new UserAddedWithDefaultParentKey(userId, role));}import io.cratis.chronicle.IEventStore
class GroupMembershipWithDefaultParentKey(private val eventStore: IEventStore) { suspend fun addUserToGroup(groupId: String, userId: String, role: String) = eventStore.eventLog.append(groupId, UserAddedWithDefaultParentKey(userId, role))}import io.cratis.chronicle.EventStore;import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.java.EventLogJavaBridge;
@EventTyperecord UserAddedWithDefaultParentKey(String userId, String role) {}
class GroupMembershipWithDefaultParentKey { private final EventStore eventStore;
GroupMembershipWithDefaultParentKey(EventStore eventStore) { this.eventStore = eventStore; }
void addUserToGroup(String groupId, String userId, String role) { EventLogJavaBridge.append(eventStore.getEventLog(), groupId, new UserAddedWithDefaultParentKey(userId, role), null); }}Elixir does not support this workflow yet.import { IEventStore } from '@cratis/chronicle';
class GroupMembershipWithDefaultParentKey { constructor(private readonly eventStore: IEventStore) {}
addUserToGroup(groupId: string, userId: string, role: string): Promise<unknown> { return this.eventStore.eventLog.append(groupId, new UserAddedWithDefaultParentKey(userId, role)); }}The groupId (EventSourceId) is automatically used to find the parent Group.
Extracting parent key from event content
Section titled “Extracting parent key from event content”If your event contains the parent key as a property (instead of using EventSourceId), use UsingParentKey():
using Cratis.Chronicle.Events;using Cratis.Chronicle.Projections;
[EventType]public record GroupCreatedWithEventParentKey(string Name);
[EventType]public record UserAddedWithEventParentKey(string GroupId, string UserId, string Role);
public record GroupWithEventParentKey( string Name, IEnumerable<GroupMemberWithEventParentKey> Members);
public record GroupMemberWithEventParentKey( string UserId, string Role);
public class GroupWithEventParentKeyProjection : IProjectionFor<GroupWithEventParentKey>{ public void Define(IProjectionBuilderFor<GroupWithEventParentKey> builder) => builder .From<GroupCreatedWithEventParentKey>() .Children(m => m.Members, children => children .IdentifiedBy(m => m.UserId) .From<UserAddedWithEventParentKey>(b => b .UsingParentKey(e => e.GroupId) .UsingKey(e => e.UserId)));}import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.IProjectionBuilderForimport io.cratis.chronicle.projections.IProjectionFor
@EventTypedata class GroupCreatedWithEventParentKey(val name: String)
@EventTypedata class UserAddedWithEventParentKey(val groupId: String, val userId: String, val role: String)
data class GroupWithEventParentKey( val name: String = "", val members: List<GroupMemberWithEventParentKey> = emptyList())
data class GroupMemberWithEventParentKey( val userId: String = "", val role: String = "")
class GroupWithEventParentKeyProjection : IProjectionFor<GroupWithEventParentKey> { override fun define(builder: IProjectionBuilderFor<GroupWithEventParentKey>) { builder .from(GroupCreatedWithEventParentKey::class) .children(GroupWithEventParentKey::members, GroupMemberWithEventParentKey::class) { children -> children .identifiedBy("userId") .from(UserAddedWithEventParentKey::class) { it.usingParentKey("groupId") .usingKey("userId") } } }}import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.IProjectionBuilderFor;import io.cratis.chronicle.projections.IProjectionFor;
import java.util.List;
@EventTyperecord GroupCreatedWithEventParentKey(String name) {}
record GroupWithEventParentKey(String name, List<GroupMemberWithEventParentKey> members) {}
record GroupMemberWithEventParentKey(String userId, String role) {}
class GroupWithEventParentKeyProjection implements IProjectionFor<GroupWithEventParentKey> { @Override public void define(IProjectionBuilderFor<GroupWithEventParentKey> builder) { builder .from(GroupCreatedWithEventParentKey.class) .children("members", GroupMemberWithEventParentKey.class, children -> { children .identifiedBy("userId") .from(UserAddedWithEventParentKey.class, fb -> { fb.usingParentKey("groupId"); fb.usingKey("userId"); }); }); }}Elixir does not support this workflow yet.import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle';
@eventType()class GroupCreatedWithEventParentKey { constructor(readonly name: string) {}}
@eventType()class UserAddedWithEventParentKey { constructor(readonly groupId: string, readonly userId: string, readonly role: string) {}}
class GroupMemberWithEventParentKey { userId = ''; role = '';}
class GroupWithEventParentKey { name = ''; members: GroupMemberWithEventParentKey[] = [];}
@projection()class GroupWithEventParentKeyProjection implements IProjectionFor<GroupWithEventParentKey> { define(builder: IProjectionBuilderFor<GroupWithEventParentKey>): void { builder .from(GroupCreatedWithEventParentKey) .children<GroupMemberWithEventParentKey>(m => m.members, children => children .identifiedBy(m => m.userId) .from(UserAddedWithEventParentKey, b => b .usingParentKey(e => e.groupId) .usingKey(e => e.userId))); }}When you append the event:
using Cratis.Chronicle;using Cratis.Chronicle.Events;using Cratis.Chronicle.EventSequences;
public class GroupMembershipWithEventParentKey(IEventStore eventStore){ public Task AddUserToGroup(EventSourceId userId, string groupId, string role) => eventStore.EventLog.Append(userId, new UserAddedWithEventParentKey(groupId, userId.Value, role));}import io.cratis.chronicle.IEventStore
class GroupMembershipWithEventParentKey(private val eventStore: IEventStore) { suspend fun addUserToGroup(userId: String, groupId: String, role: String) = eventStore.eventLog.append(userId, UserAddedWithEventParentKey(groupId, userId, role))}import io.cratis.chronicle.EventStore;import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.java.EventLogJavaBridge;
@EventTyperecord UserAddedWithEventParentKey(String groupId, String userId, String role) {}
class GroupMembershipWithEventParentKey { private final EventStore eventStore;
GroupMembershipWithEventParentKey(EventStore eventStore) { this.eventStore = eventStore; }
void addUserToGroup(String userId, String groupId, String role) { EventLogJavaBridge.append(eventStore.getEventLog(), userId, new UserAddedWithEventParentKey(groupId, userId, role), null); }}Elixir does not support this workflow yet.import { IEventStore } from '@cratis/chronicle';
class GroupMembershipWithEventParentKey { constructor(private readonly eventStore: IEventStore) {}
addUserToGroup(userId: string, groupId: string, role: string): Promise<unknown> { return this.eventStore.eventLog.append(userId, new UserAddedWithEventParentKey(groupId, userId, role)); }}The groupId property from the event content is used to find the parent Group.
Using EventSourceId explicitly with UsingParentKeyFromContext
Section titled “Using EventSourceId explicitly with UsingParentKeyFromContext”In some advanced scenarios, you might want to explicitly indicate that the EventSourceId should be used as the parent key (e.g., for documentation clarity):
using Cratis.Chronicle.Events;using Cratis.Chronicle.Projections;
[EventType]public record GroupCreatedWithContextParentKey(string Name);
[EventType]public record UserAddedWithContextParentKey(string UserId, string Role);
public record GroupWithContextParentKey( string Name, IEnumerable<GroupMemberWithContextParentKey> Members);
public record GroupMemberWithContextParentKey( string UserId, string Role);
public class GroupWithContextParentKeyProjection : IProjectionFor<GroupWithContextParentKey>{ public void Define(IProjectionBuilderFor<GroupWithContextParentKey> builder) => builder .From<GroupCreatedWithContextParentKey>() .Children(m => m.Members, children => children .IdentifiedBy(m => m.UserId) .From<UserAddedWithContextParentKey>(b => b .UsingParentKeyFromContext(c => c.EventSourceId) .UsingKey(e => e.UserId)));}Kotlin does not support this workflow yet.`IChildFromBuilderFor` only has `usingParentKey(eventPropertyName: String)`, which reads the parentkey from an event property. There is no `usingParentKeyFromContext`-equivalent for explicitlydocumenting that the EventSourceId is used — that is only ever the implicit default.Java does not support this workflow yet.`IChildFromBuilderFor` only has `usingParentKey(eventPropertyName: String)`, which reads the parentkey from an event property. There is no `usingParentKeyFromContext`-equivalent for explicitlydocumenting that the EventSourceId is used — that is only ever the implicit default.Elixir does not support this workflow yet.import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle';
@eventType()class GroupCreatedWithContextParentKey { constructor(readonly name: string) {}}
@eventType()class UserAddedWithContextParentKey { constructor(readonly userId: string, readonly role: string) {}}
class GroupMemberWithContextParentKey { userId = ''; role = '';}
class GroupWithContextParentKey { name = ''; members: GroupMemberWithContextParentKey[] = [];}
@projection()class GroupWithContextParentKeyProjection implements IProjectionFor<GroupWithContextParentKey> { define(builder: IProjectionBuilderFor<GroupWithContextParentKey>): void { builder .from(GroupCreatedWithContextParentKey) .children<GroupMemberWithContextParentKey>(m => m.members, children => children .identifiedBy(m => m.userId) .from(UserAddedWithContextParentKey, b => b .usingParentKeyFromContext('eventSourceId') .usingKey(e => e.userId))); }}This is functionally equivalent to not specifying the parent key at all, but can make the intent clearer in complex projections.
When to use each approach
Section titled “When to use each approach”- No parent key specified (default): Use when EventSourceId represents the parent identifier
UsingParentKey(e => e.Property): Use when parent identifier is in the event contentUsingParentKeyFromContext(ctx => ctx.EventSourceId): Use for explicit documentation of default behavior
Child lifecycle
Section titled “Child lifecycle”- Adding children: When a new event arrives with a previously unseen key, a new child is created
- Updating children: When an event arrives with an existing key, that child is updated
- Removing children: Use
RemovedWith<>()to specify which events remove child items
Removing children
Section titled “Removing children”The RemovedWith<>() method specifies how to remove child items from collections:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Projections;
[EventType]public record GroupCreatedWithRemoval(string Name);
[EventType]public record UserAddedWithRemoval(string UserId, string Role);
[EventType]public record UserRemovedWithRemoval(string UserId);
public record GroupWithRemoval( string Name, IEnumerable<GroupMemberWithRemoval> Members);
public record GroupMemberWithRemoval( string UserId, string Role);
public class GroupWithRemovalProjection : IProjectionFor<GroupWithRemoval>{ public void Define(IProjectionBuilderFor<GroupWithRemoval> builder) => builder .From<GroupCreatedWithRemoval>() .Children(m => m.Members, children => children .IdentifiedBy(m => m.UserId) .From<UserAddedWithRemoval>(b => b .UsingKey(e => e.UserId)) .RemovedWith<UserRemovedWithRemoval>(b => b .UsingKey(e => e.UserId)));}Kotlin does not support this workflow yet.`IChildrenBuilderFor`/`IChildFromBuilderFor` have no `removedWith`-equivalent method, and`ProjectionsService` never wires a removal list into the children definition it builds from thefluent builder — only model-bound `@RemovedWith`/`@RemovedWithJoin` on a `@ChildrenFrom` propertycan remove a single child today.Java does not support this workflow yet.`IChildrenBuilderFor`/`IChildFromBuilderFor` have no `removedWith`-equivalent method, and`ProjectionsService` never wires a removal list into the children definition it builds from thefluent builder — only model-bound `@RemovedWith`/`@RemovedWithJoin` on a `@ChildrenFrom` propertycan remove a single child today.Elixir does not support this workflow yet.import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle';
@eventType()class GroupCreatedWithRemoval { constructor(readonly name: string) {}}
@eventType()class UserAddedWithRemoval { constructor(readonly userId: string, readonly role: string) {}}
@eventType()class UserRemovedWithRemoval { constructor(readonly userId: string) {}}
class GroupMemberWithRemoval { userId = ''; role = '';}
class GroupWithRemoval { name = ''; members: GroupMemberWithRemoval[] = [];}
@projection()class GroupWithRemovalProjection implements IProjectionFor<GroupWithRemoval> { define(builder: IProjectionBuilderFor<GroupWithRemoval>): void { builder .from(GroupCreatedWithRemoval) .children<GroupMemberWithRemoval>(m => m.members, children => children .identifiedBy(m => m.userId) .from(UserAddedWithRemoval, b => b .usingKey(e => e.userId)) .removedWith(UserRemovedWithRemoval, b => b .usingKey(e => e.userId))); }}When a UserRemovedFromGroup event is processed:
- The projection looks up the child using the specified key (
e.UserId) - If found, the child is removed from the collection
- If not found, the event is ignored
You can also remove children conditionally or based on other criteria by using multiple RemovedWith<>() calls.
Multiple child collections
Section titled “Multiple child collections”A single projection can have multiple child collections:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Projections;
[EventType]public record GroupCreatedWithMultipleCollections(string Name);
[EventType]public record MemberAddedToGroup(string UserId, string Role);
[EventType]public record TaskAssignedToGroup(string TaskId, string Title);
public record GroupWithMultipleCollections( string Name, IEnumerable<GroupMemberInMultipleCollections> Members, IEnumerable<GroupTaskInMultipleCollections> Tasks);
public record GroupMemberInMultipleCollections( string UserId, string Role);
public record GroupTaskInMultipleCollections( string TaskId, string Title);
public class GroupWithMultipleCollectionsProjection : IProjectionFor<GroupWithMultipleCollections>{ public void Define(IProjectionBuilderFor<GroupWithMultipleCollections> builder) => builder .From<GroupCreatedWithMultipleCollections>() .Children(m => m.Members, children => children .IdentifiedBy(m => m.UserId) .From<MemberAddedToGroup>(b => b .UsingKey(e => e.UserId))) .Children(m => m.Tasks, children => children .IdentifiedBy(m => m.TaskId) .From<TaskAssignedToGroup>(b => b .UsingKey(e => e.TaskId)));}import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.IProjectionBuilderForimport io.cratis.chronicle.projections.IProjectionFor
@EventTypedata class GroupCreatedWithMultipleCollections(val name: String)
@EventTypedata class MemberAddedToGroup(val userId: String, val role: String)
@EventTypedata class TaskAssignedToGroup(val taskId: String, val title: String)
data class GroupWithMultipleCollections( val name: String = "", val members: List<GroupMemberInMultipleCollections> = emptyList(), val tasks: List<GroupTaskInMultipleCollections> = emptyList())
data class GroupMemberInMultipleCollections( val userId: String = "", val role: String = "")
data class GroupTaskInMultipleCollections( val taskId: String = "", val title: String = "")
class GroupWithMultipleCollectionsProjection : IProjectionFor<GroupWithMultipleCollections> { override fun define(builder: IProjectionBuilderFor<GroupWithMultipleCollections>) { builder .from(GroupCreatedWithMultipleCollections::class) .children(GroupWithMultipleCollections::members, GroupMemberInMultipleCollections::class) { children -> children .identifiedBy("userId") .from(MemberAddedToGroup::class) { it.usingKey("userId") } } .children(GroupWithMultipleCollections::tasks, GroupTaskInMultipleCollections::class) { children -> children .identifiedBy("taskId") .from(TaskAssignedToGroup::class) { it.usingKey("taskId") } } }}import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.IProjectionBuilderFor;import io.cratis.chronicle.projections.IProjectionFor;
import java.util.List;
@EventTyperecord GroupCreatedWithMultipleCollections(String name) {}
@EventTyperecord MemberAddedToGroup(String userId, String role) {}
@EventTyperecord TaskAssignedToGroup(String taskId, String title) {}
record GroupWithMultipleCollections( String name, List<GroupMemberInMultipleCollections> members, List<GroupTaskInMultipleCollections> tasks) {}
record GroupMemberInMultipleCollections(String userId, String role) {}
record GroupTaskInMultipleCollections(String taskId, String title) {}
class GroupWithMultipleCollectionsProjection implements IProjectionFor<GroupWithMultipleCollections> { @Override public void define(IProjectionBuilderFor<GroupWithMultipleCollections> builder) { builder .from(GroupCreatedWithMultipleCollections.class) .children("members", GroupMemberInMultipleCollections.class, children -> { children .identifiedBy("userId") .from(MemberAddedToGroup.class, fb -> { fb.usingKey("userId"); }); }) .children("tasks", GroupTaskInMultipleCollections.class, children -> { children .identifiedBy("taskId") .from(TaskAssignedToGroup.class, fb -> { fb.usingKey("taskId"); }); }); }}Elixir does not support this workflow yet.import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle';
@eventType()class GroupCreatedWithMultipleCollections { constructor(readonly name: string) {}}
@eventType()class MemberAddedToGroup { constructor(readonly userId: string, readonly role: string) {}}
@eventType()class TaskAssignedToGroup { constructor(readonly taskId: string, readonly title: string) {}}
class GroupMemberInMultipleCollections { userId = ''; role = '';}
class GroupTaskInMultipleCollections { taskId = ''; title = '';}
class GroupWithMultipleCollections { name = ''; members: GroupMemberInMultipleCollections[] = []; tasks: GroupTaskInMultipleCollections[] = [];}
@projection()class GroupWithMultipleCollectionsProjection implements IProjectionFor<GroupWithMultipleCollections> { define(builder: IProjectionBuilderFor<GroupWithMultipleCollections>): void { builder .from(GroupCreatedWithMultipleCollections) .children<GroupMemberInMultipleCollections>(m => m.members, children => children .identifiedBy(m => m.userId) .from(MemberAddedToGroup, b => b .usingKey(e => e.userId))) .children<GroupTaskInMultipleCollections>(m => m.tasks, children => children .identifiedBy(m => m.taskId) .from(TaskAssignedToGroup, b => b .usingKey(e => e.taskId))); }}This pattern allows you to build rich, hierarchical read models from events.