Constant Keys
A constant key allows all events of a given type to accumulate into a single read model instance, regardless of which event source the events originate from. This is useful for global aggregates, system-wide counters, and singleton read models.
Defining a constant key
Section titled “Defining a constant key”Use UsingConstantKey(string value) to specify a fixed key value for a from block. The read model is a normal record or class — the constant key becomes its _id in the underlying store:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Projections;
[EventType]public record DecConstantKeyOrderPlaced(decimal Total);
public record DecConstantKeyGlobalCounter(int TotalOrders);
public class DecConstantKeyGlobalCounterProjection : IProjectionFor<DecConstantKeyGlobalCounter>{ public void Define(IProjectionBuilderFor<DecConstantKeyGlobalCounter> builder) => builder .From<DecConstantKeyOrderPlaced>(_ => _ .UsingConstantKey("global") .Count(m => m.TotalOrders));}Kotlin does not support this workflow yet.Java does not support this workflow yet.defmodule MyApp.Events.DecConstantKeyOrderPlaced do use Chronicle.Events.EventType, id: "dec-constant-key-order-placed"
defstruct [:total]end
defmodule MyApp.ReadModels.DecConstantKeyGlobalCounter do use Chronicle.ReadModels.ReadModel
defstruct total_orders: 0end
defmodule MyApp.Projections.DecConstantKeyGlobalCounterProjection do use Chronicle.Projections.Projection, model: MyApp.ReadModels.DecConstantKeyGlobalCounter
alias MyApp.Events.DecConstantKeyOrderPlaced
from DecConstantKeyOrderPlaced, key: "global", count: :total_ordersendEvery OrderPlaced event, from every event source, updates the single GlobalCounter instance with the key "global".
TypeScript note: the fluent builder’s
count()is not implemented yet (FromBuilder.tsthrows at runtime even though it type-checks). This particular example — an unconditional occurrence counter — is C#-only for now.increment()/decrement()are implemented; see the next example.
Combining with functions
Section titled “Combining with functions”Constant keys work with all counting and arithmetic functions, making them ideal for global aggregates. The TypeScript example below tracks only activeSessions via increment()/decrement() — it omits the C# example’s count()-based totalUsers, since count() isn’t implemented in TypeScript yet:
[EventType]public record DecConstantKeyUserRegistered(string Name);
[EventType]public record DecConstantKeyUserLoggedIn;
[EventType]public record DecConstantKeyUserLoggedOut;
public record DecConstantKeySiteStatistics( int TotalUsers, int ActiveSessions);
public class DecConstantKeySiteStatisticsProjection : IProjectionFor<DecConstantKeySiteStatistics>{ public void Define(IProjectionBuilderFor<DecConstantKeySiteStatistics> builder) => builder .From<DecConstantKeyUserRegistered>(_ => _ .UsingConstantKey("site") .Count(m => m.TotalUsers)) .From<DecConstantKeyUserLoggedIn>(_ => _ .UsingConstantKey("site") .Increment(m => m.ActiveSessions)) .From<DecConstantKeyUserLoggedOut>(_ => _ .UsingConstantKey("site") .Decrement(m => m.ActiveSessions));}Kotlin does not support this workflow yet.Java does not support this workflow yet.defmodule MyApp.Events.DecConstantKeyUserRegistered do use Chronicle.Events.EventType, id: "dec-constant-key-user-registered"
defstruct [:name]end
defmodule MyApp.Events.DecConstantKeyUserLoggedIn do use Chronicle.Events.EventType, id: "dec-constant-key-user-logged-in"
defstruct []end
defmodule MyApp.Events.DecConstantKeyUserLoggedOut do use Chronicle.Events.EventType, id: "dec-constant-key-user-logged-out"
defstruct []end
defmodule MyApp.ReadModels.DecConstantKeySiteStatistics do use Chronicle.ReadModels.ReadModel
defstruct total_users: 0, active_sessions: 0end
defmodule MyApp.Projections.DecConstantKeySiteStatisticsProjection do use Chronicle.Projections.Projection, model: MyApp.ReadModels.DecConstantKeySiteStatistics
alias MyApp.Events.{DecConstantKeyUserRegistered, DecConstantKeyUserLoggedIn, DecConstantKeyUserLoggedOut}
from DecConstantKeyUserRegistered, key: "site", count: :total_users
from DecConstantKeyUserLoggedIn, key: "site", add: [active_sessions: 1]
from DecConstantKeyUserLoggedOut, key: "site", subtract: [active_sessions: 1]endimport { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle';
@eventType()class DecConstantKeyUserLoggedIn {}
@eventType()class DecConstantKeyUserLoggedOut {}
class DecConstantKeySiteStatistics { activeSessions = 0;}
@projection()class DecConstantKeySiteStatisticsProjection implements IProjectionFor<DecConstantKeySiteStatistics> { define(builder: IProjectionBuilderFor<DecConstantKeySiteStatistics>): void { builder .from(DecConstantKeyUserLoggedIn, _ => _ .usingConstantKey('site') .increment(m => m.activeSessions)) .from(DecConstantKeyUserLoggedOut, _ => _ .usingConstantKey('site') .decrement(m => m.activeSessions)); }}Constant parent keys
Section titled “Constant parent keys”Use UsingConstantParentKey(string value) when working with child collections and you want all events to target the same parent read model:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Projections;
[EventType]public record DecConstantKeyUserJoined(string UserId, string UserName);
public record DecConstantKeyTeamMember(string UserId, string Name);
public record DecConstantKeyTeam(IEnumerable<DecConstantKeyTeamMember> Members);
public class DecConstantKeyTeamActivityProjection : IProjectionFor<DecConstantKeyTeam>{ public void Define(IProjectionBuilderFor<DecConstantKeyTeam> builder) => builder .Children(m => m.Members, children => children .IdentifiedBy(e => e.UserId) .From<DecConstantKeyUserJoined>(_ => _ .UsingConstantParentKey("main-team") .Set(m => m.Name).To(e => e.UserName)));}Comparison to other key strategies
Section titled “Comparison to other key strategies”| Strategy | Method | When to use |
|---|---|---|
| Event source ID | (default) | Each event stream is one instance |
| Event property | UsingKey(e => e.Property) | Property on the event identifies instance |
| Event context | UsingKeyFromContext(c => c.Property) | Event context property identifies instance |
| Composite | UsingCompositeKey<T>(...) | Multiple values together form identity |
| Constant | UsingConstantKey("value") | All events update the same instance |
Full example
Section titled “Full example”using Cratis.Chronicle.Events;using Cratis.Chronicle.Projections;
[EventType]public record DecConstantKeyPageViewed(string PageUrl);
[EventType]public record DecConstantKeyButtonClicked(string ButtonId);
[EventType]public record DecConstantKeyFormSubmitted(string FormId);
public record DecConstantKeyEngagementMetrics( int PageViews, int ButtonClicks, int FormSubmissions);
public class DecConstantKeyEngagementMetricsProjection : IProjectionFor<DecConstantKeyEngagementMetrics>{ public void Define(IProjectionBuilderFor<DecConstantKeyEngagementMetrics> builder) => builder .From<DecConstantKeyPageViewed>(_ => _ .UsingConstantKey("metrics") .Count(m => m.PageViews)) .From<DecConstantKeyButtonClicked>(_ => _ .UsingConstantKey("metrics") .Count(m => m.ButtonClicks)) .From<DecConstantKeyFormSubmitted>(_ => _ .UsingConstantKey("metrics") .Count(m => m.FormSubmissions));}Kotlin does not support this workflow yet.Java does not support this workflow yet.defmodule MyApp.Events.DecConstantKeyPageViewed do use Chronicle.Events.EventType, id: "dec-constant-key-page-viewed"
defstruct [:page_url]end
defmodule MyApp.Events.DecConstantKeyButtonClicked do use Chronicle.Events.EventType, id: "dec-constant-key-button-clicked"
defstruct [:button_id]end
defmodule MyApp.Events.DecConstantKeyFormSubmitted do use Chronicle.Events.EventType, id: "dec-constant-key-form-submitted"
defstruct [:form_id]end
defmodule MyApp.ReadModels.DecConstantKeyEngagementMetrics do use Chronicle.ReadModels.ReadModel
defstruct page_views: 0, button_clicks: 0, form_submissions: 0end
defmodule MyApp.Projections.DecConstantKeyEngagementMetricsProjection do use Chronicle.Projections.Projection, model: MyApp.ReadModels.DecConstantKeyEngagementMetrics
alias MyApp.Events.{DecConstantKeyPageViewed, DecConstantKeyButtonClicked, DecConstantKeyFormSubmitted}
from DecConstantKeyPageViewed, key: "metrics", count: :page_views
from DecConstantKeyButtonClicked, key: "metrics", count: :button_clicks
from DecConstantKeyFormSubmitted, key: "metrics", count: :form_submissionsendThis projection collects engagement events from all users and event sources into a single EngagementMetrics document with the key "metrics".