Skip to content

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).

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);

When an AccountClosed event occurs, the corresponding Account read model is removed from the store.

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);

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);

In this example, an account can be removed by:

  • An AccountClosed event (direct removal)
  • An AccountMerged event when it’s the source account
  • An OrganizationClosed event through a join relationship

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);

When the company is dissolved, all employees associated with that company are removed.

Children can be removed in two ways:

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);

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);

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.

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)

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);

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);
  1. Use class-level removal for root read models to keep the removal logic with the model definition
  2. 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
  3. Always specify keys explicitly when the default EventSourceId doesn’t apply
  4. Use RemovedWithJoin for removal events from different streams (e.g., when a parent entity in another aggregate is deleted)
  5. Combine multiple removal attributes when a model can be removed by different events