Skip to content

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.

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));
}

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
}

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.

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);

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);

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()

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:

  • AccountOpened sets initial values for all properties
  • MoneyDeposited only updates Balance and LastTransaction
  • Other properties retain their previous values

This approach gives you precise control over how your read models are built from events.