Removing Read Models
Model-bound projections remove root read models with RemovedWith and child items with RemovedWith or RemovedWithJoin. Root-level RemovedWithJoin is not yet supported: the engine ignores it and logs a warning. Do not rely on it to remove root instances.
Removing Root Read Models
Section titled “Removing Root Read Models”Use RemovedWith at the class level to specify which event removes the entire read model instance:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Keys;using Cratis.Chronicle.Projections.ModelBound;
[EventType]public record MbRemovalAccountOpened(string Name, decimal Balance);
[EventType]public record MbRemovalAccountClosed;
[RemovedWith<MbRemovalAccountClosed>]public record MbRemovalAccount( [Key] Guid Id,
[SetFrom<MbRemovalAccountOpened>(nameof(MbRemovalAccountOpened.Name))] string Name,
[SetFrom<MbRemovalAccountOpened>(nameof(MbRemovalAccountOpened.Balance))] decimal Balance);import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.projections.RemovedWithimport io.cratis.chronicle.projections.SetFromimport io.cratis.chronicle.readModels.ReadModel
@EventTypedata class MbRemovalAccountOpened(val name: String, val balance: Double)
@EventTypeclass MbRemovalAccountClosed
@ReadModel@FromEvent(MbRemovalAccountOpened::class)@RemovedWith(MbRemovalAccountClosed::class)data class MbRemovalAccount( @SetFrom("name", MbRemovalAccountOpened::class) val name: String = "",
@SetFrom("balance", MbRemovalAccountOpened::class) val balance: Double = 0.0)import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.projections.RemovedWith;import io.cratis.chronicle.projections.SetFrom;import io.cratis.chronicle.readModels.ReadModel;
@EventTyperecord MbRemovalAccountOpened(String name, double balance) {}
@EventTypeclass MbRemovalAccountClosed {}
@ReadModel@FromEvent(eventType = MbRemovalAccountOpened.class)@RemovedWith(eventType = MbRemovalAccountClosed.class)class MbRemovalAccount { @SetFrom(propertyPath = "name", eventType = MbRemovalAccountOpened.class) public String name = "";
@SetFrom(propertyPath = "balance", eventType = MbRemovalAccountOpened.class) public double balance = 0.0;}defmodule MyApp.Events.RemovalAccountOpened do use Chronicle.Events.EventType, id: "removal-account-opened-v1"
defstruct [:name, :balance]end
defmodule MyApp.Events.RemovalAccountClosed do use Chronicle.Events.EventType, id: "removal-account-closed-v1"
defstruct []end
defmodule MyApp.ReadModels.RemovalAccount do use Chronicle.ReadModels.ReadModel
alias MyApp.Events.{RemovalAccountOpened, RemovalAccountClosed}
defstruct [:id, :name, :balance]
from RemovalAccountOpened, set: [id: :event_source_id, name: :name, balance: :balance]
removed_with RemovalAccountClosed, []endimport { eventType, Guid, removedWith, setFrom } from '@cratis/chronicle';
@eventType()export class MbRemovalAccountOpened { name = ''; balance = 0;}
@eventType()export class MbRemovalAccountClosed {}
@removedWith(MbRemovalAccountClosed)export class MbRemovalAccount { id: Guid = Guid.empty;
@setFrom(MbRemovalAccountOpened, 'name') name = '';
@setFrom(MbRemovalAccountOpened, 'balance') balance = 0;}When an AccountClosed event occurs, the corresponding Account read model is removed from the store.
With Custom Key
Section titled “With Custom Key”You can specify which property on the event identifies the read model to remove:
[EventType]public record MbRemovalWithKeyAccountOpened(string Name);
[EventType]public record MbRemovalWithKeyAccountClosed(Guid AccountId);
[RemovedWith<MbRemovalWithKeyAccountClosed>(key: nameof(MbRemovalWithKeyAccountClosed.AccountId))]public record MbRemovalWithKeyAccount( [Key] Guid Id,
[SetFrom<MbRemovalWithKeyAccountOpened>(nameof(MbRemovalWithKeyAccountOpened.Name))] string Name);import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.projections.RemovedWithimport io.cratis.chronicle.projections.SetFromimport io.cratis.chronicle.readModels.ReadModel
@EventTypedata class MbRemovalWithKeyAccountOpened(val name: String)
@EventTypedata class MbRemovalWithKeyAccountClosed(val accountId: String)
@ReadModel@FromEvent(MbRemovalWithKeyAccountOpened::class)@RemovedWith(MbRemovalWithKeyAccountClosed::class, key = "accountId")data class MbRemovalWithKeyAccount( @SetFrom("name", MbRemovalWithKeyAccountOpened::class) val name: String = "")import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.projections.RemovedWith;import io.cratis.chronicle.projections.SetFrom;import io.cratis.chronicle.readModels.ReadModel;
@EventTyperecord MbRemovalWithKeyAccountOpened(String name) {}
@EventTyperecord MbRemovalWithKeyAccountClosed(String accountId) {}
@ReadModel@FromEvent(eventType = MbRemovalWithKeyAccountOpened.class)@RemovedWith(eventType = MbRemovalWithKeyAccountClosed.class, key = "accountId")class MbRemovalWithKeyAccount { @SetFrom(propertyPath = "name", eventType = MbRemovalWithKeyAccountOpened.class) public String name = "";}defmodule MyApp.Events.RemovalWithKeyAccountOpened do use Chronicle.Events.EventType, id: "removal-with-key-account-opened-v1"
defstruct [:name]end
defmodule MyApp.Events.RemovalWithKeyAccountClosed do use Chronicle.Events.EventType, id: "removal-with-key-account-closed-v1"
defstruct [:account_id]end
defmodule MyApp.ReadModels.RemovalWithKeyAccount do use Chronicle.ReadModels.ReadModel
alias MyApp.Events.{RemovalWithKeyAccountOpened, RemovalWithKeyAccountClosed}
defstruct [:id, :name]
from RemovalWithKeyAccountOpened, set: [id: :event_source_id, name: :name]
removed_with RemovalWithKeyAccountClosed, key: :account_idendimport { eventType, Guid, removedWith, setFrom } from '@cratis/chronicle';
@eventType()export class MbRemovalWithKeyAccountOpened { name = '';}
@eventType()export class MbRemovalWithKeyAccountClosed { accountId: Guid = Guid.empty;}
@removedWith(MbRemovalWithKeyAccountClosed, 'accountId')export class MbRemovalWithKeyAccount { id: Guid = Guid.empty;
@setFrom(MbRemovalWithKeyAccountOpened, 'name') name = '';}Multiple Removal Options
Section titled “Multiple Removal Options”A read model can be removed by multiple different events:
[EventType]public record MbRemovalMultipleAccountOpened(string Name);
[EventType]public record MbRemovalMultipleAccountClosed;
[EventType]public record MbRemovalMultipleAccountMerged(Guid SourceAccountId);
[RemovedWith<MbRemovalMultipleAccountClosed>][RemovedWith<MbRemovalMultipleAccountMerged>(key: nameof(MbRemovalMultipleAccountMerged.SourceAccountId))]public record MbRemovalMultipleAccount( [Key] Guid Id,
[SetFrom<MbRemovalMultipleAccountOpened>(nameof(MbRemovalMultipleAccountOpened.Name))] string Name);import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.projections.RemovedWithimport io.cratis.chronicle.projections.RemovedWithJoinimport io.cratis.chronicle.projections.SetFromimport io.cratis.chronicle.readModels.ReadModel
@EventTypedata class MbRemovalMultipleAccountOpened(val name: String)
@EventTypeclass MbRemovalMultipleAccountClosed
@EventTypedata class MbRemovalMultipleAccountMerged(val sourceAccountId: String)
@EventTypeclass MbRemovalMultipleOrganizationClosed
@ReadModel@FromEvent(MbRemovalMultipleAccountOpened::class)@RemovedWith(MbRemovalMultipleAccountClosed::class)@RemovedWith(MbRemovalMultipleAccountMerged::class, key = "sourceAccountId")@RemovedWithJoin(MbRemovalMultipleOrganizationClosed::class)data class MbRemovalMultipleAccount( @SetFrom("name", MbRemovalMultipleAccountOpened::class) val name: String = "")import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.projections.RemovedWith;import io.cratis.chronicle.projections.RemovedWithJoin;import io.cratis.chronicle.projections.SetFrom;import io.cratis.chronicle.readModels.ReadModel;
@EventTyperecord MbRemovalMultipleAccountOpened(String name) {}
@EventTypeclass MbRemovalMultipleAccountClosed {}
@EventTyperecord MbRemovalMultipleAccountMerged(String sourceAccountId) {}
@EventTypeclass MbRemovalMultipleOrganizationClosed {}
@ReadModel@FromEvent(eventType = MbRemovalMultipleAccountOpened.class)@RemovedWith(eventType = MbRemovalMultipleAccountClosed.class)@RemovedWith(eventType = MbRemovalMultipleAccountMerged.class, key = "sourceAccountId")@RemovedWithJoin(eventType = MbRemovalMultipleOrganizationClosed.class)class MbRemovalMultipleAccount { @SetFrom(propertyPath = "name", eventType = MbRemovalMultipleAccountOpened.class) public String name = "";}defmodule MyApp.Events.MbRemovalMultipleAccountOpened do use Chronicle.Events.EventType, id: "mb-removal-multiple-account-opened"
defstruct [:name]end
defmodule MyApp.Events.MbRemovalMultipleAccountClosed do use Chronicle.Events.EventType, id: "mb-removal-multiple-account-closed"
defstruct []end
defmodule MyApp.Events.MbRemovalMultipleAccountMerged do use Chronicle.Events.EventType, id: "mb-removal-multiple-account-merged"
defstruct [:source_account_id]end
defmodule MyApp.ReadModels.MbRemovalMultipleAccount do use Chronicle.ReadModels.ReadModel
defstruct id: nil, name: nil
from MyApp.Events.MbRemovalMultipleAccountOpened, set: [id: :event_source_id, name: :name]
removed_with MyApp.Events.MbRemovalMultipleAccountClosed, []
# The merged event is raised on the surviving account; its `source_account_id` names the # account that is removed. removed_with MyApp.Events.MbRemovalMultipleAccountMerged, key: :source_account_idendimport { eventType, Guid, removedWith, removedWithJoin, setFrom } from '@cratis/chronicle';
@eventType()export class MbRemovalMultipleAccountOpened { name = '';}
@eventType()export class MbRemovalMultipleAccountClosed {}
@eventType()export class MbRemovalMultipleAccountMerged { sourceAccountId: Guid = Guid.empty;}
@eventType()export class MbRemovalMultipleOrganizationClosed {}
@removedWith(MbRemovalMultipleAccountClosed)@removedWith(MbRemovalMultipleAccountMerged, 'sourceAccountId')@removedWithJoin(MbRemovalMultipleOrganizationClosed)export class MbRemovalMultipleAccount { id: Guid = Guid.empty;
@setFrom(MbRemovalMultipleAccountOpened, 'name') name = '';}In this example, an account can be removed by an AccountClosed event or an AccountMerged event when it is the source account.
RemovedWithJoin
Section titled “RemovedWithJoin”RemovedWithJoin on a root class is not yet supported. The engine logs a warning and does not subscribe to the removal event. The .NET client also logs a warning at startup when a projection declares it at the root. Do not add this attribute to a root read model: it will not delete its instances. Use RemovedWith for root deletion, or use RemovedWithJoin on a child collection for child-item removal.
Removing Children
Section titled “Removing Children”Children can be removed in two ways:
Property-Level Removal
Section titled “Property-Level Removal”Apply RemovedWith on the collection property alongside ChildrenFrom:
[EventType]public record MbChildrenRemovalPropertyLineItemAdded(Guid ItemId, string Description);
[EventType]public record MbChildrenRemovalPropertyLineItemRemoved(Guid ItemId);
public record MbChildrenRemovalPropertyOrder( [Key] Guid Id,
[ChildrenFrom<MbChildrenRemovalPropertyLineItemAdded>(key: nameof(MbChildrenRemovalPropertyLineItemAdded.ItemId))] [RemovedWith<MbChildrenRemovalPropertyLineItemRemoved>(key: nameof(MbChildrenRemovalPropertyLineItemRemoved.ItemId))] IEnumerable<MbChildrenRemovalPropertyOrderLine> Lines);
public record MbChildrenRemovalPropertyOrderLine( [Key] Guid Id, string Description);import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.ChildrenFromimport io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.projections.RemovedWithimport io.cratis.chronicle.projections.SetFromimport io.cratis.chronicle.readModels.ReadModel
@EventTypedata class MbChildrenRemovalPropertyLineItemAdded(val itemId: String, val description: String)
@EventTypedata class MbChildrenRemovalPropertyLineItemRemoved(val itemId: String)
@ReadModel@FromEvent(MbChildrenRemovalPropertyLineItemAdded::class)data class MbChildrenRemovalPropertyOrder( @ChildrenFrom(MbChildrenRemovalPropertyLineItemAdded::class, key = "itemId", identifiedBy = "itemId") @RemovedWith(MbChildrenRemovalPropertyLineItemRemoved::class, key = "itemId") val lines: List<MbChildrenRemovalPropertyOrderLine> = emptyList())
data class MbChildrenRemovalPropertyOrderLine( val itemId: String = "",
@SetFrom("description", MbChildrenRemovalPropertyLineItemAdded::class) val description: String = "")import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.ChildrenFrom;import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.projections.RemovedWith;import io.cratis.chronicle.projections.SetFrom;import io.cratis.chronicle.readModels.ReadModel;
import java.util.Collections;import java.util.List;
@EventTyperecord MbChildrenRemovalPropertyLineItemAdded(String itemId, String description) {}
@EventTyperecord MbChildrenRemovalPropertyLineItemRemoved(String itemId) {}
@ReadModel@FromEvent(eventType = MbChildrenRemovalPropertyLineItemAdded.class)class MbChildrenRemovalPropertyOrder { @ChildrenFrom(eventType = MbChildrenRemovalPropertyLineItemAdded.class, key = "itemId", identifiedBy = "itemId") @RemovedWith(eventType = MbChildrenRemovalPropertyLineItemRemoved.class, key = "itemId") public List<MbChildrenRemovalPropertyOrderLine> lines = Collections.emptyList();}
class MbChildrenRemovalPropertyOrderLine { public String itemId = "";
@SetFrom(propertyPath = "description", eventType = MbChildrenRemovalPropertyLineItemAdded.class) public String description = "";}defmodule MyApp.Events.MbChildrenLineItemAddedForRemoval do use Chronicle.Events.EventType, id: "mb-children-line-item-added-for-removal"
defstruct [:line_item_id, :product_name]end
defmodule MyApp.Events.MbChildrenLineItemRemoved do use Chronicle.Events.EventType, id: "mb-children-line-item-removed"
defstruct [:line_item_id]end
defmodule MyApp.ReadModels.MbChildrenRemovableLine do defstruct line_item_id: nil, product_name: nilend
defmodule MyApp.ReadModels.MbChildrenOrderWithRemoval do use Chronicle.ReadModels.ReadModel, types: [lines: {:list, MyApp.ReadModels.MbChildrenRemovableLine}]
defstruct order_id: nil, lines: []
children :lines, identified_by: :line_item_id do from MyApp.Events.MbChildrenLineItemAddedForRemoval, key: :line_item_id, set: [line_item_id: :line_item_id, product_name: :product_name]
removed_with MyApp.Events.MbChildrenLineItemRemoved, key: :line_item_id endendimport { childrenFrom, eventType, Guid, removedWith } from '@cratis/chronicle';import { field } from '@cratis/fundamentals';
@eventType()export class MbChildrenRemovalPropertyLineItemAdded { itemId: Guid = Guid.empty; description = '';}
@eventType()export class MbChildrenRemovalPropertyLineItemRemoved { itemId: Guid = Guid.empty;}
export class MbChildrenRemovalPropertyOrderLine { id: Guid = Guid.empty; description = '';}
export class MbChildrenRemovalPropertyOrder { id: Guid = Guid.empty;
@childrenFrom(MbChildrenRemovalPropertyLineItemAdded, 'itemId') @removedWith(MbChildrenRemovalPropertyLineItemRemoved, 'itemId') @field(Array, { genericArguments: [MbChildrenRemovalPropertyOrderLine] }) lines: MbChildrenRemovalPropertyOrderLine[] = [];}Class-Level Removal on Child Types
Section titled “Class-Level Removal on Child Types”Apply RemovedWith directly on the child type. This is particularly useful when the same child model is used in multiple parents or when you want to keep removal logic with the child definition:
[EventType]public record MbChildrenRemovalClassLineItemAdded(Guid ItemId, string Description);
[EventType]public record MbChildrenRemovalClassLineItemRemoved(Guid OrderId, Guid ItemId);
public record MbChildrenRemovalClassOrder( [Key] Guid Id,
[ChildrenFrom<MbChildrenRemovalClassLineItemAdded>(key: nameof(MbChildrenRemovalClassLineItemAdded.ItemId))] IEnumerable<MbChildrenRemovalClassOrderLine> Lines);
[RemovedWith<MbChildrenRemovalClassLineItemRemoved>( key: nameof(MbChildrenRemovalClassLineItemRemoved.ItemId), parentKey: nameof(MbChildrenRemovalClassLineItemRemoved.OrderId))]public record MbChildrenRemovalClassOrderLine( [Key] Guid Id, string Description);Kotlin does not support this workflow yet.Java does not support this workflow yet.# Elixir has no separate child-type module to carry a class-level removal attribute — a child's# `removed_with` always lives inside the owning `children` block, so this is the same declaration# as the property-level form.defmodule MyApp.Events.MbChildrenLineItemAddedForClassRemoval do use Chronicle.Events.EventType, id: "mb-children-line-item-added-for-class-removal"
defstruct [:line_item_id, :product_name]end
defmodule MyApp.Events.MbChildrenLineItemRemovedClassLevel do use Chronicle.Events.EventType, id: "mb-children-line-item-removed-class-level"
defstruct [:line_item_id]end
defmodule MyApp.ReadModels.MbChildrenRemovableLineClassLevel do defstruct line_item_id: nil, product_name: nilend
defmodule MyApp.ReadModels.MbChildrenOrderWithClassLevelRemoval do use Chronicle.ReadModels.ReadModel, types: [lines: {:list, MyApp.ReadModels.MbChildrenRemovableLineClassLevel}]
defstruct order_id: nil, lines: []
children :lines, identified_by: :line_item_id do from MyApp.Events.MbChildrenLineItemAddedForClassRemoval, key: :line_item_id, set: [line_item_id: :line_item_id, product_name: :product_name]
removed_with MyApp.Events.MbChildrenLineItemRemovedClassLevel, key: :line_item_id endendimport { childrenFrom, eventType, Guid, removedWith } from '@cratis/chronicle';import { field } from '@cratis/fundamentals';
@eventType()export class MbChildrenRemovalClassLineItemAdded { itemId: Guid = Guid.empty; description = '';}
@eventType()export class MbChildrenRemovalClassLineItemRemoved { orderId: Guid = Guid.empty; itemId: Guid = Guid.empty;}
@removedWith(MbChildrenRemovalClassLineItemRemoved, 'itemId', 'orderId')export class MbChildrenRemovalClassOrderLine { id: Guid = Guid.empty; description = '';}
export class MbChildrenRemovalClassOrder { id: Guid = Guid.empty;
@childrenFrom(MbChildrenRemovalClassLineItemAdded, 'itemId') @field(Array, { genericArguments: [MbChildrenRemovalClassOrderLine] }) lines: MbChildrenRemovalClassOrderLine[] = [];}Both approaches produce the same result. The class-level approach keeps the removal definition with the child type, while the property-level approach keeps it with the parent.
Parameters
Section titled “Parameters”For child removal, you can specify:
- key: Property on the event that identifies which child to remove
- parentKey: Property on the event that identifies the parent (defaults to EventSourceId)
Children with RemovedWithJoin
Section titled “Children with RemovedWithJoin”For children that should be removed based on join events, apply RemovedWithJoin on the collection property (it also works at the child type’s class level, the same way RemovedWith does above):
[EventType]public record MbChildrenRemovedFeatureActivated(Guid FeatureId, string Name);
[EventType]public record MbChildrenRemovedFeatureDeactivated(Guid FeatureId);
public record MbChildrenRemovedSubscription( [Key] Guid SubscriptionId,
[ChildrenFrom<MbChildrenRemovedFeatureActivated>(key: nameof(MbChildrenRemovedFeatureActivated.FeatureId))] [RemovedWithJoin<MbChildrenRemovedFeatureDeactivated>(key: nameof(MbChildrenRemovedFeatureDeactivated.FeatureId))] IEnumerable<MbChildrenRemovedFeature> Features);
public record MbChildrenRemovedFeature( [Key] Guid FeatureId, string Name);import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.ChildrenFromimport io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.projections.RemovedWithJoinimport io.cratis.chronicle.projections.SetFromimport io.cratis.chronicle.readModels.ReadModel
@EventTypedata class MbChildrenRemovedFeatureActivated(val featureId: String, val name: String)
@EventTypedata class MbChildrenRemovedFeatureDeactivated(val featureId: String)
@ReadModel@FromEvent(MbChildrenRemovedFeatureActivated::class)data class MbChildrenRemovedSubscription( @ChildrenFrom(MbChildrenRemovedFeatureActivated::class, key = "featureId", identifiedBy = "featureId") @RemovedWithJoin(MbChildrenRemovedFeatureDeactivated::class, key = "featureId") val features: List<MbChildrenRemovedFeature> = emptyList())
data class MbChildrenRemovedFeature( val featureId: String = "",
@SetFrom("name", MbChildrenRemovedFeatureActivated::class) val name: String = "")import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.ChildrenFrom;import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.projections.RemovedWithJoin;import io.cratis.chronicle.projections.SetFrom;import io.cratis.chronicle.readModels.ReadModel;
import java.util.Collections;import java.util.List;
@EventTyperecord MbChildrenRemovedFeatureActivated(String featureId, String name) {}
@EventTyperecord MbChildrenRemovedFeatureDeactivated(String featureId) {}
@ReadModel@FromEvent(eventType = MbChildrenRemovedFeatureActivated.class)class MbChildrenRemovedSubscription { @ChildrenFrom(eventType = MbChildrenRemovedFeatureActivated.class, key = "featureId", identifiedBy = "featureId") @RemovedWithJoin(eventType = MbChildrenRemovedFeatureDeactivated.class, key = "featureId") public List<MbChildrenRemovedFeature> features = Collections.emptyList();}
class MbChildrenRemovedFeature { public String featureId = "";
@SetFrom(propertyPath = "name", eventType = MbChildrenRemovedFeatureActivated.class) public String name = "";}defmodule MyApp.Events.MbChildrenGroupMemberAdded do use Chronicle.Events.EventType, id: "mb-children-group-member-added"
defstruct [:member_id, :member_name]end
defmodule MyApp.Events.MbChildrenMemberDeletedElsewhere do use Chronicle.Events.EventType, id: "mb-children-member-deleted-elsewhere"
defstruct [:member_id]end
defmodule MyApp.ReadModels.MbChildrenGroupMember do defstruct member_id: nil, member_name: nilend
defmodule MyApp.ReadModels.MbChildrenGroupWithJoinRemoval do use Chronicle.ReadModels.ReadModel, types: [members: {:list, MyApp.ReadModels.MbChildrenGroupMember}]
defstruct group_id: nil, members: []
children :members, identified_by: :member_id do from MyApp.Events.MbChildrenGroupMemberAdded, key: :member_id, set: [member_id: :member_id, member_name: :member_name]
# removed_with_join matches by a property on an event from a different stream, rather than # by the child's own key — the event never has to come from the group's own event source. removed_with_join MyApp.Events.MbChildrenMemberDeletedElsewhere, key: :member_id endendimport { childrenFrom, eventType, Guid, removedWithJoin } from '@cratis/chronicle';import { field } from '@cratis/fundamentals';
@eventType()export class MbChildrenRemovedFeatureActivated { featureId: Guid = Guid.empty; name = '';}
@eventType()export class MbChildrenRemovedFeatureDeactivated { featureId: Guid = Guid.empty;}
export class MbChildrenRemovedFeature { featureId: Guid = Guid.empty; name = '';}
export class MbChildrenRemovedSubscription { id: Guid = Guid.empty;
@childrenFrom(MbChildrenRemovedFeatureActivated, 'featureId', 'featureId') @removedWithJoin(MbChildrenRemovedFeatureDeactivated, 'featureId') @field(Array, { genericArguments: [MbChildrenRemovedFeature] }) features: MbChildrenRemovedFeature[] = [];}Complete Example
Section titled “Complete Example”This example shows supported root RemovedWith and child RemovedWith declarations:
// Events[EventType]public record MbRemovalFullShoppingCartCreated(string CustomerName);
[EventType]public record MbRemovalFullItemAddedToCart(Guid ItemId, string ProductName, decimal Price);
[EventType]public record MbRemovalFullItemRemovedFromCart(Guid CartId, Guid ItemId);
[EventType]public record MbRemovalFullCartCheckedOut;
[EventType]public record MbRemovalFullCartAbandoned;
// Read Models[RemovedWith<MbRemovalFullCartCheckedOut>][RemovedWith<MbRemovalFullCartAbandoned>]public record MbRemovalFullShoppingCart( [Key] Guid Id,
[SetFrom<MbRemovalFullShoppingCartCreated>(nameof(MbRemovalFullShoppingCartCreated.CustomerName))] string Customer,
[ChildrenFrom<MbRemovalFullItemAddedToCart>(key: nameof(MbRemovalFullItemAddedToCart.ItemId))] IEnumerable<MbRemovalFullCartItem> Items);
[RemovedWith<MbRemovalFullItemRemovedFromCart>( key: nameof(MbRemovalFullItemRemovedFromCart.ItemId), parentKey: nameof(MbRemovalFullItemRemovedFromCart.CartId))]public record MbRemovalFullCartItem( [Key] Guid Id,
[SetFrom<MbRemovalFullItemAddedToCart>(nameof(MbRemovalFullItemAddedToCart.ProductName))] string Product,
[SetFrom<MbRemovalFullItemAddedToCart>(nameof(MbRemovalFullItemAddedToCart.Price))] decimal Price);Kotlin does not support this workflow yet.Java does not support this workflow yet.# Eventsdefmodule MyApp.Events.MbRemovalFullShoppingCartCreated do use Chronicle.Events.EventType, id: "mb-removal-full-shopping-cart-created"
defstruct [:customer_name]end
defmodule MyApp.Events.MbRemovalFullItemAddedToCart do use Chronicle.Events.EventType, id: "mb-removal-full-item-added-to-cart"
defstruct [:item_id, :product_name, :price]end
defmodule MyApp.Events.MbRemovalFullItemRemovedFromCart do use Chronicle.Events.EventType, id: "mb-removal-full-item-removed-from-cart"
defstruct [:cart_id, :item_id]end
defmodule MyApp.Events.MbRemovalFullCartCheckedOut do use Chronicle.Events.EventType, id: "mb-removal-full-cart-checked-out"
defstruct []end
defmodule MyApp.Events.MbRemovalFullCartAbandoned do use Chronicle.Events.EventType, id: "mb-removal-full-cart-abandoned"
defstruct []end
# Read modelsdefmodule MyApp.ReadModels.MbRemovalFullCartItem do defstruct id: nil, product: nil, price: 0end
defmodule MyApp.ReadModels.MbRemovalFullShoppingCart do use Chronicle.ReadModels.ReadModel, types: [items: {:list, MyApp.ReadModels.MbRemovalFullCartItem}]
alias MyApp.Events.{ MbRemovalFullCartAbandoned, MbRemovalFullCartCheckedOut, MbRemovalFullItemAddedToCart, MbRemovalFullItemRemovedFromCart, MbRemovalFullShoppingCartCreated }
defstruct id: nil, customer: nil, items: []
from MbRemovalFullShoppingCartCreated, set: [id: :event_source_id, customer: :customer_name]
# Either event removes the whole cart. removed_with MbRemovalFullCartCheckedOut, [] removed_with MbRemovalFullCartAbandoned, []
children :items, identified_by: :id do from MbRemovalFullItemAddedToCart, key: :item_id, set: [id: :item_id, product: :product_name, price: :price]
# Removes one item; `key` identifies the item and `parent_key` the cart it belongs to. removed_with MbRemovalFullItemRemovedFromCart, key: :item_id, parent_key: :cart_id endendimport { childrenFrom, eventType, Guid, removedWith, setFrom } from '@cratis/chronicle';import { field } from '@cratis/fundamentals';
// Events@eventType()export class MbRemovalFullShoppingCartCreated { customerName = '';}
@eventType()export class MbRemovalFullItemAddedToCart { itemId: Guid = Guid.empty; productName = ''; price = 0;}
@eventType()export class MbRemovalFullItemRemovedFromCart { cartId: Guid = Guid.empty; itemId: Guid = Guid.empty;}
@eventType()export class MbRemovalFullCartCheckedOut {}
@eventType()export class MbRemovalFullCartAbandoned {}
// Read Models@removedWith(MbRemovalFullItemRemovedFromCart, 'itemId', 'cartId')export class MbRemovalFullCartItem { id: Guid = Guid.empty;
@setFrom(MbRemovalFullItemAddedToCart, 'productName') product = '';
@setFrom(MbRemovalFullItemAddedToCart, 'price') price = 0;}
@removedWith(MbRemovalFullCartCheckedOut)@removedWith(MbRemovalFullCartAbandoned)export class MbRemovalFullShoppingCart { id: Guid = Guid.empty;
@setFrom(MbRemovalFullShoppingCartCreated, 'customerName') customer = '';
@childrenFrom(MbRemovalFullItemAddedToCart, 'itemId') @field(Array, { genericArguments: [MbRemovalFullCartItem] }) items: MbRemovalFullCartItem[] = [];}Best Practices
Section titled “Best Practices”- Use class-level removal for root read models to keep the removal logic with the model definition
- Choose property vs class-level removal for children based on where the logic fits best:
- Property-level if the removal is specific to how the child is used in that parent
- Class-level if the removal logic applies universally to that child type
- Always specify keys explicitly when the default EventSourceId doesn’t apply
- Use RemovedWithJoin only for children when a removal event comes from another stream; root removal through a join is not supported
- Combine multiple removal attributes when a model can be removed by different events