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.

Use Set() to map a property explicitly — typically because the event and the read model name it differently:

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

View C# snippet source on GitHub

AutoMap is on by default, so you never call .AutoMap() yourself: every property whose name matches is already mapped. Add explicit mappings only for the properties that need a transformation or a different source:

public class DecSetPropsCombinedAccountProjection : IProjectionFor<DecSetPropsAccount>
{
public void Define(IProjectionBuilderFor<DecSetPropsAccount> builder) => builder
.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
}

View C# snippet source on GitHub

AutoMap works recursively, automatically mapping:

  • Properties with matching names and compatible types
  • Nested objects and their properties
  • Collections and arrays

NoAutoMap() turns it off for the projection (and .AutoMap() turns it back on inside a child scope) — 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);

View C# snippet source on GitHub

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

View C# snippet source on GitHub

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.