Variants
Some entities do not have one shape for their whole lifetime — a work item is a backlog entry until a
pull request exists for it, then it is a pull request until it merges, then it is closed. Modeling that
as a single read model with a Status property and an ever-growing set of nullable columns makes every
query filter on state before it can trust a field, and a shape meant for one stage leaks properties that
only make sense in another.
Variants let you declare several mutually exclusive read models for the same logical entity instead. Each variant is an ordinary model-bound projection with its own shape — only the properties that stage of the entity’s life actually has. Entering one variant automatically removes the entity from every other variant in the group, so at any point in time an entity exists in exactly one of them.
Declaring a group of variants
Section titled “Declaring a group of variants”Use [VariantOf<TIdentity>] on every read model that represents one stage, and [EntersOn<TEvent>] to say
which event activates that stage. TIdentity anchors the group — every variant marked with the same
identity type is mutually exclusive with every other:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Keys;using Cratis.Chronicle.Projections.ModelBound;
[EventType]public record MbVariantIssueCreated(string Title);
[EventType]public record MbVariantPullRequestCreated(string PullRequestUrl);
/// <summary>/// Anchors the logical identity shared by every variant. It does not need to be a/// read model itself, and it does not need a common CLR base type with any of the variants./// </summary>public class MbVariantWorkItem;
[VariantOf<MbVariantWorkItem>][EntersOn<MbVariantIssueCreated>]public record MbVariantBacklogItem([property: Key] Guid Id, string Title);
[VariantOf<MbVariantWorkItem>][EntersOn<MbVariantPullRequestCreated>]public record MbVariantPullRequestItem([property: Key] Guid Id, [property: SetFrom<MbVariantPullRequestCreated>] string PullRequestUrl);import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.EntersOnimport io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.projections.FromEventSourceIdimport io.cratis.chronicle.projections.SetFromimport io.cratis.chronicle.projections.VariantOfimport io.cratis.chronicle.readModels.ReadModel
@EventTypedata class MbVariantIssueCreated(val title: String)
@EventTypedata class MbVariantPullRequestCreated(val pullRequestUrl: String)
/** * Anchors the logical identity shared by every variant. It does not need to be a read model * itself, and it does not need a common supertype with any of the variants. */class MbVariantWorkItem
@ReadModel@VariantOf(MbVariantWorkItem::class, key = "id")@EntersOn(MbVariantIssueCreated::class)@FromEvent(MbVariantIssueCreated::class)data class MbVariantBacklogItem( @FromEventSourceId val id: String = "",
@SetFrom("title", MbVariantIssueCreated::class) val title: String = "")
@ReadModel@VariantOf(MbVariantWorkItem::class, key = "id")@EntersOn(MbVariantPullRequestCreated::class)@FromEvent(MbVariantPullRequestCreated::class)data class MbVariantPullRequestItem( @FromEventSourceId val id: String = "",
@SetFrom("pullRequestUrl", MbVariantPullRequestCreated::class) val pullRequestUrl: String = "")import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.EntersOn;import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.projections.FromEventSourceId;import io.cratis.chronicle.projections.SetFrom;import io.cratis.chronicle.projections.VariantOf;import io.cratis.chronicle.readModels.ReadModel;
@EventTyperecord MbVariantIssueCreated(String title) {}
@EventTyperecord MbVariantPullRequestCreated(String pullRequestUrl) {}
/** * Anchors the logical identity shared by every variant. It does not need to be a read model * itself, and it does not need a common supertype with any of the variants. */class MbVariantWorkItem {}
@ReadModel@VariantOf(identity = MbVariantWorkItem.class, key = "id")@EntersOn(eventType = MbVariantIssueCreated.class)@FromEvent(eventType = MbVariantIssueCreated.class)class MbVariantBacklogItem { @FromEventSourceId public String id = "";
@SetFrom(propertyPath = "title", eventType = MbVariantIssueCreated.class) public String title = "";}
@ReadModel@VariantOf(identity = MbVariantWorkItem.class, key = "id")@EntersOn(eventType = MbVariantPullRequestCreated.class)@FromEvent(eventType = MbVariantPullRequestCreated.class)class MbVariantPullRequestItem { @FromEventSourceId public String id = "";
@SetFrom(propertyPath = "pullRequestUrl", eventType = MbVariantPullRequestCreated.class) public String pullRequestUrl = "";}defmodule MyApp.Events.MbVariantIssueCreated do use Chronicle.Events.EventType, id: "mb-variant-issue-created-v1"
defstruct [:title]end
defmodule MyApp.Events.MbVariantPullRequestCreated do use Chronicle.Events.EventType, id: "mb-variant-pull-request-created-v1"
defstruct [:pull_request_url]end
# Anchors the logical identity shared by every variant. It does not need to be a read model# itself, and it does not need a common shape with any of the variants.defmodule MyApp.ReadModels.MbVariantWorkItem doend
defmodule MyApp.ReadModels.MbVariantBacklogItem do use Chronicle.ReadModels.ReadModel
alias MyApp.Events.MbVariantIssueCreated alias MyApp.ReadModels.MbVariantWorkItem
defstruct [:id, :title]
variant_of MbVariantWorkItem, key: :id enters_on MbVariantIssueCreated
from MbVariantIssueCreated, set: [id: :event_source_id, title: :title]end
defmodule MyApp.ReadModels.MbVariantPullRequestItem do use Chronicle.ReadModels.ReadModel
alias MyApp.Events.MbVariantPullRequestCreated alias MyApp.ReadModels.MbVariantWorkItem
defstruct [:id, :pull_request_url]
variant_of MbVariantWorkItem, key: :id enters_on MbVariantPullRequestCreated
from MbVariantPullRequestCreated, set: [id: :event_source_id, pull_request_url: :pull_request_url]endimport { entersOn, eventType, fromEvent, readModel, setFrom, variantOf } from '@cratis/chronicle';
@eventType()class MbVariantIssueCreated { title = '';}
@eventType()class MbVariantPullRequestCreated { pullRequestUrl = '';}
/** * Anchors the logical identity shared by every variant. It does not need to be a read model * itself, and it does not need a common shape with any of the variants. */class MbVariantWorkItem {}
@variantOf(MbVariantWorkItem, 'id')@entersOn(MbVariantIssueCreated)@fromEvent(MbVariantIssueCreated)@readModel()class MbVariantBacklogItem { id = '';
@setFrom(MbVariantIssueCreated, 'title') title = '';}
@variantOf(MbVariantWorkItem, 'id')@entersOn(MbVariantPullRequestCreated)@fromEvent(MbVariantPullRequestCreated)@readModel()class MbVariantPullRequestItem { id = '';
@setFrom(MbVariantPullRequestCreated, 'pullRequestUrl') pullRequestUrl = '';}WorkItem does not have to be a read model itself, and it does not need a common CLR base type with any of
the variants — its only job is to be a shared type every variant in the group points at.
When an IssueCreated event is processed, a BacklogItem is created — Title maps by convention, exactly
as it would on an ordinary [FromEvent<T>] projection. When a PullRequestCreated event is processed for
the same entity, a PullRequestItem is created and the BacklogItem for that entity is removed — no
RemovedWith attribute needed, the group takes care of it. If PullRequestCreated arrives for an entity
that was never a BacklogItem at all, PullRequestItem is still created; variants are not required to be
entered in any particular order, only to be mutually exclusive once entered.
Only the entering event can create a variant
Section titled “Only the entering event can create a variant”Everything a variant maps from besides its entering event is still declared exactly like an ordinary
projection — [SetFrom<TEvent>], [Key], and the rest all work the same way. The only difference is what
that mapping is allowed to do: it can update an already-active instance of the variant, but it can never
create one.
[EventType]public record MbVariantUpdatingPullRequestCreated(string PullRequestUrl);
[EventType]public record MbVariantUpdatingBuildCompleted(string BuildStatus);
public class MbVariantUpdatingWorkItem;
/// <summary>/// BuildStatus is mapped from MbVariantUpdatingBuildCompleted - an event that is NOT this/// variant's entering event, so it is automatically reclassified into an update-only join. It can bring/// an already-active instance up to date, but it can never create one on its own./// </summary>[VariantOf<MbVariantUpdatingWorkItem>][EntersOn<MbVariantUpdatingPullRequestCreated>]public record MbVariantUpdatingPullRequestItem( [property: Key] Guid Id, [property: SetFrom<MbVariantUpdatingPullRequestCreated>] string PullRequestUrl, [property: SetFrom<MbVariantUpdatingBuildCompleted>] string BuildStatus);import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.EntersOnimport io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.projections.FromEventSourceIdimport io.cratis.chronicle.projections.SetFromimport io.cratis.chronicle.projections.VariantOfimport io.cratis.chronicle.readModels.ReadModel
@EventTypedata class MbVariantUpdatingPullRequestCreated(val pullRequestUrl: String)
@EventTypedata class MbVariantUpdatingBuildCompleted(val buildStatus: String)
class MbVariantUpdatingWorkItem
/** * buildStatus is mapped from MbVariantUpdatingBuildCompleted - an event that is NOT this variant's * entering event, so it is automatically reclassified into an update-only join. It can bring an * already-active instance up to date, but it can never create one on its own. */@ReadModel@VariantOf(MbVariantUpdatingWorkItem::class, key = "id")@EntersOn(MbVariantUpdatingPullRequestCreated::class)@FromEvent(MbVariantUpdatingPullRequestCreated::class)@FromEvent(MbVariantUpdatingBuildCompleted::class)data class MbVariantUpdatingPullRequestItem( @FromEventSourceId val id: String = "",
@SetFrom("pullRequestUrl", MbVariantUpdatingPullRequestCreated::class) val pullRequestUrl: String = "",
@SetFrom("buildStatus", MbVariantUpdatingBuildCompleted::class) val buildStatus: String = "")import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.EntersOn;import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.projections.FromEventSourceId;import io.cratis.chronicle.projections.SetFrom;import io.cratis.chronicle.projections.VariantOf;import io.cratis.chronicle.readModels.ReadModel;
@EventTyperecord MbVariantUpdatingPullRequestCreated(String pullRequestUrl) {}
@EventTyperecord MbVariantUpdatingBuildCompleted(String buildStatus) {}
class MbVariantUpdatingWorkItem {}
/** * buildStatus is mapped from MbVariantUpdatingBuildCompleted - an event that is NOT this variant's * entering event, so it is automatically reclassified into an update-only join. It can bring an * already-active instance up to date, but it can never create one on its own. */@ReadModel@VariantOf(identity = MbVariantUpdatingWorkItem.class, key = "id")@EntersOn(eventType = MbVariantUpdatingPullRequestCreated.class)@FromEvent(eventType = MbVariantUpdatingPullRequestCreated.class)@FromEvent(eventType = MbVariantUpdatingBuildCompleted.class)class MbVariantUpdatingPullRequestItem { @FromEventSourceId public String id = "";
@SetFrom(propertyPath = "pullRequestUrl", eventType = MbVariantUpdatingPullRequestCreated.class) public String pullRequestUrl = "";
@SetFrom(propertyPath = "buildStatus", eventType = MbVariantUpdatingBuildCompleted.class) public String buildStatus = "";}defmodule MyApp.Events.MbVariantUpdatingPullRequestCreated do use Chronicle.Events.EventType, id: "mb-variant-updating-pull-request-created-v1"
defstruct [:pull_request_url]end
defmodule MyApp.Events.MbVariantUpdatingBuildCompleted do use Chronicle.Events.EventType, id: "mb-variant-updating-build-completed-v1"
defstruct [:build_status]end
defmodule MyApp.ReadModels.MbVariantUpdatingWorkItem doend
# build_status is mapped from MbVariantUpdatingBuildCompleted - an event that is NOT this# variant's entering event, so it is automatically reclassified into an update-only join. It can# bring an already-active instance up to date, but it can never create one on its own.defmodule MyApp.ReadModels.MbVariantUpdatingPullRequestItem do use Chronicle.ReadModels.ReadModel
alias MyApp.Events.{MbVariantUpdatingPullRequestCreated, MbVariantUpdatingBuildCompleted} alias MyApp.ReadModels.MbVariantUpdatingWorkItem
defstruct [:id, :pull_request_url, :build_status]
variant_of MbVariantUpdatingWorkItem, key: :id enters_on MbVariantUpdatingPullRequestCreated
from MbVariantUpdatingPullRequestCreated, set: [id: :event_source_id, pull_request_url: :pull_request_url]
from MbVariantUpdatingBuildCompleted, set: [build_status: :build_status]endimport { entersOn, eventType, fromEvent, readModel, setFrom, variantOf } from '@cratis/chronicle';
@eventType()class MbVariantUpdatingPullRequestCreated { pullRequestUrl = '';}
@eventType()class MbVariantUpdatingBuildCompleted { buildStatus = '';}
class MbVariantUpdatingWorkItem {}
/** * buildStatus is mapped from MbVariantUpdatingBuildCompleted - an event that is NOT this variant's * entering event, so it is automatically reclassified into an update-only join. It can bring an * already-active instance up to date, but it can never create one on its own. */@variantOf(MbVariantUpdatingWorkItem, 'id')@entersOn(MbVariantUpdatingPullRequestCreated)@fromEvent(MbVariantUpdatingPullRequestCreated)@fromEvent(MbVariantUpdatingBuildCompleted)@readModel()class MbVariantUpdatingPullRequestItem { id = '';
@setFrom(MbVariantUpdatingPullRequestCreated, 'pullRequestUrl') pullRequestUrl = '';
@setFrom(MbVariantUpdatingBuildCompleted, 'buildStatus') buildStatus = '';}BuildStatus is mapped from BuildCompleted. Because BuildCompleted is not the event named with
[EntersOn<T>], Chronicle reclassifies that mapping into an update-only join on the variant’s own key
before it ever reaches the projection engine — the entering event stays the only door into the variant.
Sharing handlers across variants
Section titled “Sharing handlers across variants”A mapping that every variant needs — a title every stage keeps up to date, for example — does not have to
be repeated on each variant type. Declare it once on a type marked [GlobalFor<TIdentity>] instead:
[EventType]public record MbVariantSharedIssueCreated(string Title);
[EventType]public record MbVariantSharedPullRequestCreated(string PullRequestUrl);
[EventType]public record MbVariantSharedTitleChanged(string Title);
public class MbVariantSharedWorkItem;
[VariantOf<MbVariantSharedWorkItem>][EntersOn<MbVariantSharedIssueCreated>]public record MbVariantSharedBacklogItem([property: Key] Guid Id, string Title);
[VariantOf<MbVariantSharedWorkItem>][EntersOn<MbVariantSharedPullRequestCreated>]public record MbVariantSharedPullRequestItem( [property: Key] Guid Id, string Title, [property: SetFrom<MbVariantSharedPullRequestCreated>] string PullRequestUrl);
/// <summary>/// Declares a mapping every variant of MbVariantSharedWorkItem shares. Every variant must have a/// Title member - one that does not is a declaration error, not a silently skipped mapping./// </summary>/// <param name="Title">The title every variant carrying one keeps up to date.</param>[GlobalFor<MbVariantSharedWorkItem>]public record MbVariantSharedHandlers([property: SetFrom<MbVariantSharedTitleChanged>] string Title);import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.EntersOnimport io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.projections.FromEventSourceIdimport io.cratis.chronicle.projections.GlobalForimport io.cratis.chronicle.projections.SetFromimport io.cratis.chronicle.projections.VariantOfimport io.cratis.chronicle.readModels.ReadModel
@EventTypedata class MbVariantSharedIssueCreated(val title: String)
@EventTypedata class MbVariantSharedPullRequestCreated(val pullRequestUrl: String)
@EventTypedata class MbVariantSharedTitleChanged(val title: String)
class MbVariantSharedWorkItem
@ReadModel@VariantOf(MbVariantSharedWorkItem::class, key = "id")@EntersOn(MbVariantSharedIssueCreated::class)@FromEvent(MbVariantSharedIssueCreated::class)data class MbVariantSharedBacklogItem( @FromEventSourceId val id: String = "", val title: String = "")
@ReadModel@VariantOf(MbVariantSharedWorkItem::class, key = "id")@EntersOn(MbVariantSharedPullRequestCreated::class)@FromEvent(MbVariantSharedPullRequestCreated::class)data class MbVariantSharedPullRequestItem( @FromEventSourceId val id: String = "", val title: String = "",
@SetFrom("pullRequestUrl", MbVariantSharedPullRequestCreated::class) val pullRequestUrl: String = "")
/** * Declares a mapping every variant of MbVariantSharedWorkItem shares. Every variant must have a * title member - one that does not is a declaration error, not a silently skipped mapping. */@GlobalFor(MbVariantSharedWorkItem::class)@FromEvent(MbVariantSharedTitleChanged::class)data class MbVariantSharedHandlers( @SetFrom("title", MbVariantSharedTitleChanged::class) val title: String = "")import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.EntersOn;import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.projections.FromEventSourceId;import io.cratis.chronicle.projections.GlobalFor;import io.cratis.chronicle.projections.SetFrom;import io.cratis.chronicle.projections.VariantOf;import io.cratis.chronicle.readModels.ReadModel;
@EventTyperecord MbVariantSharedIssueCreated(String title) {}
@EventTyperecord MbVariantSharedPullRequestCreated(String pullRequestUrl) {}
@EventTyperecord MbVariantSharedTitleChanged(String title) {}
class MbVariantSharedWorkItem {}
@ReadModel@VariantOf(identity = MbVariantSharedWorkItem.class, key = "id")@EntersOn(eventType = MbVariantSharedIssueCreated.class)@FromEvent(eventType = MbVariantSharedIssueCreated.class)class MbVariantSharedBacklogItem { @FromEventSourceId public String id = ""; public String title = "";}
@ReadModel@VariantOf(identity = MbVariantSharedWorkItem.class, key = "id")@EntersOn(eventType = MbVariantSharedPullRequestCreated.class)@FromEvent(eventType = MbVariantSharedPullRequestCreated.class)class MbVariantSharedPullRequestItem { @FromEventSourceId public String id = ""; public String title = "";
@SetFrom(propertyPath = "pullRequestUrl", eventType = MbVariantSharedPullRequestCreated.class) public String pullRequestUrl = "";}
/** * Declares a mapping every variant of MbVariantSharedWorkItem shares. Every variant must have a * title member - one that does not is a declaration error, not a silently skipped mapping. */@GlobalFor(identity = MbVariantSharedWorkItem.class)@FromEvent(eventType = MbVariantSharedTitleChanged.class)class MbVariantSharedHandlers { @SetFrom(propertyPath = "title", eventType = MbVariantSharedTitleChanged.class) public String title = "";}defmodule MyApp.Events.MbVariantSharedIssueCreated do use Chronicle.Events.EventType, id: "mb-variant-shared-issue-created-v1"
defstruct [:title]end
defmodule MyApp.Events.MbVariantSharedPullRequestCreated do use Chronicle.Events.EventType, id: "mb-variant-shared-pull-request-created-v1"
defstruct [:pull_request_url]end
defmodule MyApp.Events.MbVariantSharedTitleChanged do use Chronicle.Events.EventType, id: "mb-variant-shared-title-changed-v1"
defstruct [:title]end
defmodule MyApp.ReadModels.MbVariantSharedWorkItem doend
defmodule MyApp.ReadModels.MbVariantSharedBacklogItem do use Chronicle.ReadModels.ReadModel
alias MyApp.Events.MbVariantSharedIssueCreated alias MyApp.ReadModels.MbVariantSharedWorkItem
defstruct [:id, :title]
variant_of MbVariantSharedWorkItem, key: :id enters_on MbVariantSharedIssueCreated
from MbVariantSharedIssueCreated, set: [id: :event_source_id]end
defmodule MyApp.ReadModels.MbVariantSharedPullRequestItem do use Chronicle.ReadModels.ReadModel
alias MyApp.Events.MbVariantSharedPullRequestCreated alias MyApp.ReadModels.MbVariantSharedWorkItem
defstruct [:id, :title, :pull_request_url]
variant_of MbVariantSharedWorkItem, key: :id enters_on MbVariantSharedPullRequestCreated
from MbVariantSharedPullRequestCreated, set: [id: :event_source_id, pull_request_url: :pull_request_url]end
# Declares a mapping every variant of MbVariantSharedWorkItem shares. Every variant must have a# title member - one that does not is a declaration error, not a silently skipped mapping. Never# registered as a projection on its own.defmodule MyApp.Projections.MbVariantSharedHandlers do use Chronicle.Projections.GlobalHandler, identity: MyApp.ReadModels.MbVariantSharedWorkItem
alias MyApp.Events.MbVariantSharedTitleChanged
from MbVariantSharedTitleChanged, set: [title: :title]endimport { entersOn, eventType, fromEvent, globalFor, readModel, setFrom, variantOf } from '@cratis/chronicle';
@eventType()class MbVariantSharedIssueCreated { title = '';}
@eventType()class MbVariantSharedPullRequestCreated { pullRequestUrl = '';}
@eventType()class MbVariantSharedTitleChanged { title = '';}
class MbVariantSharedWorkItem {}
@variantOf(MbVariantSharedWorkItem, 'id')@entersOn(MbVariantSharedIssueCreated)@fromEvent(MbVariantSharedIssueCreated)@readModel()class MbVariantSharedBacklogItem { id = ''; title = '';}
@variantOf(MbVariantSharedWorkItem, 'id')@entersOn(MbVariantSharedPullRequestCreated)@fromEvent(MbVariantSharedPullRequestCreated)@readModel()class MbVariantSharedPullRequestItem { id = ''; title = '';
@setFrom(MbVariantSharedPullRequestCreated, 'pullRequestUrl') pullRequestUrl = '';}
/** * Declares a mapping every variant of MbVariantSharedWorkItem shares. Every variant must have a * title member - one that does not is a declaration error, not a silently skipped mapping. Never * registered as a projection on its own. */@globalFor(MbVariantSharedWorkItem)class MbVariantSharedHandlers { @setFrom(MbVariantSharedTitleChanged, 'title') title = '';}Every mapping declared on the shared handler is merged into every variant of WorkItem. Like any other
non-entering event, a shared mapping can only update an already-active variant — it can never create or
resurrect one, so it is safe to share across a group whose members enter at different times.
Every variant must have the member a shared mapping targets. A variant that does not is a declaration error, not a silently skipped mapping — this is caught when the projection is discovered, not at some unpredictable point at runtime once a shared event happens to arrive for that variant.
Complete example
Section titled “Complete example”using Cratis.Chronicle.Events;using Cratis.Chronicle.Keys;using Cratis.Chronicle.Projections.ModelBound;
[EventType]public record MbVariantFullIssueCreated(string Title);
[EventType]public record MbVariantFullPullRequestCreated(string PullRequestUrl);
[EventType]public record MbVariantFullBuildCompleted(string BuildStatus);
[EventType]public record MbVariantFullTitleChanged(string Title);
/// <summary>/// Anchors the logical identity shared by MbVariantFullBacklogItem and MbVariantFullPullRequestItem./// Deliberately not a read model itself./// </summary>public class MbVariantFullWorkItem;
/// <summary>/// The variant an entity is in before a pull request exists for it./// </summary>[VariantOf<MbVariantFullWorkItem>][EntersOn<MbVariantFullIssueCreated>]public record MbVariantFullBacklogItem([property: Key] Guid Id, string Title);
/// <summary>/// The variant an entity enters once a pull request is created for it. BuildStatus is/// mapped from MbVariantFullBuildCompleted - an event that is NOT this variant's entering event, so it/// becomes an update-only join and can never create the row on its own./// </summary>[VariantOf<MbVariantFullWorkItem>][EntersOn<MbVariantFullPullRequestCreated>]public record MbVariantFullPullRequestItem( [property: Key] Guid Id, string Title, [property: SetFrom<MbVariantFullPullRequestCreated>] string PullRequestUrl, [property: SetFrom<MbVariantFullBuildCompleted>] string BuildStatus);
/// <summary>/// Declares a mapping every variant of MbVariantFullWorkItem shares./// </summary>/// <param name="Title">The title every variant carrying one keeps up to date.</param>[GlobalFor<MbVariantFullWorkItem>]public record MbVariantFullSharedHandlers([property: SetFrom<MbVariantFullTitleChanged>] string Title);import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.EntersOnimport io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.projections.FromEventSourceIdimport io.cratis.chronicle.projections.GlobalForimport io.cratis.chronicle.projections.SetFromimport io.cratis.chronicle.projections.VariantOfimport io.cratis.chronicle.readModels.ReadModel
@EventTypedata class MbVariantFullIssueCreated(val title: String)
@EventTypedata class MbVariantFullPullRequestCreated(val pullRequestUrl: String)
@EventTypedata class MbVariantFullBuildCompleted(val buildStatus: String)
@EventTypedata class MbVariantFullTitleChanged(val title: String)
/** * Anchors the logical identity shared by MbVariantFullBacklogItem and MbVariantFullPullRequestItem. * Deliberately not a read model itself. */class MbVariantFullWorkItem
/** The variant an entity is in before a pull request exists for it. */@ReadModel@VariantOf(MbVariantFullWorkItem::class, key = "id")@EntersOn(MbVariantFullIssueCreated::class)@FromEvent(MbVariantFullIssueCreated::class)data class MbVariantFullBacklogItem( @FromEventSourceId val id: String = "", val title: String = "")
/** * The variant an entity enters once a pull request is created for it. buildStatus is mapped from * MbVariantFullBuildCompleted - an event that is NOT this variant's entering event, so it becomes * an update-only join and can never create the row on its own. */@ReadModel@VariantOf(MbVariantFullWorkItem::class, key = "id")@EntersOn(MbVariantFullPullRequestCreated::class)@FromEvent(MbVariantFullPullRequestCreated::class)@FromEvent(MbVariantFullBuildCompleted::class)data class MbVariantFullPullRequestItem( @FromEventSourceId val id: String = "", val title: String = "",
@SetFrom("pullRequestUrl", MbVariantFullPullRequestCreated::class) val pullRequestUrl: String = "",
@SetFrom("buildStatus", MbVariantFullBuildCompleted::class) val buildStatus: String = "")
/** Declares a mapping every variant of MbVariantFullWorkItem shares. */@GlobalFor(MbVariantFullWorkItem::class)@FromEvent(MbVariantFullTitleChanged::class)data class MbVariantFullSharedHandlers( @SetFrom("title", MbVariantFullTitleChanged::class) val title: String = "")import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.EntersOn;import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.projections.FromEventSourceId;import io.cratis.chronicle.projections.GlobalFor;import io.cratis.chronicle.projections.SetFrom;import io.cratis.chronicle.projections.VariantOf;import io.cratis.chronicle.readModels.ReadModel;
@EventTyperecord MbVariantFullIssueCreated(String title) {}
@EventTyperecord MbVariantFullPullRequestCreated(String pullRequestUrl) {}
@EventTyperecord MbVariantFullBuildCompleted(String buildStatus) {}
@EventTyperecord MbVariantFullTitleChanged(String title) {}
/** * Anchors the logical identity shared by MbVariantFullBacklogItem and MbVariantFullPullRequestItem. * Deliberately not a read model itself. */class MbVariantFullWorkItem {}
/** The variant an entity is in before a pull request exists for it. */@ReadModel@VariantOf(identity = MbVariantFullWorkItem.class, key = "id")@EntersOn(eventType = MbVariantFullIssueCreated.class)@FromEvent(eventType = MbVariantFullIssueCreated.class)class MbVariantFullBacklogItem { @FromEventSourceId public String id = ""; public String title = "";}
/** * The variant an entity enters once a pull request is created for it. buildStatus is mapped from * MbVariantFullBuildCompleted - an event that is NOT this variant's entering event, so it becomes * an update-only join and can never create the row on its own. */@ReadModel@VariantOf(identity = MbVariantFullWorkItem.class, key = "id")@EntersOn(eventType = MbVariantFullPullRequestCreated.class)@FromEvent(eventType = MbVariantFullPullRequestCreated.class)@FromEvent(eventType = MbVariantFullBuildCompleted.class)class MbVariantFullPullRequestItem { @FromEventSourceId public String id = ""; public String title = "";
@SetFrom(propertyPath = "pullRequestUrl", eventType = MbVariantFullPullRequestCreated.class) public String pullRequestUrl = "";
@SetFrom(propertyPath = "buildStatus", eventType = MbVariantFullBuildCompleted.class) public String buildStatus = "";}
/** Declares a mapping every variant of MbVariantFullWorkItem shares. */@GlobalFor(identity = MbVariantFullWorkItem.class)@FromEvent(eventType = MbVariantFullTitleChanged.class)class MbVariantFullSharedHandlers { @SetFrom(propertyPath = "title", eventType = MbVariantFullTitleChanged.class) public String title = "";}defmodule MyApp.Events.MbVariantFullIssueCreated do use Chronicle.Events.EventType, id: "mb-variant-full-issue-created-v1"
defstruct [:title]end
defmodule MyApp.Events.MbVariantFullPullRequestCreated do use Chronicle.Events.EventType, id: "mb-variant-full-pull-request-created-v1"
defstruct [:pull_request_url]end
defmodule MyApp.Events.MbVariantFullBuildCompleted do use Chronicle.Events.EventType, id: "mb-variant-full-build-completed-v1"
defstruct [:build_status]end
defmodule MyApp.Events.MbVariantFullTitleChanged do use Chronicle.Events.EventType, id: "mb-variant-full-title-changed-v1"
defstruct [:title]end
# Anchors the logical identity shared by MbVariantFullBacklogItem and# MbVariantFullPullRequestItem. Deliberately not a read model itself.defmodule MyApp.ReadModels.MbVariantFullWorkItem doend
# The variant an entity is in before a pull request exists for it.defmodule MyApp.ReadModels.MbVariantFullBacklogItem do use Chronicle.ReadModels.ReadModel
alias MyApp.Events.MbVariantFullIssueCreated alias MyApp.ReadModels.MbVariantFullWorkItem
defstruct [:id, :title]
variant_of MbVariantFullWorkItem, key: :id enters_on MbVariantFullIssueCreated
from MbVariantFullIssueCreated, set: [id: :event_source_id]end
# The variant an entity enters once a pull request is created for it. build_status is mapped# from MbVariantFullBuildCompleted - an event that is NOT this variant's entering event, so it# becomes an update-only join and can never create the row on its own.defmodule MyApp.ReadModels.MbVariantFullPullRequestItem do use Chronicle.ReadModels.ReadModel
alias MyApp.Events.{MbVariantFullPullRequestCreated, MbVariantFullBuildCompleted} alias MyApp.ReadModels.MbVariantFullWorkItem
defstruct [:id, :title, :pull_request_url, :build_status]
variant_of MbVariantFullWorkItem, key: :id enters_on MbVariantFullPullRequestCreated
from MbVariantFullPullRequestCreated, set: [id: :event_source_id, pull_request_url: :pull_request_url]
from MbVariantFullBuildCompleted, set: [build_status: :build_status]end
# Declares a mapping every variant of MbVariantFullWorkItem shares.defmodule MyApp.Projections.MbVariantFullSharedHandlers do use Chronicle.Projections.GlobalHandler, identity: MyApp.ReadModels.MbVariantFullWorkItem
alias MyApp.Events.MbVariantFullTitleChanged
from MbVariantFullTitleChanged, set: [title: :title]endimport { entersOn, eventType, fromEvent, globalFor, readModel, setFrom, variantOf } from '@cratis/chronicle';
@eventType()class MbVariantFullIssueCreated { title = '';}
@eventType()class MbVariantFullPullRequestCreated { pullRequestUrl = '';}
@eventType()class MbVariantFullBuildCompleted { buildStatus = '';}
@eventType()class MbVariantFullTitleChanged { title = '';}
/** * Anchors the logical identity shared by MbVariantFullBacklogItem and MbVariantFullPullRequestItem. * Deliberately not a read model itself. */class MbVariantFullWorkItem {}
/** The variant an entity is in before a pull request exists for it. */@variantOf(MbVariantFullWorkItem, 'id')@entersOn(MbVariantFullIssueCreated)@fromEvent(MbVariantFullIssueCreated)@readModel()class MbVariantFullBacklogItem { id = ''; title = '';}
/** * The variant an entity enters once a pull request is created for it. buildStatus is mapped from * MbVariantFullBuildCompleted - an event that is NOT this variant's entering event, so it becomes * an update-only join and can never create the row on its own. */@variantOf(MbVariantFullWorkItem, 'id')@entersOn(MbVariantFullPullRequestCreated)@fromEvent(MbVariantFullPullRequestCreated)@fromEvent(MbVariantFullBuildCompleted)@readModel()class MbVariantFullPullRequestItem { id = ''; title = '';
@setFrom(MbVariantFullPullRequestCreated, 'pullRequestUrl') pullRequestUrl = '';
@setFrom(MbVariantFullBuildCompleted, 'buildStatus') buildStatus = '';}
/** Declares a mapping every variant of MbVariantFullWorkItem shares. */@globalFor(MbVariantFullWorkItem)class MbVariantFullSharedHandlers { @setFrom(MbVariantFullTitleChanged, 'title') title = '';}Best practices
Section titled “Best practices”- Pick an identity type that means something on its own —
WorkItemin the examples above, not a marker interface with no purpose beyond grouping. It is the type every variant, and every shared handler, points back to. - Give every variant only the properties that stage of the entity actually has. A shared property
that every stage needs belongs on a
[GlobalFor<T>]handler instead of being copy-pasted onto each variant. - Reach for variants when the shapes genuinely diverge. A single read model with a
Statusproperty is still the right choice when every stage shares almost all of its properties and differs only in a flag or two — see Choosing a read-model style. - Remember each variant is its own collection. There is no built-in query that spans a whole group — a caller that needs “this entity, whichever stage it’s currently in” queries each variant explicitly (or by its shared key) rather than assuming a single combined collection.