Skip to content

Projection with joins

Joins allow projections to incorporate data from events that don’t share the same event source ID. This enables building read models that combine data from different streams.

Use the Join() method to include data from events with different keys:

using Cratis.Chronicle.Projections;
public class DecJoinsUserProjection : IProjectionFor<DecJoinsUser>
{
public void Define(IProjectionBuilderFor<DecJoinsUser> builder) => builder
.AutoMap()
.From<DecJoinsUserCreated>()
.From<DecJoinsUserAssignedToGroup>(b => b
.UsingKey(e => e.UserId)
.Set(m => m.GroupId).ToEventSourceId())
.Join<DecJoinsGroupCreated>(j => j
.On(m => m.GroupId))
.Join<DecJoinsGroupRenamed>(j => j
.On(m => m.GroupId));
}

The read model includes properties populated from different event sources:

public record DecJoinsUser(
string Name,
string Email,
string? GroupId,
string? GroupName,
string? GroupDescription);

Events come from different streams but are joined based on common identifiers:

using Cratis.Chronicle.Events;
// User stream events
[EventType]
public record DecJoinsUserCreated(string Name, string Email);
[EventType]
public record DecJoinsUserAssignedToGroup(string UserId, string GroupId);
// Group stream events
[EventType]
public record DecJoinsGroupCreated(string Name, string Description);
[EventType]
public record DecJoinsGroupRenamed(string NewName);
  1. Primary events establish the read model and may set join keys
  2. Join conditions specify which property links to other streams
  3. Joined events update properties when their event source ID matches the join key
  4. Join properties are updated whenever relevant events occur

Join keys are typically set from events that establish relationships — in the example above, UserAssignedToGroup sets GroupId to the group’s own event source ID via .ToEventSourceId().

Joins match events based on their event source ID and the join property — .Join<GroupCreated>(j => j.On(m => m.GroupId)) links in group data whenever a GroupCreated/GroupRenamed event’s event source ID matches the read model’s GroupId.

A projection can join with multiple streams:

using Cratis.Chronicle.Events;
using Cratis.Chronicle.Projections;
[EventType]
public record DecJoinsMultipleEmployeeAssigned(string GroupId, string DepartmentId, string LocationId);
[EventType]
public record DecJoinsMultipleGroupCreated(string Name);
[EventType]
public record DecJoinsMultipleDepartmentCreated(string Name);
[EventType]
public record DecJoinsMultipleLocationUpdated(string Address);
public record DecJoinsMultipleEmployeeSummary(
string? GroupId,
string? GroupName,
string? DepartmentId,
string? DepartmentName,
string? LocationId,
string? LocationAddress);
public class DecJoinsMultipleEmployeeSummaryProjection : IProjectionFor<DecJoinsMultipleEmployeeSummary>
{
public void Define(IProjectionBuilderFor<DecJoinsMultipleEmployeeSummary> builder) => builder
.AutoMap()
.From<DecJoinsMultipleEmployeeAssigned>()
.Join<DecJoinsMultipleGroupCreated>(j => j.On(m => m.GroupId))
.Join<DecJoinsMultipleDepartmentCreated>(j => j.On(m => m.DepartmentId))
.Join<DecJoinsMultipleLocationUpdated>(j => j.On(m => m.LocationId));
}

Joins can also be used within child collections:

using Cratis.Chronicle.Events;
using Cratis.Chronicle.Projections;
[EventType]
public record DecJoinsChildTaskAssigned(string TaskId, string ProjectId);
[EventType]
public record DecJoinsChildProjectCreated(string Name);
public record DecJoinsChildTask(string TaskId, string ProjectId, string? ProjectName);
public record DecJoinsChildProjectBoard(IEnumerable<DecJoinsChildTask> Tasks);
public class DecJoinsChildProjectBoardProjection : IProjectionFor<DecJoinsChildProjectBoard>
{
public void Define(IProjectionBuilderFor<DecJoinsChildProjectBoard> builder) => builder
.Children(m => m.Tasks, children => children
.IdentifiedBy(e => e.TaskId)
.AutoMap()
.From<DecJoinsChildTaskAssigned>(b => b
.UsingKey(e => e.TaskId))
.Join<DecJoinsChildProjectCreated>(j => j
.On(m => m.ProjectId)));
}
  • Joins require Chronicle to track relationships between streams
  • The system automatically manages join indexes and updates
  • Consider the frequency of joined events when designing projections
  • Large numbers of joins may impact projection performance

Joins enable powerful cross-stream read models while maintaining the benefits of event sourcing and proper stream boundaries.