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
Use the Children() method to define child collections:
using Cratis.Chronicle.Projections;
public class GroupProjection : IProjectionFor<Group>
{
public void Define(IProjectionBuilderFor<Group> builder) => builder
.From<GroupCreated>(b => b
.Set(m => m.Name).To(e => e.Name)
.Set(m => m.Description).To(e => e.Description))
.Children(m => m.Members, children => children
.IdentifiedBy(e => e.UserId)
.From<UserAddedToGroup>(b => b
.UsingKey(e => e.UserId)
.Set(m => m.UserId).To(e => e.UserId)
.Set(m => m.Role).To(e => e.Role))
.From<UserRoleChanged>(b => b
.UsingKey(e => e.UserId)
.Set(m => m.Role).To(e => e.NewRole))
.RemovedWith<UserRemovedFromGroup>(e => e.UserId));
}
Read model with children
The read model includes a collection property for the children:
public record Group(
string Name,
string Description,
IEnumerable<GroupMember> Members);
public record GroupMember(
string UserId,
string Role);
Event definitions
Events that affect children use keys to identify which child to update:
[EventType]
public record GroupCreated(string Name, string Description);
[EventType]
public record UserAddedToGroup(string UserId, string Role);
[EventType]
public record UserRoleChanged(string UserId, string NewRole);
[EventType]
public record UserRemovedFromGroup(string UserId);
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
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
The RemovedWith<>() method specifies how to remove child items from collections:
.Children(m => m.Members, children => children
.IdentifiedBy(e => e.UserId)
.From<UserAddedToGroup>(_ => /* ... */)
.RemovedWith<UserRemovedFromGroup>(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
A single projection can have multiple child collections:
.Children(m => m.Members, children => children
.IdentifiedBy(e => e.UserId)
.From<UserAddedToGroup>(_ => /* ... */))
.Children(m => m.Tasks, children => children
.IdentifiedBy(e => e.TaskId)
.From<TaskAssignedToGroup>(_ => /* ... */));
This pattern allows you to build rich, hierarchical read models from events.