Constant Keys
A constant key fixes the read model key to a literal string value, so all events of a given type accumulate into a single read model instance regardless of which event source they come from.
FromEvent with ConstantKey
Section titled “FromEvent with ConstantKey”Use ConstantKey on the [FromEvent] attribute at the class level to route all matching events to a fixed read model instance:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Projections.ModelBound;
[EventType]public record MbConstantKeyOrderPlaced(string CustomerName, DateTimeOffset PlacedAt);
[FromEvent<MbConstantKeyOrderPlaced>(ConstantKey = "global")]public record MbConstantKeyGlobalOrderSummary( [SetFrom<MbConstantKeyOrderPlaced>(nameof(MbConstantKeyOrderPlaced.CustomerName))] string LastCustomer,
[SetFrom<MbConstantKeyOrderPlaced>(nameof(MbConstantKeyOrderPlaced.PlacedAt))] DateTimeOffset LastOrderDate);Kotlin does not support this workflow yet.Java does not support this workflow yet.defmodule MyApp.Events.ConstantKeyOrderPlaced do use Chronicle.Events.EventType, id: "constant-key-order-placed-v1"
defstruct [:customer_name, :placed_at]end
defmodule MyApp.ReadModels.GlobalOrderSummary do use Chronicle.ReadModels.ReadModel
alias MyApp.Events.ConstantKeyOrderPlaced
defstruct [:last_customer, :last_order_date]
# A literal `$value(...)` key expression routes every event to the same # instance, regardless of which event source produced it. from ConstantKeyOrderPlaced, key: "$value(global)", set: [last_customer: :customer_name, last_order_date: :placed_at]endimport { eventType, fromEvent, readModel, setFrom } from '@cratis/chronicle';
@eventType()class MbConstantKeyOrderPlaced { customerName = ''; placedAt = new Date();}
@readModel()@fromEvent(MbConstantKeyOrderPlaced, { constantKey: 'global' })class MbConstantKeyGlobalOrderSummary { @setFrom(MbConstantKeyOrderPlaced, 'customerName') lastCustomer = '';
@setFrom(MbConstantKeyOrderPlaced, 'placedAt') lastOrderDate = new Date();}Every OrderPlaced event from every event source updates the same GlobalOrderSummary instance.
Count, Increment, and Decrement with ConstantKey
Section titled “Count, Increment, and Decrement with ConstantKey”Count, Increment, and Decrement attributes also support ConstantKey for collecting events from all event sources into a single document:
using Cratis.Chronicle.Keys;using Cratis.Chronicle.Projections.ModelBound;
[EventType]public record MbConstantKeyOrderPlacedForMetrics;
[EventType]public record MbConstantKeyUserLoggedIn;
[EventType]public record MbConstantKeyUserLoggedOut;
[EventType]public record MbConstantKeyErrorOccurred;
public record MbConstantKeySystemMetrics( [Count<MbConstantKeyOrderPlacedForMetrics>(ConstantKey = "metrics")] int TotalOrders,
[Increment<MbConstantKeyUserLoggedIn>(ConstantKey = "metrics")] [Decrement<MbConstantKeyUserLoggedOut>(ConstantKey = "metrics")] int ActiveSessions,
[Count<MbConstantKeyErrorOccurred>(ConstantKey = "metrics")] int TotalErrors);Kotlin does not support this workflow yet.Java does not support this workflow yet.defmodule MyApp.Events.ConstantKeyOrderPlacedForMetrics do use Chronicle.Events.EventType, id: "constant-key-order-placed-for-metrics-v1"
defstruct []end
defmodule MyApp.Events.ConstantKeyUserLoggedIn do use Chronicle.Events.EventType, id: "constant-key-user-logged-in-v1"
defstruct []end
defmodule MyApp.Events.ConstantKeyUserLoggedOut do use Chronicle.Events.EventType, id: "constant-key-user-logged-out-v1"
defstruct []end
defmodule MyApp.Events.ConstantKeyErrorOccurred do use Chronicle.Events.EventType, id: "constant-key-error-occurred-v1"
defstruct []end
defmodule MyApp.ReadModels.SystemMetrics do use Chronicle.ReadModels.ReadModel
alias MyApp.Events.{ ConstantKeyOrderPlacedForMetrics, ConstantKeyUserLoggedIn, ConstantKeyUserLoggedOut, ConstantKeyErrorOccurred }
defstruct total_orders: 0, active_sessions: 0, total_errors: 0
from ConstantKeyOrderPlacedForMetrics, key: "$value(metrics)", count: :total_orders
from ConstantKeyUserLoggedIn, key: "$value(metrics)", add: [active_sessions: 1]
from ConstantKeyUserLoggedOut, key: "$value(metrics)", subtract: [active_sessions: 1]
from ConstantKeyErrorOccurred, key: "$value(metrics)", count: :total_errorsendimport { count, decrement, eventType, increment, readModel } from '@cratis/chronicle';
@eventType()class MbConstantKeyOrderPlacedForMetrics {}
@eventType()class MbConstantKeyUserLoggedIn {}
@eventType()class MbConstantKeyUserLoggedOut {}
@eventType()class MbConstantKeyErrorOccurred {}
@readModel()class MbConstantKeySystemMetrics { @count(MbConstantKeyOrderPlacedForMetrics, 'metrics') totalOrders = 0;
@increment(MbConstantKeyUserLoggedIn, 'metrics') @decrement(MbConstantKeyUserLoggedOut, 'metrics') activeSessions = 0;
@count(MbConstantKeyErrorOccurred, 'metrics') totalErrors = 0;}All three properties converge on the "metrics" document regardless of which user or order they come from.
Mixing event source key and constant key
Section titled “Mixing event source key and constant key”You can mix regular key-based events with constant key events on the same read model:
[EventType]public record MbConstantKeyUserRegistered;
[EventType]public record MbConstantKeyOrderPlacedGlobal;
[FromEvent<MbConstantKeyUserRegistered>]public record MbConstantKeyUserDashboard( [Key] Guid UserId,
string Name,
// A per-instance property alongside a constant-keyed one on the same read model [Count<MbConstantKeyOrderPlacedGlobal>(ConstantKey = "global-stats")] int PlatformTotalOrders);Kotlin does not support this workflow yet.Java does not support this workflow yet.defmodule MyApp.Events.ConstantKeyUserRegistered do use Chronicle.Events.EventType, id: "constant-key-user-registered-v1"
defstruct [:name]end
defmodule MyApp.Events.ConstantKeyOrderPlacedGlobal do use Chronicle.Events.EventType, id: "constant-key-order-placed-global-v1"
defstruct []end
defmodule MyApp.ReadModels.UserDashboard do use Chronicle.ReadModels.ReadModel
alias MyApp.Events.{ConstantKeyUserRegistered, ConstantKeyOrderPlacedGlobal}
defstruct [:user_id, :name, platform_total_orders: 0]
# Regular, per-instance key — one document per user. from ConstantKeyUserRegistered, set: [user_id: :event_source_id, name: :name]
# A literal key routes this counter to a single shared document instead — # "global-stats", not the per-user instance above. from ConstantKeyOrderPlacedGlobal, key: "$value(global-stats)", count: :platform_total_ordersendimport { count, eventType, fromEvent, Guid, readModel } from '@cratis/chronicle';
@eventType()class MbConstantKeyUserRegistered {}
@eventType()class MbConstantKeyOrderPlacedGlobal {}
@readModel()@fromEvent(MbConstantKeyUserRegistered)class MbConstantKeyUserDashboard { id: Guid = Guid.empty; name = '';
// A per-instance property alongside a constant-keyed one on the same read model @count(MbConstantKeyOrderPlacedGlobal, 'global-stats') platformTotalOrders = 0;}Note: When
ConstantKeyis set on a counter attribute, it affects which read model instance the counter updates — not the key of the read model the attribute belongs to. In this example,PlatformTotalOrderswould update the document with key"global-stats", not theUserDashboardinstance.
Complete example
Section titled “Complete example”// Events[EventType]public record MbConstantKeyProductPurchased(string ProductId, decimal Amount);
[EventType]public record MbConstantKeyProductReturned(string ProductId, decimal Amount);
[EventType]public record MbConstantKeyPageViewed(string PageUrl);
// Global read modelpublic record MbConstantKeyStoreMetrics( [Count<MbConstantKeyProductPurchased>(ConstantKey = "store")] int TotalPurchases,
[Count<MbConstantKeyProductReturned>(ConstantKey = "store")] int TotalReturns,
[Increment<MbConstantKeyProductPurchased>(ConstantKey = "store")] [Decrement<MbConstantKeyProductReturned>(ConstantKey = "store")] int NetTransactions,
[Count<MbConstantKeyPageViewed>(ConstantKey = "store")] int TotalPageViews);Kotlin does not support this workflow yet.Java does not support this workflow yet.defmodule MyApp.Events.ConstantKeyProductPurchased do use Chronicle.Events.EventType, id: "constant-key-product-purchased-v1"
defstruct [:product_id, :amount]end
defmodule MyApp.Events.ConstantKeyProductReturned do use Chronicle.Events.EventType, id: "constant-key-product-returned-v1"
defstruct [:product_id, :amount]end
defmodule MyApp.Events.ConstantKeyPageViewed do use Chronicle.Events.EventType, id: "constant-key-page-viewed-v1"
defstruct [:page_url]end
defmodule MyApp.ReadModels.StoreMetrics do use Chronicle.ReadModels.ReadModel
alias MyApp.Events.{ ConstantKeyProductPurchased, ConstantKeyProductReturned, ConstantKeyPageViewed }
defstruct total_purchases: 0, total_returns: 0, net_transactions: 0, total_page_views: 0
from ConstantKeyProductPurchased, key: "$value(store)", count: :total_purchases, add: [net_transactions: 1]
from ConstantKeyProductReturned, key: "$value(store)", count: :total_returns, subtract: [net_transactions: 1]
from ConstantKeyPageViewed, key: "$value(store)", count: :total_page_viewsendimport { count, decrement, eventType, increment, readModel } from '@cratis/chronicle';
// Events@eventType()class MbConstantKeyProductPurchased { productId = ''; amount = 0;}
@eventType()class MbConstantKeyProductReturned { productId = ''; amount = 0;}
@eventType()class MbConstantKeyPageViewed { pageUrl = '';}
// Global read model@readModel()class MbConstantKeyStoreMetrics { @count(MbConstantKeyProductPurchased, 'store') totalPurchases = 0;
@count(MbConstantKeyProductReturned, 'store') totalReturns = 0;
@increment(MbConstantKeyProductPurchased, 'store') @decrement(MbConstantKeyProductReturned, 'store') netTransactions = 0;
@count(MbConstantKeyPageViewed, 'store') totalPageViews = 0;}All events from all users and products accumulate into the single StoreMetrics document with key "store".