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 } } }}Java does not support this workflow yet.Elixir does not support this workflow yet.import { 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 }}Java does not support this workflow yet.Elixir does not support this workflow yet.import { 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)Java does not support this workflow yet.Elixir does not support this workflow yet.class 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)
@EventType(id = "dec-set-props-account-opened")data class DecSetPropsAccountOpened( val number: String, val owner: DecSetPropsCustomer, val timestamp: OffsetDateTime)
@EventType(id = "dec-set-props-money-deposited")data class DecSetPropsMoneyDeposited( val amount: BigDecimal, val timestamp: OffsetDateTime)Java does not support this workflow yet.Elixir does not support this workflow yet.import { 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.