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”Instead of using AutoMap(), use Set() methods to explicitly define property mappings:
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));}import io.cratis.chronicle.projections.IProjectionBuilderForimport io.cratis.chronicle.projections.IProjectionForimport java.math.BigDecimal
class DecSetPropsAccountProjection : IProjectionFor<DecSetPropsAccount> { override fun define(builder: IProjectionBuilderFor<DecSetPropsAccount>) { builder .from(DecSetPropsAccountOpened::class) { it.set(DecSetPropsAccount::accountNumber).to { e -> e.number } it.set(DecSetPropsAccount::customerName).to { e -> e.owner.name } it.set(DecSetPropsAccount::balance).to { BigDecimal("42.0") } it.set(DecSetPropsAccount::isActive).to { true } it.set(DecSetPropsAccount::openedAt).to { e -> e.timestamp } } .from(DecSetPropsMoneyDeposited::class) { it.set(DecSetPropsAccount::balance).to { e -> e.amount } it.set(DecSetPropsAccount::lastTransaction).to { e -> e.timestamp } } }}import io.cratis.chronicle.projections.IProjectionBuilderFor;import io.cratis.chronicle.projections.IProjectionFor;
import java.math.BigDecimal;
class DecSetPropsAccountProjection implements IProjectionFor<DecSetPropsAccount> { @Override public void define(IProjectionBuilderFor<DecSetPropsAccount> builder) { builder .from(DecSetPropsAccountOpened.class, fb -> { fb.<String>set("accountNumber").to(e -> e.number()); fb.<String>set("customerName").to(e -> e.owner().name()); fb.<BigDecimal>set("balance").to(e -> new BigDecimal("42.0")); fb.<Boolean>set("isActive").to(e -> true); fb.<String>set("openedAt").to(e -> e.timestamp()); }) .from(DecSetPropsMoneyDeposited.class, fb -> { fb.<Double>set("balance").to(e -> e.amount()); fb.<String>set("lastTransaction").to(e -> e.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”You can use AutoMap() at the top level to automatically map matching properties, then add explicit mappings for specific transformations:
public class DecSetPropsCombinedAccountProjection : IProjectionFor<DecSetPropsAccount>{ public void Define(IProjectionBuilderFor<DecSetPropsAccount> builder) => 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}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; these two properties don't have matching event // properties, so they're set explicitly and everything else is left to AutoMap. .from(DecSetPropsAccountOpened::class) { it.set(DecSetPropsAccount::customerName).to { e -> e.owner.name } it.set(DecSetPropsAccount::isActive).to { true } } .from(DecSetPropsMoneyDeposited::class) // Uses AutoMap for all properties }}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; these two properties don't have matching event // properties, so they're set explicitly and everything else is left to AutoMap. .from(DecSetPropsAccountOpened.class, fb -> { fb.<String>set("customerName").to(e -> e.owner().name()); fb.<Boolean>set("isActive").to(e -> 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
AutoMap()/NoAutoMap() are set once at the projection level — 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.