Basic Property Mapping
Model-bound projections support three fundamental property mapping operations: SetFrom, AddFrom, and SubtractFrom.
SetFrom
The SetFrom attribute maps a property from an event to the read model. This is the most common mapping operation.
using Cratis.Chronicle.Keys;
using Cratis.Chronicle.Projections.ModelBound;
public record User(
[Key]
Guid Id,
[SetFrom<UserRegistered>(nameof(UserRegistered.Email))]
string Email,
[SetFrom<UserRegistered>(nameof(UserRegistered.Name))]
string Name);
Property Name Convention
If the property name on the event matches the read model property name, you can omit the property name parameter:
public record User(
[Key] Guid Id,
[SetFrom<UserRegistered>] string Email, // Maps to UserRegistered.Email
[SetFrom<UserRegistered>] string Name); // Maps to UserRegistered.Name
Multiple Events
You can set a property from multiple different events:
public record Account(
[Key] Guid Id,
[SetFrom<AccountOpened>(nameof(AccountOpened.AccountName))]
[SetFrom<AccountRenamed>(nameof(AccountRenamed.NewName))]
string Name);
AddFrom
The AddFrom attribute adds values from event properties to the read model property. This is useful for accumulating values like balances or totals.
public record Account(
[Key] Guid Id,
[SetFrom<AccountOpened>(nameof(AccountOpened.InitialBalance))]
[AddFrom<DepositMade>(nameof(DepositMade.Amount))]
decimal Balance);
When a DepositMade event occurs, the Amount is added to the current Balance.
SubtractFrom
The SubtractFrom attribute subtracts values from event properties. This is commonly used with AddFrom to model increases and decreases.
public record Account(
[Key] Guid Id,
[SetFrom<AccountOpened>(nameof(AccountOpened.InitialBalance))]
[AddFrom<DepositMade>(nameof(DepositMade.Amount))]
[SubtractFrom<WithdrawalMade>(nameof(WithdrawalMade.Amount))]
decimal Balance);
Complete Example
Here's a complete example showing all three mapping operations:
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Keys;
using Cratis.Chronicle.Projections.ModelBound;
// Events
[EventType]
public record AccountOpened(string AccountName, decimal InitialBalance);
[EventType]
public record DepositMade(decimal Amount);
[EventType]
public record WithdrawalMade(decimal Amount);
[EventType]
public record AccountRenamed(string NewName);
// Read Model
public record BankAccount(
[Key]
Guid Id,
[SetFrom<AccountOpened>(nameof(AccountOpened.AccountName))]
[SetFrom<AccountRenamed>(nameof(AccountRenamed.NewName))]
string Name,
[SetFrom<AccountOpened>(nameof(AccountOpened.InitialBalance))]
[AddFrom<DepositMade>(nameof(DepositMade.Amount))]
[SubtractFrom<WithdrawalMade>(nameof(WithdrawalMade.Amount))]
decimal Balance);
SetFromContext
The SetFromContext attribute maps properties from the event context to the read model. This is useful for capturing event metadata like timestamps, sequence numbers, or correlation IDs for specific event types.
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Keys;
using Cratis.Chronicle.Projections.ModelBound;
public record Order(
[Key]
Guid Id,
[SetFrom<OrderPlaced>(nameof(OrderPlaced.CustomerName))]
string CustomerName,
[SetFromContext<OrderPlaced>(nameof(EventContext.Occurred))]
DateTimeOffset OrderedAt,
[SetFromContext<OrderShipped>(nameof(EventContext.Occurred))]
DateTimeOffset? ShippedAt);
In this example:
OrderedAtis set to the occurrence time when anOrderPlacedevent is processedShippedAtis set to the occurrence time when anOrderShippedevent is processed
Available Event Context Properties
The EventContext provides several properties you can map to:
public record AuditedEntity(
[Key] Guid Id,
[SetFromContext<EntityCreated>(nameof(EventContext.Occurred))]
DateTimeOffset CreatedAt,
[SetFromContext<EntityCreated>(nameof(EventContext.SequenceNumber))]
ulong CreatedAtSequence,
[SetFromContext<EntityCreated>(nameof(EventContext.CorrelationId))]
CorrelationId CreatedByCorrelation,
[SetFromContext<EntityUpdated>(nameof(EventContext.Occurred))]
DateTimeOffset? LastUpdatedAt);
Context Property Name Convention
If the event context property name matches the read model property name, you can omit the property name parameter:
public record Event(
[Key] Guid Id,
[SetFromContext<EventHappened>] DateTimeOffset Occurred, // Maps to EventContext.Occurred
[SetFromContext<EventHappened>] ulong SequenceNumber); // Maps to EventContext.SequenceNumber
SetFromContext vs FromEvery
The key difference between SetFromContext and FromEvery:
- SetFromContext - Maps event context properties for specific event types
- FromEvery - Maps properties that should be updated for every event affecting the projection
Use SetFromContext when you want to track when specific events occurred (e.g., "when was this order placed?"), and use FromEvery when you want to track the most recent update across all events (e.g., "when was this entity last modified by any event?").
public record OrderWithAudit(
[Key] Guid Id,
// Specific event context properties
[SetFromContext<OrderPlaced>(nameof(EventContext.Occurred))]
DateTimeOffset PlacedAt,
[SetFromContext<OrderShipped>(nameof(EventContext.Occurred))]
DateTimeOffset? ShippedAt,
// Updated by any event
[FromEvery(contextProperty: nameof(EventContext.Occurred))]
DateTimeOffset LastModified);
Event Flow
When events are processed:
- AccountOpened - Sets both
NameandBalance(initial balance) - DepositMade - Adds to
Balance - WithdrawalMade - Subtracts from
Balance - AccountRenamed - Updates
Name
The projection automatically maintains the current state of the account based on the events.