Skip to content

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 mappings copy a value from an event into the read model. This is the most common model-bound operation.

Model-bound set mapping
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);

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.

Convention-based set mapping
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);

Use multiple set mappings when different events can update the same read model property.

Multiple set mappings
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);

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.

Add from an event
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);

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.

Subtract from an event
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);

The event flow for a balance-style read model is:

  1. The opening event sets the initial balance.
  2. Deposit events add to the balance.
  3. Withdrawal events subtract from the balance.
  4. Rename or profile events update descriptive properties without changing the balance.
Complete balance projection
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);

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.

Map event context
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);

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.

Specific context vs every event
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);
MappingUse it when
SetA read model property should take a value from a specific event.
AddA numeric property should increase when an event occurs.
SubtractA numeric property should decrease when an event occurs.
ContextA read model property should come from event metadata.
Every eventA 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.