Skip to content

Projection with a Nested Object

Projections can populate a single nullable child object on a read model using the Nested() method. Unlike children collections, which manage an array of items identified by a key, Nested() targets a scalar nullable property that is set from an event and cleared (set to null) by another event.

Use the Nested() method with ClearWith<TEvent>() to define the nested relationship:

Nested object projection
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Projections;
[EventType]
public record SliceCreatedForNestedBasic(string Name);
[EventType]
public record CommandSetForDeclarativeNestedBasic(string Name, string Schema);
[EventType]
public record CommandClearedForDeclarativeNestedBasic;
public record SliceForNestedBasic(
string Name,
CommandItemForNestedBasic? Command);
public record CommandItemForNestedBasic(
string Name,
string Schema);
public class SliceProjectionForNestedBasic : IProjectionFor<SliceForNestedBasic>
{
public void Define(IProjectionBuilderFor<SliceForNestedBasic> builder) => builder
.From<SliceCreatedForNestedBasic>()
.Nested(m => m.Command, nested => nested
.From<CommandSetForDeclarativeNestedBasic>()
.ClearWith<CommandClearedForDeclarativeNestedBasic>());
}

The nested property must be nullable on the read model:

Read model with nested object
public record SliceWithNestedCommand(
string Name,
CommandItemForNestedCommand? Command);
public record CommandItemForNestedCommand(
string Name,
string Schema);
Nested object events
using Cratis.Chronicle.Events;
[EventType]
public record SliceCreatedForNestedEvents(string Name);
[EventType]
public record CommandSetForNestedEvents(string Name, string Schema);
[EventType]
public record CommandClearedForNestedEvents;
  1. When CommandSetForSlice is appended the Command property is populated on the parent
  2. Subsequent CommandSetForSlice events replace the nested object with new values
  3. When CommandClearedForSlice is appended the Command property is set to null

Call From<TEvent>() multiple times to update the nested object from several event types:

Multiple nested events
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Projections;
[EventType]
public record SliceCreatedForNestedUpdates(string Name);
[EventType]
public record CommandSetForNestedUpdates(string Name, string Schema);
[EventType]
public record CommandRenamedForNestedUpdates(string NewName);
[EventType]
public record CommandSchemaUpdatedForNestedUpdates(string UpdatedSchema);
[EventType]
public record CommandClearedForNestedUpdates;
public record SliceForNestedUpdates(
string Name,
CommandItemForNestedUpdates? Command);
public record CommandItemForNestedUpdates(
string Name,
string Schema);
public class SliceProjectionForNestedUpdates : IProjectionFor<SliceForNestedUpdates>
{
public void Define(IProjectionBuilderFor<SliceForNestedUpdates> builder) => builder
.From<SliceCreatedForNestedUpdates>()
.Nested(m => m.Command, nested => nested
.From<CommandSetForNestedUpdates>()
.From<CommandRenamedForNestedUpdates>(b => b
.Set(m => m.Name).To(e => e.NewName))
.From<CommandSchemaUpdatedForNestedUpdates>(b => b
.Set(m => m.Schema).To(e => e.UpdatedSchema))
.ClearWith<CommandClearedForNestedUpdates>());
}

Each From<TEvent>() call updates only the properties it explicitly maps or auto-maps — it does not replace the entire nested object.

AutoMap is enabled on the nested builder and inherits from the parent. Properties on the nested read model that share a name with properties on the event are mapped automatically:

AutoMap in a nested scope
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Projections;
[EventType]
public record SliceCreatedForNestedAutoMap(string Name);
[EventType]
public record CommandSetForNestedAutoMap(string Name, string Schema);
[EventType]
public record CommandUpdatedForNestedAutoMap(string Schema);
[EventType]
public record CommandClearedForNestedAutoMap;
public record SliceForNestedAutoMap(
string Name,
CommandItemForNestedAutoMap? Command);
public record CommandItemForNestedAutoMap(
string Name,
string Schema);
public class SliceProjectionForNestedAutoMap : IProjectionFor<SliceForNestedAutoMap>
{
public void Define(IProjectionBuilderFor<SliceForNestedAutoMap> builder) => builder
.From<SliceCreatedForNestedAutoMap>()
.Nested(m => m.Command, nested => nested
.From<CommandSetForNestedAutoMap>()
.From<CommandUpdatedForNestedAutoMap>()
.ClearWith<CommandClearedForNestedAutoMap>());
}

A single projection can have multiple independent nested properties:

Multiple nested objects
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Projections;
[EventType]
public record SliceCreatedWithMultipleNested(string Name);
[EventType]
public record CommandSetWithMultipleNested(string Name, string Schema);
[EventType]
public record CommandClearedWithMultipleNested;
[EventType]
public record ValidationConfiguredWithMultipleNested(string RuleName);
[EventType]
public record ValidationRemovedWithMultipleNested;
public record SliceWithMultipleNested(
string Name,
CommandItemWithMultipleNested? Command,
ValidationConfigWithMultipleNested? Validation);
public record CommandItemWithMultipleNested(
string Name,
string Schema);
public record ValidationConfigWithMultipleNested(
string RuleName);
public class SliceProjectionWithMultipleNested : IProjectionFor<SliceWithMultipleNested>
{
public void Define(IProjectionBuilderFor<SliceWithMultipleNested> builder) => builder
.From<SliceCreatedWithMultipleNested>()
.Nested(m => m.Command, nested => nested
.From<CommandSetWithMultipleNested>()
.ClearWith<CommandClearedWithMultipleNested>())
.Nested(m => m.Validation, nested => nested
.From<ValidationConfiguredWithMultipleNested>()
.ClearWith<ValidationRemovedWithMultipleNested>());
}

You can call Nested() from within a Children() builder to define a nested object on each child item:

Nested object in children
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Projections;
[EventType]
public record ProjectCreatedWithNestedChildren(string Name);
[EventType]
public record TaskAddedWithNestedChild(Guid TaskId, string Title);
[EventType]
public record TaskAssignedWithNestedChild(Guid TaskId, string Name, string Email);
[EventType]
public record TaskUnassignedWithNestedChild(Guid TaskId);
public record ProjectWithDeclarativeNestedChildren(
string Name,
IEnumerable<TaskWithNestedAssignee> Tasks);
public record TaskWithNestedAssignee(
Guid TaskId,
string Title,
AssigneeForNestedChild? Assignee);
public record AssigneeForNestedChild(
string Name,
string Email);
public class ProjectProjectionWithDeclarativeNestedChildren : IProjectionFor<ProjectWithDeclarativeNestedChildren>
{
public void Define(IProjectionBuilderFor<ProjectWithDeclarativeNestedChildren> builder) => builder
.From<ProjectCreatedWithNestedChildren>()
.Children(m => m.Tasks, tasks => tasks
.IdentifiedBy(m => m.TaskId)
.From<TaskAddedWithNestedChild>(b => b
.UsingKey(e => e.TaskId))
.Nested(m => m.Assignee, assignee => assignee
.From<TaskAssignedWithNestedChild>(b => b
.UsingKey(e => e.TaskId))
.ClearWith<TaskUnassignedWithNestedChild>()));
}
Employee contract projection
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Projections;
[EventType]
public record EmployeeHiredWithNestedContract(string Name, string Department);
[EventType]
public record ContractStartedWithNestedContract(Guid ContractId, DateOnly StartDate, DateOnly EndDate, string Type);
[EventType]
public record ContractExtendedWithNestedContract(DateOnly NewEndDate);
[EventType]
public record ContractEndedWithNestedContract;
public record EmployeeWithNestedContract(
string Name,
string Department,
ContractForNestedEmployee? ActiveContract);
public record ContractForNestedEmployee(
Guid ContractId,
DateOnly StartDate,
DateOnly EndDate,
string Type);
public class EmployeeProjectionWithNestedContract : IProjectionFor<EmployeeWithNestedContract>
{
public void Define(IProjectionBuilderFor<EmployeeWithNestedContract> builder) => builder
.From<EmployeeHiredWithNestedContract>()
.Nested(m => m.ActiveContract, contract => contract
.From<ContractStartedWithNestedContract>()
.From<ContractExtendedWithNestedContract>(b => b
.Set(m => m.EndDate).To(e => e.NewEndDate))
.ClearWith<ContractEndedWithNestedContract>());
}

Events:

Employee contract events
using Cratis.Chronicle.Events;
[EventType]
public record EmployeeHiredForNestedContractEvents(string Name, string Department);
[EventType]
public record ContractStartedForNestedContractEvents(Guid ContractId, DateOnly StartDate, DateOnly EndDate, string Type);
[EventType]
public record ContractExtendedForNestedContractEvents(DateOnly NewEndDate);
[EventType]
public record ContractEndedForNestedContractEvents;
Product promotion projection
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Projections;
[EventType]
public record ProductListedWithNestedPromotion(string Name, decimal BasePrice);
[EventType]
public record PromotionAppliedWithNestedPromotion(string Label, int DiscountPercent, DateTimeOffset ValidUntil);
[EventType]
public record PromotionRemovedWithNestedPromotion;
public record ProductWithNestedPromotion(
string Name,
decimal BasePrice,
PromotionForNestedProduct? Promotion);
public record PromotionForNestedProduct(
string Label,
int DiscountPercent,
DateTimeOffset ValidUntil);
public class ProductProjectionWithNestedPromotion : IProjectionFor<ProductWithNestedPromotion>
{
public void Define(IProjectionBuilderFor<ProductWithNestedPromotion> builder) => builder
.From<ProductListedWithNestedPromotion>()
.Nested(m => m.Promotion, promotion => promotion
.From<PromotionAppliedWithNestedPromotion>()
.ClearWith<PromotionRemovedWithNestedPromotion>());
}
  • Children — collections of items managed independently within a parent
  • Simple projection — getting started with projections
  • AutoMap — automatic property mapping