Skip to content

Projection with RemoveWithJoin

The RemovedWithJoin<>() method allows you to remove child items from collections when events from other streams indicate that related data should be removed. This is particularly useful in scenarios where the removal event occurs on a different stream than the one that originally added the child.

Understanding RemoveWithJoin vs RemovedWith

Section titled “Understanding RemoveWithJoin vs RemovedWith”
  • RemovedWith<>(): Removes children based on events from the same stream
  • RemovedWithJoin<>(): Removes children based on events from different streams (joins)

Use RemovedWithJoin<>() in child projections to specify which events should trigger child removal:

using Cratis.Chronicle.Events;
using Cratis.Chronicle.Projections;
[EventType]
public record DecRemoveWithJoinBasicUserCreated(string Name);
[EventType]
public record DecRemoveWithJoinBasicUserAddedToGroup(string UserId, string GroupId);
[EventType]
public record DecRemoveWithJoinBasicGroupCreated(string Name);
[EventType]
public record DecRemoveWithJoinBasicGroupDeleted;
public record DecRemoveWithJoinBasicUserGroup(string GroupId, string Name, DateTimeOffset JoinedAt);
public record DecRemoveWithJoinBasicUser(string Name, IEnumerable<DecRemoveWithJoinBasicUserGroup> Groups);
public class DecRemoveWithJoinBasicUserProjection : IProjectionFor<DecRemoveWithJoinBasicUser>
{
public void Define(IProjectionBuilderFor<DecRemoveWithJoinBasicUser> builder) => builder
.AutoMap()
.From<DecRemoveWithJoinBasicUserCreated>()
.Children(m => m.Groups, children => children
.IdentifiedBy(e => e.GroupId)
.AutoMap()
.From<DecRemoveWithJoinBasicUserAddedToGroup>(_ => _
.UsingParentKey(e => e.UserId)
.Set(m => m.JoinedAt).ToEventContextProperty(c => c.Occurred))
.Join<DecRemoveWithJoinBasicGroupCreated>(_ => _
.On(m => m.GroupId))
.RemovedWithJoin<DecRemoveWithJoinBasicGroupDeleted>());
}

In this example:

  • When a UserAddedToGroup event occurs, a group is added to the user’s collection
  • UsingParentKey(e => e.UserId) extracts the parent (user) identifier from the event content
  • When a GroupDeleted event occurs anywhere in the system, that group is removed from all users
  • The removal is based on the group ID that was used to join the data

Note: If you don’t specify UsingParentKey(), the framework uses the EventSourceId as the parent identifier by default. Use UsingParentKey() when the parent identifier is a property in the event content rather than the EventSourceId.

When using RemovedWithJoin<>():

  1. Event occurs: The specified event type (e.g., GroupDeleted) is processed
  2. Key extraction: The system extracts the key from the event source ID or specified key expression
  3. Child lookup: All projections with children joined on that key are found
  4. Removal: The matching child items are removed from all affected collections

You can specify which property to use as the key for removal:

using Cratis.Chronicle.Events;
using Cratis.Chronicle.Projections;
[EventType]
public record DecRemoveWithJoinExplicitEmployeeHired(string Name);
[EventType]
public record DecRemoveWithJoinExplicitEmployeeAssignedToProject(string EmployeeId, string ProjectId);
[EventType]
public record DecRemoveWithJoinExplicitProjectCreated(string Name);
[EventType]
public record DecRemoveWithJoinExplicitProjectCancelled(string ProjectId);
public record DecRemoveWithJoinExplicitEmployeeProject(string ProjectId, string Name, DateTimeOffset AssignedAt);
public record DecRemoveWithJoinExplicitEmployee(string Name, IEnumerable<DecRemoveWithJoinExplicitEmployeeProject> Projects);
public class DecRemoveWithJoinExplicitEmployeeProjection : IProjectionFor<DecRemoveWithJoinExplicitEmployee>
{
public void Define(IProjectionBuilderFor<DecRemoveWithJoinExplicitEmployee> builder) => builder
.AutoMap()
.From<DecRemoveWithJoinExplicitEmployeeHired>()
.Children(m => m.Projects, children => children
.IdentifiedBy(e => e.ProjectId)
.AutoMap()
.From<DecRemoveWithJoinExplicitEmployeeAssignedToProject>(_ => _
.UsingParentKey(e => e.EmployeeId)
.UsingKey(e => e.ProjectId)
.Set(m => m.AssignedAt).ToEventContextProperty(c => c.Occurred))
.Join<DecRemoveWithJoinExplicitProjectCreated>(_ => _
.On(m => m.ProjectId))
.RemovedWithJoin<DecRemoveWithJoinExplicitProjectCancelled>(_ => _
.UsingKey(e => e.ProjectId)));
}

Consider a system where users can be members of groups, and groups can be deleted:

public class DecRemoveWithJoinGroupMembershipProjection : IProjectionFor<DecRemoveWithJoinUserProfile>
{
public void Define(IProjectionBuilderFor<DecRemoveWithJoinUserProfile> builder) => builder
.AutoMap()
.From<DecRemoveWithJoinUserRegistered>(_ => _
.Set(m => m.UserId).ToEventSourceId()
.Set(m => m.RegisteredAt).ToEventContextProperty(c => c.Occurred))
.Children(m => m.Memberships, children => children
.IdentifiedBy(e => e.GroupId)
.AutoMap()
.From<DecRemoveWithJoinUserJoinedGroup>(_ => _
.UsingParentKey(e => e.UserId)
.UsingKey(e => e.GroupId)
.Set(m => m.JoinedAt).ToEventContextProperty(c => c.Occurred))
.Join<DecRemoveWithJoinGroupCreated>(_ => _
.On(m => m.GroupId))
.RemovedWith<DecRemoveWithJoinUserLeftGroup>(_ => _
.UsingParentKey(e => e.UserId)
.UsingKey(e => e.GroupId))
.RemovedWithJoin<DecRemoveWithJoinGroupDisbanded>());
}

In this example:

  • RemovedWith<UserLeftGroup>: Removes membership when a user explicitly leaves a group
  • RemovedWithJoin<GroupDisbanded>: Removes membership from all users when a group is disbanded
public class DecRemoveWithJoinDeveloperProjectsProjection : IProjectionFor<DecRemoveWithJoinDeveloperProfile>
{
public void Define(IProjectionBuilderFor<DecRemoveWithJoinDeveloperProfile> builder) => builder
.AutoMap()
.From<DecRemoveWithJoinDeveloperOnboarded>(_ => _
.Set(m => m.DeveloperId).ToEventSourceId()
.Set(m => m.OnboardedAt).ToEventContextProperty(c => c.Occurred))
.Children(m => m.CurrentProjects, children => children
.IdentifiedBy(e => e.ProjectId)
.AutoMap()
.From<DecRemoveWithJoinDeveloperAssignedToProject>(_ => _
.UsingParentKey(e => e.DeveloperId)
.UsingKey(e => e.ProjectId)
.Set(m => m.AssignedAt).ToEventContextProperty(c => c.Occurred))
.Join<DecRemoveWithJoinProjectInitiated>(_ => _
.On(m => m.ProjectId))
.RemovedWith<DecRemoveWithJoinDeveloperUnassignedFromProject>(_ => _
.UsingParentKey(e => e.DeveloperId)
.UsingKey(e => e.ProjectId))
.RemovedWithJoin<DecRemoveWithJoinProjectCancelled>()
.RemovedWithJoin<DecRemoveWithJoinProjectCompleted>());
}

This handles three removal scenarios:

  1. Individual unassignment: DeveloperUnassignedFromProject removes one developer from one project
  2. Project cancellation: ProjectCancelled removes the project from all developers
  3. Project completion: ProjectCompleted removes the project from all developers
public record DecRemoveWithJoinUserProfile(
string UserId,
string Username,
string Email,
DateTimeOffset RegisteredAt,
IEnumerable<DecRemoveWithJoinGroupMembership> Memberships);
public record DecRemoveWithJoinGroupMembership(
string GroupId,
string GroupName,
string GroupType,
DateTimeOffset JoinedAt,
string Role);
public record DecRemoveWithJoinDeveloperProfile(
string DeveloperId,
string Name,
IEnumerable<string> Skills,
DateTimeOffset OnboardedAt,
IEnumerable<DecRemoveWithJoinProjectAssignment> CurrentProjects);
public record DecRemoveWithJoinProjectAssignment(
string ProjectId,
string ProjectName,
string Priority,
DateTimeOffset Deadline,
DateTimeOffset AssignedAt,
string Role,
int Allocation);
using Cratis.Chronicle.Events;
[EventType]
public record DecRemoveWithJoinUserRegistered(string Username, string Email);
[EventType]
public record DecRemoveWithJoinUserJoinedGroup(string UserId, string GroupId, string Role);
[EventType]
public record DecRemoveWithJoinUserLeftGroup(string UserId, string GroupId);
[EventType]
public record DecRemoveWithJoinGroupCreated(string GroupName, string GroupType);
[EventType]
public record DecRemoveWithJoinGroupDisbanded;
[EventType]
public record DecRemoveWithJoinDeveloperOnboarded(string Name, IEnumerable<string> Skills);
[EventType]
public record DecRemoveWithJoinDeveloperAssignedToProject(string DeveloperId, string ProjectId, string Role, int Allocation);
[EventType]
public record DecRemoveWithJoinDeveloperUnassignedFromProject(string DeveloperId, string ProjectId);
[EventType]
public record DecRemoveWithJoinProjectInitiated(string ProjectName, string Priority, DateTimeOffset Deadline);
[EventType]
public record DecRemoveWithJoinProjectCancelled;
[EventType]
public record DecRemoveWithJoinProjectCompleted;

RemovedWithJoin<>() is ideal when:

  • Events from one stream should trigger cleanup in projections of other stream
  • You need to maintain referential integrity across stream boundaries
  • Deletion or deactivation events should cascade to related read models

Often you’ll use both removal methods together:

  • RemovedWith<>() for explicit removals within the same stream
  • RemovedWithJoin<>() for cascade removals from related streams

Be aware that RemovedWithJoin<>() events might be processed before all related Join<>() events have completed, so ensure your system handles partial data gracefully.

The RemovedWithJoin<>() method provides powerful cross-stream cleanup capabilities, ensuring that your read models stay consistent when related data is removed from other parts of your system.