Skip to content

Projection With Initial Values

Initial values define the read model state Chronicle starts from before applying event mappings. Use them when a property needs a meaningful default even though no event has assigned it yet.

The exact API name depends on the client. .NET exposes WithInitialValues, while TypeScript exposes withInitialValues.

Provide a factory that returns a fresh read model instance with the desired default values. Event mappings then apply on top of that initial state.

Kotlin and Elixir don’t have a WithInitialValues-style builder call, but they achieve the same result idiomatically: the read model’s own constructor/struct default values become its initial state (see Collections below for a working example that doesn’t need event context). This particular example also maps an event context property (Occurred), which neither client’s fluent projection API can access yet.

Initial values
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Projections;
public enum InitialValuesUserStatus
{
Inactive,
Active
}
[EventType]
public record InitialValuesUserCreated(string Name, string Email);
public record InitialValuesUserProfile(
string Name,
string Email,
InitialValuesUserStatus Status,
DateTimeOffset CreatedAt,
DateTimeOffset? LastLogin,
int LoginCount,
bool IsVerified);
public class InitialValuesUserProfileProjection : IProjectionFor<InitialValuesUserProfile>
{
public void Define(IProjectionBuilderFor<InitialValuesUserProfile> builder) => builder
.WithInitialValues(() => new InitialValuesUserProfile(
Name: "Unknown user",
Email: string.Empty,
Status: InitialValuesUserStatus.Inactive,
CreatedAt: DateTimeOffset.UnixEpoch,
LastLogin: null,
LoginCount: 0,
IsVerified: false))
.From<InitialValuesUserCreated>(_ => _
.Set(m => m.Status).ToValue(InitialValuesUserStatus.Active)
.Set(m => m.CreatedAt).ToEventContextProperty(c => c.Occurred));
}

Without initial values, properties that are not touched by a projection mapping keep the platform’s default representation. That can mean null, 0, false, an empty string, or an unset date depending on the client and storage representation.

Initial values are useful for:

NeedExample
Business defaultsA new order starts as draft until an event changes its status.
Non-null collectionsA customer read model starts with empty address and tag collections.
Sentinel valuesA timestamp starts at a known sentinel until the first event provides a real value.
Partial eventsInventory limits are configured once while stock events update only quantities.

Set defaults that describe the domain state before the first relevant event changes it.

Business defaults
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Projections;
public enum InitialValuesOrderStatus
{
Draft,
Submitted
}
[EventType]
public record InitialValuesOrderSubmitted(string CustomerName, decimal TotalAmount);
public record InitialValuesOrderSummary(
string CustomerName,
InitialValuesOrderStatus Status,
decimal TotalAmount,
DateTimeOffset SubmittedAt,
string Notes);
public class InitialValuesOrderSummaryProjection : IProjectionFor<InitialValuesOrderSummary>
{
public void Define(IProjectionBuilderFor<InitialValuesOrderSummary> builder) => builder
.WithInitialValues(() => new InitialValuesOrderSummary(
CustomerName: string.Empty,
Status: InitialValuesOrderStatus.Draft,
TotalAmount: 0m,
SubmittedAt: DateTimeOffset.UnixEpoch,
Notes: "No notes"))
.From<InitialValuesOrderSubmitted>(_ => _
.Set(m => m.Status).ToValue(InitialValuesOrderStatus.Submitted)
.Set(m => m.SubmittedAt).ToEventContextProperty(c => c.Occurred));
}

Initialize collections so consumers can treat them as empty collections instead of checking for missing values.

Initialize collections
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Projections;
public record InitialValuesAddress(string Street, string City);
[EventType]
public record InitialValuesCustomerRegistered(string Name);
public record InitialValuesCustomerRecord(
string Name,
IEnumerable<InitialValuesAddress> Addresses,
IEnumerable<string> Tags);
public class InitialValuesCustomerRecordProjection : IProjectionFor<InitialValuesCustomerRecord>
{
public void Define(IProjectionBuilderFor<InitialValuesCustomerRecord> builder) => builder
.WithInitialValues(() => new InitialValuesCustomerRecord(
Name: string.Empty,
Addresses: Array.Empty<InitialValuesAddress>(),
Tags: Array.Empty<string>()))
.From<InitialValuesCustomerRegistered>();
}

Initial values are useful when events update operational fields but leave configuration fields unchanged.

TypeScript note: this example uses Add()/With(), the fluent builder’s variable-delta accumulation operators. TypeScript’s add()/subtract() are not implemented yet (FromBuilder.ts throws at runtime even though the call type-checks), so this specific example is unsupported in TypeScript for now. Elixir’s add:/subtract: property options do work, so this example is also shown there.

Defaults for fields events do not set
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Projections;
[EventType]
public record InitialValuesStockReceived(int Quantity);
[EventType]
public record InitialValuesStockReserved(int Quantity);
public record InitialValuesInventoryItem(
int CurrentStock,
int ReservedStock,
DateTimeOffset LastUpdated,
int MinimumLevel,
int MaximumLevel,
int ReorderPoint);
public class InitialValuesInventoryProjection : IProjectionFor<InitialValuesInventoryItem>
{
public void Define(IProjectionBuilderFor<InitialValuesInventoryItem> builder) => builder
.WithInitialValues(() => new InitialValuesInventoryItem(
CurrentStock: 0,
ReservedStock: 0,
LastUpdated: DateTimeOffset.UnixEpoch,
MinimumLevel: 10,
MaximumLevel: 1000,
ReorderPoint: 20))
.From<InitialValuesStockReceived>(_ => _
.Add(m => m.CurrentStock).With(e => e.Quantity)
.Set(m => m.LastUpdated).ToEventContextProperty(c => c.Occurred))
.From<InitialValuesStockReserved>(_ => _
.Add(m => m.ReservedStock).With(e => e.Quantity)
.Set(m => m.LastUpdated).ToEventContextProperty(c => c.Occurred));
}
  • Use the factory form so each read model starts from a fresh instance.
  • Prefer deterministic defaults. The initial state is captured when the projection definition is built.
  • Keep defaults domain meaningful; do not duplicate platform defaults unless the value communicates intent.
  • Initialize collections that readers naturally expect to enumerate.
  • Let events own facts that actually happened; initial values are for the state before those facts are projected.