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.
Defining a nested object projection
Section titled “Defining a nested object projection”Use the Nested() method with ClearWith<TEvent>() to define the nested relationship:
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>());}Read model with a nested property
Section titled “Read model with a nested property”The nested property must be nullable on the read model:
public record SliceWithNestedCommand( string Name, CommandItemForNestedCommand? Command);
public record CommandItemForNestedCommand( string Name, string Schema);Event definitions
Section titled “Event definitions”using Cratis.Chronicle.Events;
[EventType]public record SliceCreatedForNestedEvents(string Name);
[EventType]public record CommandSetForNestedEvents(string Name, string Schema);
[EventType]public record CommandClearedForNestedEvents;How nested objects work
Section titled “How nested objects work”- When
CommandSetForSliceis appended theCommandproperty is populated on the parent - Subsequent
CommandSetForSliceevents replace the nested object with new values - When
CommandClearedForSliceis appended theCommandproperty is set tonull
Multiple from events
Section titled “Multiple from events”Call From<TEvent>() multiple times to update the nested object from several event types:
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 on nested objects
Section titled “AutoMap on nested objects”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:
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>());}Multiple nested objects
Section titled “Multiple nested objects”A single projection can have multiple independent nested properties:
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>());}Nested within children
Section titled “Nested within children”You can call Nested() from within a Children() builder to define a nested object on each child item:
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>()));}Examples
Section titled “Examples”Employee with optional active contract
Section titled “Employee with optional active contract”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:
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 with optional promotion
Section titled “Product with optional promotion”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>());}See Also
Section titled “See Also”- Children — collections of items managed independently within a parent
- Simple projection — getting started with projections
- AutoMap — automatic property mapping