Table of Contents

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

  1. Root events (GroupCreated) update properties on the main read model
  2. Child events (UserAddedToGroup, UserRoleChanged) are routed to child items
  3. IdentifiedBy() specifies how to identify child items (by UserId in this example)
  4. UsingKey() tells the projection which property contains the child identifier
  5. 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:

  1. The projection looks up the child using the specified key (e.UserId)
  2. If found, the child is removed from the collection
  3. 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.