Basic Property Mapping
Model-bound projections map event data to read model properties close to the read model definition. Use them when each event property maps directly to a read model property, when a property should accumulate numeric values, or when event metadata belongs in the read model.
The exact syntax belongs to each client. The projection definition sent to Chronicle still has the same shape: event type, read model key, and property mappings.
Set A Property From An Event
Section titled “Set A Property From An Event”Set mappings copy a value from an event into the read model. This is the most common model-bound operation.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Keys;using Cratis.Chronicle.Projections.ModelBound;
[EventType]public record UserRegisteredForContact(string Name, string Email);
public record UserContact( [Key] Guid Id,
[SetFrom<UserRegisteredForContact>(nameof(UserRegisteredForContact.Email))] string Email,
[SetFrom<UserRegisteredForContact>(nameof(UserRegisteredForContact.Name))] string Name);import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.projections.SetFromimport io.cratis.chronicle.readModels.ReadModel
@EventType(id = "user-registered-for-contact")data class UserRegisteredForContact( val name: String, val email: String)
@ReadModel@FromEvent(UserRegisteredForContact::class)data class UserContact( @SetFrom("email", UserRegisteredForContact::class) val email: String = "",
@SetFrom("name", UserRegisteredForContact::class) val name: String = "")import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.projections.SetFrom;import io.cratis.chronicle.readModels.ReadModel;
@EventType(id = "user-registered-for-contact")record UserRegisteredForContact(String name, String email) {}
@ReadModel@FromEvent(eventType = UserRegisteredForContact.class)class UserContact { @SetFrom(propertyPath = "email", eventType = UserRegisteredForContact.class) public String email = "";
@SetFrom(propertyPath = "name", eventType = UserRegisteredForContact.class) public String name = "";}defmodule MyApp.Events.UserRegisteredForContact do use Chronicle.Events.EventType, id: "user-registered-for-contact-v1"
defstruct [:name, :email]end
defmodule MyApp.ReadModels.UserContact do use Chronicle.ReadModels.ReadModel
alias MyApp.Events.UserRegisteredForContact
defstruct [:id, :name, :email]
from UserRegisteredForContact, set: [ id: :event_source_id, email: :email, name: :name ]endimport { eventType, fromEvent, readModel, setFrom } from '@cratis/chronicle';
@eventType()class UserRegisteredForContact { constructor(readonly name: string, readonly email: string) {}}
@readModel()@fromEvent(UserRegisteredForContact)class UserContact { @setFrom(UserRegisteredForContact, 'email') email = '';
@setFrom(UserRegisteredForContact, 'name') name = '';}When the event property and read model property have the same name, the client can use its convention-based form instead of repeating the property name.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Keys;using Cratis.Chronicle.Projections.ModelBound;
[EventType]public record UserRegisteredForProfile(string Name, string Email);
[FromEvent<UserRegisteredForProfile>]public record UserProfile( [Key] Guid Id, string Name, string Email);import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.projections.SetFromimport io.cratis.chronicle.readModels.ReadModel
@EventType(id = "user-registered-for-profile")data class UserRegisteredForProfile( val name: String, val email: String)
@ReadModel@FromEvent(UserRegisteredForProfile::class)data class UserProfile( @SetFrom val name: String = "",
@SetFrom val email: String = "")import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.projections.SetFrom;import io.cratis.chronicle.readModels.ReadModel;
@EventType(id = "user-registered-for-profile")record UserRegisteredForProfile(String name, String email) {}
@ReadModel@FromEvent(eventType = UserRegisteredForProfile.class)class UserProfile { @SetFrom public String name = "";
@SetFrom public String email = "";}Elixir does not support this workflow yet.import { eventType, fromEvent, readModel, setFrom } from '@cratis/chronicle';
@eventType()class UserRegisteredForProfile { constructor(readonly name: string, readonly email: string) {}}
@readModel()@fromEvent(UserRegisteredForProfile)class UserProfile { @setFrom(UserRegisteredForProfile) name = '';
@setFrom(UserRegisteredForProfile) email = '';}Use multiple set mappings when different events can update the same read model property.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Keys;using Cratis.Chronicle.Projections.ModelBound;
[EventType]public record AccountOpenedForRename(string AccountName);
[EventType]public record AccountRenamedForRename(string NewName);
public record RenameableAccount( [Key] Guid Id,
[SetFrom<AccountOpenedForRename>(nameof(AccountOpenedForRename.AccountName))] [SetFrom<AccountRenamedForRename>(nameof(AccountRenamedForRename.NewName))] string Name);import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.projections.SetFromimport io.cratis.chronicle.readModels.ReadModel
@EventType(id = "account-opened-for-rename")data class AccountOpenedForRename( val accountName: String)
@EventType(id = "account-renamed-for-rename")data class AccountRenamedForRename( val newName: String)
@ReadModel@FromEvent(AccountOpenedForRename::class)@FromEvent(AccountRenamedForRename::class)data class RenameableAccount( @SetFrom("accountName", AccountOpenedForRename::class) @SetFrom("newName", AccountRenamedForRename::class) val name: String = "")import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.projections.SetFrom;import io.cratis.chronicle.readModels.ReadModel;
@EventType(id = "account-opened-for-rename")record AccountOpenedForRename(String accountName) {}
@EventType(id = "account-renamed-for-rename")record AccountRenamedForRename(String newName) {}
@ReadModel@FromEvent(eventType = AccountOpenedForRename.class)@FromEvent(eventType = AccountRenamedForRename.class)class RenameableAccount { @SetFrom(propertyPath = "accountName", eventType = AccountOpenedForRename.class) @SetFrom(propertyPath = "newName", eventType = AccountRenamedForRename.class) public String name = "";}defmodule MyApp.Events.AccountOpenedForRename do use Chronicle.Events.EventType, id: "account-opened-for-rename-v1"
defstruct [:account_name]end
defmodule MyApp.Events.AccountRenamedForRename do use Chronicle.Events.EventType, id: "account-renamed-for-rename-v1"
defstruct [:new_name]end
defmodule MyApp.ReadModels.RenameableAccount do use Chronicle.ReadModels.ReadModel
alias MyApp.Events.{AccountOpenedForRename, AccountRenamedForRename}
defstruct [:id, :name]
from AccountOpenedForRename, set: [ id: :event_source_id, name: :account_name ]
from AccountRenamedForRename, set: [name: :new_name]endimport { eventType, fromEvent, readModel, setFrom } from '@cratis/chronicle';
@eventType()class AccountOpenedForRename { constructor(readonly accountName: string) {}}
@eventType()class AccountRenamedForRename { constructor(readonly newName: string) {}}
@readModel()@fromEvent(AccountOpenedForRename)@fromEvent(AccountRenamedForRename)class RenameableAccount { @setFrom(AccountOpenedForRename, 'accountName') @setFrom(AccountRenamedForRename, 'newName') name = '';}Add And Subtract Values
Section titled “Add And Subtract Values”Add mappings increase a numeric read model property by a value from an event. They are useful for balances, totals, counters, and other accumulated values.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Keys;using Cratis.Chronicle.Projections.ModelBound;
[EventType]public record AccountOpenedForDeposits(decimal InitialBalance);
[EventType]public record DepositMadeForBalance(decimal Amount);
public record DepositAccount( [Key] Guid Id,
[SetFrom<AccountOpenedForDeposits>(nameof(AccountOpenedForDeposits.InitialBalance))] [AddFrom<DepositMadeForBalance>(nameof(DepositMadeForBalance.Amount))] decimal Balance);Kotlin does not support this workflow yet.Java does not support this workflow yet.defmodule MyApp.Events.AccountOpenedForDeposits do use Chronicle.Events.EventType, id: "account-opened-for-deposits-v1"
defstruct [:initial_balance]end
defmodule MyApp.Events.DepositMadeForBalance do use Chronicle.Events.EventType, id: "deposit-made-for-balance-v1"
defstruct [:amount]end
defmodule MyApp.ReadModels.DepositAccount do use Chronicle.ReadModels.ReadModel
alias MyApp.Events.{AccountOpenedForDeposits, DepositMadeForBalance}
defstruct [:id, balance: 0]
from AccountOpenedForDeposits, set: [ id: :event_source_id, balance: :initial_balance ]
from DepositMadeForBalance, add: [balance: :amount]endimport { addFrom, eventType, fromEvent, readModel, setFrom } from '@cratis/chronicle';
@eventType()class AccountOpenedForDeposits { constructor(readonly initialBalance: number) {}}
@eventType()class DepositMadeForBalance { constructor(readonly amount: number) {}}
@readModel()@fromEvent(AccountOpenedForDeposits)@fromEvent(DepositMadeForBalance)class DepositAccount { @setFrom(AccountOpenedForDeposits, 'initialBalance') @addFrom(DepositMadeForBalance, 'amount') balance = 0;}Subtract mappings decrease a numeric read model property by a value from an event. Combine add and subtract mappings when the read model should track a net value.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Keys;using Cratis.Chronicle.Projections.ModelBound;
[EventType]public record BalanceAccountOpened(decimal InitialBalance);
[EventType]public record BalanceDepositMade(decimal Amount);
[EventType]public record BalanceWithdrawalMade(decimal Amount);
public record BalanceAccount( [Key] Guid Id,
[SetFrom<BalanceAccountOpened>(nameof(BalanceAccountOpened.InitialBalance))] [AddFrom<BalanceDepositMade>(nameof(BalanceDepositMade.Amount))] [SubtractFrom<BalanceWithdrawalMade>(nameof(BalanceWithdrawalMade.Amount))] decimal Balance);Kotlin does not support this workflow yet.Java does not support this workflow yet.defmodule MyApp.Events.BalanceAccountOpened do use Chronicle.Events.EventType, id: "balance-account-opened-v1"
defstruct [:initial_balance]end
defmodule MyApp.Events.BalanceDepositMade do use Chronicle.Events.EventType, id: "balance-deposit-made-v1"
defstruct [:amount]end
defmodule MyApp.Events.BalanceWithdrawalMade do use Chronicle.Events.EventType, id: "balance-withdrawal-made-v1"
defstruct [:amount]end
defmodule MyApp.ReadModels.BalanceAccount do use Chronicle.ReadModels.ReadModel
alias MyApp.Events.{BalanceAccountOpened, BalanceDepositMade, BalanceWithdrawalMade}
defstruct [:id, balance: 0]
from BalanceAccountOpened, set: [ id: :event_source_id, balance: :initial_balance ]
from BalanceDepositMade, add: [balance: :amount]
from BalanceWithdrawalMade, subtract: [balance: :amount]endimport { addFrom, eventType, fromEvent, readModel, setFrom, subtractFrom } from '@cratis/chronicle';
@eventType()class BalanceAccountOpened { constructor(readonly initialBalance: number) {}}
@eventType()class BalanceDepositMade { constructor(readonly amount: number) {}}
@eventType()class BalanceWithdrawalMade { constructor(readonly amount: number) {}}
@readModel()@fromEvent(BalanceAccountOpened)@fromEvent(BalanceDepositMade)@fromEvent(BalanceWithdrawalMade)class BalanceAccount { @setFrom(BalanceAccountOpened, 'initialBalance') @addFrom(BalanceDepositMade, 'amount') @subtractFrom(BalanceWithdrawalMade, 'amount') balance = 0;}The event flow for a balance-style read model is:
- The opening event sets the initial balance.
- Deposit events add to the balance.
- Withdrawal events subtract from the balance.
- Rename or profile events update descriptive properties without changing the balance.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Keys;using Cratis.Chronicle.Projections.ModelBound;
[EventType]public record BankAccountOpened(string AccountName, decimal InitialBalance);
[EventType]public record BankAccountRenamed(string NewName);
[EventType]public record FundsDeposited(decimal Amount);
[EventType]public record FundsWithdrawn(decimal Amount);
public record BankAccount( [Key] Guid Id,
[SetFrom<BankAccountOpened>(nameof(BankAccountOpened.AccountName))] [SetFrom<BankAccountRenamed>(nameof(BankAccountRenamed.NewName))] string Name,
[SetFrom<BankAccountOpened>(nameof(BankAccountOpened.InitialBalance))] [AddFrom<FundsDeposited>(nameof(FundsDeposited.Amount))] [SubtractFrom<FundsWithdrawn>(nameof(FundsWithdrawn.Amount))] decimal Balance);Kotlin does not support this workflow yet.Java does not support this workflow yet.defmodule MyApp.Events.BankAccountOpened do use Chronicle.Events.EventType, id: "bank-account-opened-v1"
defstruct [:account_name, :initial_balance]end
defmodule MyApp.Events.BankAccountRenamed do use Chronicle.Events.EventType, id: "bank-account-renamed-v1"
defstruct [:new_name]end
defmodule MyApp.Events.FundsDeposited do use Chronicle.Events.EventType, id: "funds-deposited-v1"
defstruct [:amount]end
defmodule MyApp.Events.FundsWithdrawn do use Chronicle.Events.EventType, id: "funds-withdrawn-v1"
defstruct [:amount]end
defmodule MyApp.ReadModels.BankAccount do use Chronicle.ReadModels.ReadModel
alias MyApp.Events.{BankAccountOpened, BankAccountRenamed, FundsDeposited, FundsWithdrawn}
defstruct [:id, :name, balance: 0]
from BankAccountOpened, set: [ id: :event_source_id, name: :account_name, balance: :initial_balance ]
from BankAccountRenamed, set: [name: :new_name]
from FundsDeposited, add: [balance: :amount]
from FundsWithdrawn, subtract: [balance: :amount]endimport { addFrom, eventType, fromEvent, readModel, setFrom, subtractFrom } from '@cratis/chronicle';
@eventType()class BankAccountOpened { constructor(readonly accountName: string, readonly initialBalance: number) {}}
@eventType()class BankAccountRenamed { constructor(readonly newName: string) {}}
@eventType()class FundsDeposited { constructor(readonly amount: number) {}}
@eventType()class FundsWithdrawn { constructor(readonly amount: number) {}}
@readModel()@fromEvent(BankAccountOpened)@fromEvent(BankAccountRenamed)@fromEvent(FundsDeposited)@fromEvent(FundsWithdrawn)class BankAccount { @setFrom(BankAccountOpened, 'accountName') @setFrom(BankAccountRenamed, 'newName') name = '';
@setFrom(BankAccountOpened, 'initialBalance') @addFrom(FundsDeposited, 'amount') @subtractFrom(FundsWithdrawn, 'amount') balance = 0;}Set A Property From Event Context
Section titled “Set A Property From Event Context”Event context contains metadata such as when the event occurred, which event source it belongs to, sequence information, and correlation metadata. Map context fields when the read model needs audit or lifecycle information.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Keys;using Cratis.Chronicle.Projections.ModelBound;
[EventType]public record OrderPlacedForAudit(string CustomerName);
public record AuditedOrder( [Key] Guid Id,
[SetFrom<OrderPlacedForAudit>(nameof(OrderPlacedForAudit.CustomerName))] string CustomerName,
[SetFromContext<OrderPlacedForAudit>(nameof(EventContext.Occurred))] DateTimeOffset OrderedAt);Kotlin does not support this workflow yet.Java does not support this workflow yet.defmodule MyApp.Events.OrderPlacedForAudit do use Chronicle.Events.EventType, id: "order-placed-for-audit-v1"
defstruct [:customer_name]end
defmodule MyApp.ReadModels.AuditedOrder do use Chronicle.ReadModels.ReadModel
alias MyApp.Events.OrderPlacedForAudit
defstruct [:id, :customer_name, :ordered_at]
from OrderPlacedForAudit, set: [ id: :event_source_id, customer_name: :customer_name, ordered_at: :occurred ]endimport { eventType, fromEvent, readModel, setFrom, setFromContext } from '@cratis/chronicle';
@eventType()class OrderPlacedForAudit { constructor(readonly customerName: string) {}}
@readModel()@fromEvent(OrderPlacedForAudit)class AuditedOrder { @setFrom(OrderPlacedForAudit, 'customerName') customerName = '';
@setFromContext(OrderPlacedForAudit, 'occurred') orderedAt = new Date();}Use an every-event mapping when a read model property should reflect the latest event that affected the projection, rather than one specific event type.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Keys;using Cratis.Chronicle.Projections.ModelBound;
[EventType]public record OrderPlacedForLifecycle(string CustomerName);
[EventType]public record OrderShippedForLifecycle(string TrackingNumber);
public record OrderLifecycle( [Key] Guid Id,
[SetFromContext<OrderPlacedForLifecycle>(nameof(EventContext.Occurred))] DateTimeOffset PlacedAt,
[SetFromContext<OrderShippedForLifecycle>(nameof(EventContext.Occurred))] DateTimeOffset? ShippedAt,
[FromEvery(contextProperty: nameof(EventContext.Occurred))] DateTimeOffset LastModified);Kotlin does not support this workflow yet.Java does not support this workflow yet.defmodule MyApp.Events.OrderPlacedForLifecycle do use Chronicle.Events.EventType, id: "order-placed-for-lifecycle-v1"
defstruct [:customer_name]end
defmodule MyApp.Events.OrderShippedForLifecycle do use Chronicle.Events.EventType, id: "order-shipped-for-lifecycle-v1"
defstruct [:tracking_number]end
defmodule MyApp.ReadModels.OrderLifecycle do use Chronicle.ReadModels.ReadModel
alias MyApp.Events.{OrderPlacedForLifecycle, OrderShippedForLifecycle}
defstruct [:id, :placed_at, :shipped_at, :last_modified]
from OrderPlacedForLifecycle, set: [ id: :event_source_id, placed_at: :occurred ]
from OrderShippedForLifecycle, set: [shipped_at: :occurred]
from_every set: [last_modified: :occurred]endimport { eventType, fromEvent, fromEvery, readModel, setFromContext } from '@cratis/chronicle';
@eventType()class OrderPlacedForLifecycle { constructor(readonly customerName: string) {}}
@eventType()class OrderShippedForLifecycle { constructor(readonly trackingNumber: string) {}}
@readModel()@fromEvent(OrderPlacedForLifecycle)@fromEvent(OrderShippedForLifecycle)class OrderLifecycle { @setFromContext(OrderPlacedForLifecycle, 'occurred') placedAt = new Date();
@setFromContext(OrderShippedForLifecycle, 'occurred') shippedAt?: Date;
@fromEvery(undefined, 'occurred') lastModified = new Date();}Choosing The Mapping
Section titled “Choosing The Mapping”| Mapping | Use it when |
|---|---|
| Set | A read model property should take a value from a specific event. |
| Add | A numeric property should increase when an event occurs. |
| Subtract | A numeric property should decrease when an event occurs. |
| Context | A read model property should come from event metadata. |
| Every event | A property should update for any event that affects the projection. |
Prefer the most direct mapping that expresses the read model. If the read model needs branching, state-dependent logic, or calculations that are clearer as code, use a reducer instead.