Skip to content

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.

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

View C# snippet source on GitHub

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

View C# snippet source on GitHub

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

View C# snippet source on GitHub

In this example, an account can be removed by an AccountClosed event or an AccountMerged event when it is the source account.

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.

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

View C# snippet source on GitHub

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

View C# snippet source on GitHub

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

View C# snippet source on GitHub

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

View C# snippet source on GitHub

  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 only for children when a removal event comes from another stream; root removal through a join is not supported
  5. Combine multiple removal attributes when a model can be removed by different events