Projection with custom properties
When auto-mapping isn’t sufficient, you can explicitly map properties from events to your read model. This gives you full control over how data is transformed and mapped.
Explicit property mapping
Section titled “Explicit property mapping”Use Set() to map a property explicitly — typically because the event and the read model name it differently:
using Cratis.Chronicle.Projections;
public class DecSetPropsAccountProjection : IProjectionFor<DecSetPropsAccount>{ public void Define(IProjectionBuilderFor<DecSetPropsAccount> builder) => builder .From<DecSetPropsAccountOpened>(_ => _ .Set(m => m.AccountNumber).To(e => e.Number) .Set(m => m.CustomerName).To(e => e.Owner.Name) .Set(m => m.Balance).ToValue(42.0m) .Set(m => m.IsActive).ToValue(true) .Set(m => m.OpenedAt).To(e => e.Timestamp)) .From<DecSetPropsMoneyDeposited>(_ => _ .Set(m => m.Balance).To(e => e.Amount) .Set(m => m.LastTransaction).To(e => e.Timestamp));}// Requires io.cratis:chronicle 6.5.0 or later.import io.cratis.chronicle.projections.IProjectionBuilderForimport io.cratis.chronicle.projections.IProjectionFor
class DecSetPropsAccountProjection : IProjectionFor<DecSetPropsAccount> { override fun define(builder: IProjectionBuilderFor<DecSetPropsAccount>) { builder .from(DecSetPropsAccountOpened::class) { it.set(DecSetPropsAccount::accountNumber).toProperty("number") it.set(DecSetPropsAccount::customerName).toProperty("owner.name") it.set(DecSetPropsAccount::isActive).toValue(true) it.set(DecSetPropsAccount::openedAt).toProperty("timestamp") } .from(DecSetPropsMoneyDeposited::class) { it.set(DecSetPropsAccount::balance).toProperty("amount") it.set(DecSetPropsAccount::lastTransaction).toProperty("timestamp") } }}// Requires io.cratis:chronicle 6.5.0 or later.import io.cratis.chronicle.projections.IProjectionBuilderFor;import io.cratis.chronicle.projections.IProjectionFor;
class DecSetPropsAccountProjection implements IProjectionFor<DecSetPropsAccount> { @Override public void define(IProjectionBuilderFor<DecSetPropsAccount> builder) { builder .from(DecSetPropsAccountOpened.class, fb -> { fb.<String>set("accountNumber").toProperty("number"); fb.<String>set("customerName").toProperty("owner.name"); fb.<Boolean>set("isActive").toValue(true); fb.<String>set("openedAt").toProperty("timestamp"); }) .from(DecSetPropsMoneyDeposited.class, fb -> { fb.<Double>set("balance").toProperty("amount"); fb.<String>set("lastTransaction").toProperty("timestamp"); }); }}defmodule MyApp.Projections.DecSetPropsAccountProjection do use Chronicle.Projections.Projection, model: MyApp.ReadModels.DecSetPropsAccount
alias MyApp.Events.{DecSetPropsAccountOpened, DecSetPropsMoneyDeposited}
from DecSetPropsAccountOpened, set: [ account_number: :number, # `owner` is a nested object on the event — the dot-path expression # reaches into it the same way Chronicle's kernel resolves any other # nested event property. customer_name: "owner.name", balance: "$value(42.0)", is_active: "$value(true)", opened_at: :timestamp ]
from DecSetPropsMoneyDeposited, set: [ balance: :amount, last_transaction: :timestamp ]endimport { IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle';
@projection()class DecSetPropsAccountProjection implements IProjectionFor<DecSetPropsAccount> { define(builder: IProjectionBuilderFor<DecSetPropsAccount>): void { builder .from(DecSetPropsAccountOpened, _ => _ .set(m => m.accountNumber).to(e => e.number) .set(m => m.customerName).to(e => e.owner.name) .set(m => m.balance).toValue(42.0) .set(m => m.isActive).toValue(true) .set(m => m.openedAt).to(e => e.timestamp)) .from(DecSetPropsMoneyDeposited, _ => _ .set(m => m.balance).to(e => e.amount) .set(m => m.lastTransaction).to(e => e.timestamp)); }}Combining AutoMap with explicit mapping
Section titled “Combining AutoMap with explicit mapping”AutoMap is on by default, so you never call .AutoMap() yourself: every property whose name matches is already mapped. Add explicit mappings only for the properties that need a transformation or a different source:
public class DecSetPropsCombinedAccountProjection : IProjectionFor<DecSetPropsAccount>{ public void Define(IProjectionBuilderFor<DecSetPropsAccount> builder) => builder .From<DecSetPropsAccountOpened>(_ => _ .Set(m => m.CustomerName).To(e => e.Owner.Name) // Custom mapping for nested property .Set(m => m.IsActive).ToValue(true)) // Custom mapping for constant .From<DecSetPropsMoneyDeposited>(); // Uses AutoMap for all properties}// Requires io.cratis:chronicle 6.5.0 or later.import io.cratis.chronicle.projections.IProjectionBuilderForimport io.cratis.chronicle.projections.IProjectionFor
class DecSetPropsCombinedAccountProjection : IProjectionFor<DecSetPropsAccount> { override fun define(builder: IProjectionBuilderFor<DecSetPropsAccount>) { builder // AutoMap is on by default for matching event properties. .from(DecSetPropsAccountOpened::class) { // Map the property AutoMap cannot find: it is nested on the event. it.set(DecSetPropsAccount::customerName).toProperty("owner.name") it.set(DecSetPropsAccount::isActive).toValue(true) } .from(DecSetPropsMoneyDeposited::class) // Uses AutoMap for all properties }}// Requires io.cratis:chronicle 6.5.0 or later.import io.cratis.chronicle.projections.IProjectionBuilderFor;import io.cratis.chronicle.projections.IProjectionFor;
class DecSetPropsCombinedAccountProjection implements IProjectionFor<DecSetPropsAccount> { @Override public void define(IProjectionBuilderFor<DecSetPropsAccount> builder) { builder // AutoMap is on by default for matching event properties. .from(DecSetPropsAccountOpened.class, fb -> { // Map the property AutoMap cannot find: it is nested on the event. fb.<String>set("customerName").toProperty("owner.name"); fb.<Boolean>set("isActive").toValue(true); }) .from(DecSetPropsMoneyDeposited.class); // Uses AutoMap for all properties }}defmodule MyApp.Projections.DecSetPropsCombinedAccountProjection do use Chronicle.Projections.Projection, model: MyApp.ReadModels.DecSetPropsAccount
alias MyApp.Events.{DecSetPropsAccountOpened, DecSetPropsMoneyDeposited}
# AutoMap fills every property whose name matches the event automatically; # only the two exceptions below need an explicit set:. from DecSetPropsAccountOpened, set: [ customer_name: "owner.name", is_active: "$value(true)" ]
# Uses AutoMap for every property. from DecSetPropsMoneyDepositedendimport { IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle';
@projection()class DecSetPropsCombinedAccountProjection implements IProjectionFor<DecSetPropsAccount> { define(builder: IProjectionBuilderFor<DecSetPropsAccount>): void { builder .autoMap() // Automatically maps matching properties .from(DecSetPropsAccountOpened, _ => _ .set(m => m.customerName).to(e => e.owner.name) // Custom mapping for nested property .set(m => m.isActive).toValue(true)) // Custom mapping for constant .from(DecSetPropsMoneyDeposited); // Uses AutoMap for all properties }}AutoMap works recursively, automatically mapping:
- Properties with matching names and compatible types
- Nested objects and their properties
- Collections and arrays
NoAutoMap() turns it off for the projection (and .AutoMap() turns it back on inside a child scope) — there is no per-event-type toggle. Add explicit .Set() calls inside a specific .From<TEvent>() block for the properties that need custom mapping; AutoMap still fills in every other matching property for that event.
Read model definition
Section titled “Read model definition”The read model can have different property names and types than the events:
public record DecSetPropsAccount( string AccountNumber, string CustomerName, decimal Balance, bool IsActive, DateTimeOffset OpenedAt, DateTimeOffset? LastTransaction);import java.math.BigDecimalimport java.time.OffsetDateTime
data class DecSetPropsAccount( val accountNumber: String = "", val customerName: String = "", val balance: BigDecimal = BigDecimal.ZERO, val isActive: Boolean = false, val openedAt: OffsetDateTime? = null, val lastTransaction: OffsetDateTime? = null)class DecSetPropsAccount { public String accountNumber = ""; public String customerName = ""; public double balance = 0.0; public boolean isActive = false; public String openedAt = null; public String lastTransaction = null;}defmodule MyApp.ReadModels.DecSetPropsAccount do use Chronicle.ReadModels.ReadModel
defstruct [ :account_number, :customer_name, :balance, :is_active, :opened_at, :last_transaction ]endclass DecSetPropsAccount { accountNumber = ''; customerName = ''; balance = 0; isActive = false; openedAt = new Date(); lastTransaction: Date | null = null;}Event definitions
Section titled “Event definitions”Events can have different structures than the read model:
using Cratis.Chronicle.Events;
[EventType]public record DecSetPropsAccountOpened( string Number, DecSetPropsCustomer Owner, DateTimeOffset Timestamp);
[EventType]public record DecSetPropsMoneyDeposited( decimal Amount, DateTimeOffset Timestamp);
public record DecSetPropsCustomer(string Name, string Email);import io.cratis.chronicle.events.EventTypeimport java.math.BigDecimalimport java.time.OffsetDateTime
data class DecSetPropsCustomer(val name: String, val email: String)
@EventTypedata class DecSetPropsAccountOpened( val number: String, val owner: DecSetPropsCustomer, val timestamp: OffsetDateTime)
@EventTypedata class DecSetPropsMoneyDeposited( val amount: BigDecimal, val timestamp: OffsetDateTime)import io.cratis.chronicle.events.EventType;
record DecSetPropsCustomer(String name, String email) {}
@EventTyperecord DecSetPropsAccountOpened( String number, DecSetPropsCustomer owner, String timestamp) {}
@EventTyperecord DecSetPropsMoneyDeposited( double amount, String timestamp) {}defmodule MyApp.Events.DecSetPropsCustomer do defstruct [:name, :email]end
defmodule MyApp.Events.DecSetPropsAccountOpened do use Chronicle.Events.EventType, id: "dec-set-props-account-opened"
defstruct [:number, :owner, :timestamp]end
defmodule MyApp.Events.DecSetPropsMoneyDeposited do use Chronicle.Events.EventType, id: "dec-set-props-money-deposited"
defstruct [:amount, :timestamp]endimport { eventType } from '@cratis/chronicle';
class DecSetPropsCustomer { name = ''; email = '';}
@eventType()class DecSetPropsAccountOpened { number = ''; owner = new DecSetPropsCustomer(); timestamp = new Date();}
@eventType()class DecSetPropsMoneyDeposited { amount = 0; timestamp = new Date();}Property mapping options
Section titled “Property mapping options”You can map properties in several ways:
- From event property:
.Set(m => m.CustomerName).To(e => e.Owner.Name) - From constant value:
.Set(m => m.IsActive).ToValue(true) - From event context:
.Set(m => m.OpenedAt).ToEventContextProperty(c => c.Occurred) - From event source ID:
.Set(m => m.Id).ToEventSourceId()
Multiple events
Section titled “Multiple events”A single projection can handle multiple event types, each with its own property mappings. Properties are updated incrementally as events are processed.
In the example above:
AccountOpenedsets initial values for all propertiesMoneyDepositedonly updatesBalanceandLastTransaction- Other properties retain their previous values
This approach gives you precise control over how your read models are built from events.