Removing Read Models
Model-bound projections support removing read models and child items through the RemovedWith and RemovedWithJoin attributes. These can be applied at both the class level (for root read models and child types) and on properties/parameters (for child collections).
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);Kotlin does not support this workflow yet.Java does not support this workflow yet.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, readModel, removedWith, setFrom } from '@cratis/chronicle';
@eventType()class MbRemovalAccountOpened { name = ''; balance = 0;}
@eventType()class MbRemovalAccountClosed {}
@readModel()@removedWith(MbRemovalAccountClosed)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);Kotlin does not support this workflow yet.Java does not support this workflow yet.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, readModel, removedWith, setFrom } from '@cratis/chronicle';
@eventType()class MbRemovalWithKeyAccountOpened { name = '';}
@eventType()class MbRemovalWithKeyAccountClosed { accountId: Guid = Guid.empty;}
@readModel()@removedWith(MbRemovalWithKeyAccountClosed, 'accountId')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);
[EventType]public record MbRemovalMultipleOrganizationClosed;
[RemovedWith<MbRemovalMultipleAccountClosed>][RemovedWith<MbRemovalMultipleAccountMerged>(key: nameof(MbRemovalMultipleAccountMerged.SourceAccountId))][RemovedWithJoin<MbRemovalMultipleOrganizationClosed>]public record MbRemovalMultipleAccount( [Key] Guid Id,
[SetFrom<MbRemovalMultipleAccountOpened>(nameof(MbRemovalMultipleAccountOpened.Name))] string Name);Kotlin does not support this workflow yet.Java does not support this workflow yet.Elixir does not support this workflow yet.import { eventType, Guid, readModel, removedWith, removedWithJoin, setFrom } from '@cratis/chronicle';
@eventType()class MbRemovalMultipleAccountOpened { name = '';}
@eventType()class MbRemovalMultipleAccountClosed {}
@eventType()class MbRemovalMultipleAccountMerged { sourceAccountId: Guid = Guid.empty;}
@eventType()class MbRemovalMultipleOrganizationClosed {}
@readModel()@removedWith(MbRemovalMultipleAccountClosed)@removedWith(MbRemovalMultipleAccountMerged, 'sourceAccountId')@removedWithJoin(MbRemovalMultipleOrganizationClosed)class MbRemovalMultipleAccount { id: Guid = Guid.empty;
@setFrom(MbRemovalMultipleAccountOpened, 'name') name = '';}In this example, an account can be removed by:
- An
AccountClosedevent (direct removal) - An
AccountMergedevent when it’s the source account - An
OrganizationClosedevent through a join relationship
RemovedWithJoin
Section titled “RemovedWithJoin”Use RemovedWithJoin when the removal event comes from a different stream (join relationship):
[EventType]public record MbRemovalJoinClassEmployeeHired(string Name);
[EventType]public record MbRemovalJoinClassCompanyRegistered(string Name);
[EventType]public record MbRemovalJoinClassCompanyDissolved;
[RemovedWithJoin<MbRemovalJoinClassCompanyDissolved>]public record MbRemovalJoinClassEmployee( [Key] Guid Id,
[SetFrom<MbRemovalJoinClassEmployeeHired>(nameof(MbRemovalJoinClassEmployeeHired.Name))] string Name,
[Join<MbRemovalJoinClassCompanyRegistered>(eventPropertyName: nameof(MbRemovalJoinClassCompanyRegistered.Name))] string CompanyName);Kotlin does not support this workflow yet.Java does not support this workflow yet.Elixir does not support this workflow yet.import { eventType, Guid, join, readModel, removedWithJoin, setFrom } from '@cratis/chronicle';
@eventType()class MbRemovalJoinClassEmployeeHired { name = '';}
@eventType()class MbRemovalJoinClassCompanyRegistered { name = '';}
@eventType()class MbRemovalJoinClassCompanyDissolved {}
@readModel()@removedWithJoin(MbRemovalJoinClassCompanyDissolved)class MbRemovalJoinClassEmployee { id: Guid = Guid.empty;
@setFrom(MbRemovalJoinClassEmployeeHired, 'name') name = '';
@join(MbRemovalJoinClassCompanyRegistered, undefined, 'name') companyName = '';}When the company is dissolved, all employees associated with that company are removed.
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);Kotlin does not support this workflow yet.Java does not support this workflow yet.Elixir does not support this workflow yet.import { childrenFrom, eventType, Guid, readModel, removedWith } from '@cratis/chronicle';
@eventType()class MbChildrenRemovalPropertyLineItemAdded { itemId: Guid = Guid.empty; description = '';}
@eventType()class MbChildrenRemovalPropertyLineItemRemoved { itemId: Guid = Guid.empty;}
@readModel()class MbChildrenRemovalPropertyOrder { id: Guid = Guid.empty;
@childrenFrom(MbChildrenRemovalPropertyLineItemAdded, 'itemId') @removedWith(MbChildrenRemovalPropertyLineItemRemoved, 'itemId') lines: MbChildrenRemovalPropertyOrderLine[] = [];}
class MbChildrenRemovalPropertyOrderLine { id: Guid = Guid.empty; description = '';}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 does not support this workflow yet.import { childrenFrom, eventType, Guid, readModel, removedWith } from '@cratis/chronicle';
@eventType()class MbChildrenRemovalClassLineItemAdded { itemId: Guid = Guid.empty; description = '';}
@eventType()class MbChildrenRemovalClassLineItemRemoved { orderId: Guid = Guid.empty; itemId: Guid = Guid.empty;}
@readModel()class MbChildrenRemovalClassOrder { id: Guid = Guid.empty;
@childrenFrom(MbChildrenRemovalClassLineItemAdded, 'itemId') lines: MbChildrenRemovalClassOrderLine[] = [];}
@removedWith(MbChildrenRemovalClassLineItemRemoved, 'itemId', 'orderId')class MbChildrenRemovalClassOrderLine { id: Guid = Guid.empty; description = '';}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);Kotlin does not support this workflow yet.Java does not support this workflow yet.Elixir does not support this workflow yet.import { childrenFrom, eventType, Guid, readModel, removedWithJoin } from '@cratis/chronicle';
@eventType()class MbChildrenRemovedFeatureActivated { featureId: Guid = Guid.empty; name = '';}
@eventType()class MbChildrenRemovedFeatureDeactivated { featureId: Guid = Guid.empty;}
@readModel()class MbChildrenRemovedSubscription { id: Guid = Guid.empty;
@childrenFrom(MbChildrenRemovedFeatureActivated, 'featureId', 'featureId') @removedWithJoin(MbChildrenRemovedFeatureDeactivated, 'featureId') features: MbChildrenRemovedFeature[] = [];}
class MbChildrenRemovedFeature { featureId: Guid = Guid.empty; name = '';}Complete Example
Section titled “Complete Example”Here’s a comprehensive example showing both root and child removal:
// 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.Elixir does not support this workflow yet.import { childrenFrom, eventType, Guid, readModel, removedWith, setFrom } from '@cratis/chronicle';
// Events@eventType()class MbRemovalFullShoppingCartCreated { customerName = '';}
@eventType()class MbRemovalFullItemAddedToCart { itemId: Guid = Guid.empty; productName = ''; price = 0;}
@eventType()class MbRemovalFullItemRemovedFromCart { cartId: Guid = Guid.empty; itemId: Guid = Guid.empty;}
@eventType()class MbRemovalFullCartCheckedOut {}
@eventType()class MbRemovalFullCartAbandoned {}
// Read Models@readModel()@removedWith(MbRemovalFullCartCheckedOut)@removedWith(MbRemovalFullCartAbandoned)class MbRemovalFullShoppingCart { id: Guid = Guid.empty;
@setFrom(MbRemovalFullShoppingCartCreated, 'customerName') customer = '';
@childrenFrom(MbRemovalFullItemAddedToCart, 'itemId') items: MbRemovalFullCartItem[] = [];}
@removedWith(MbRemovalFullItemRemovedFromCart, 'itemId', 'cartId')class MbRemovalFullCartItem { id: Guid = Guid.empty;
@setFrom(MbRemovalFullItemAddedToCart, 'productName') product = '';
@setFrom(MbRemovalFullItemAddedToCart, 'price') price = 0;}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 for removal events from different streams (e.g., when a parent entity in another aggregate is deleted)
- Combine multiple removal attributes when a model can be removed by different events