Basic Property Mapping
Model-bound projections support three fundamental property mapping operations: SetFrom, AddFrom, and SubtractFrom.
SetFrom
Section titled “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
Section titled “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.NameMultiple Events
Section titled “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
Section titled “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
Section titled “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
Section titled “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 Modelpublic 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
Section titled “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
Section titled “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
Section titled “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.SequenceNumberSetFromContext vs FromEvery
Section titled “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
Section titled “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.