Table of Contents

Projection with initial values

The WithInitialValues() method allows you to set default values for properties in your read models. This is particularly useful when you want to ensure properties have meaningful default values instead of being null or uninitialized.

Basic initial values usage

Use WithInitialValues() to provide a factory function that creates a read model instance with default values:

public class UserProfileProjection : IProjectionFor<UserProfile>
{
    public void Define(IProjectionBuilderFor<UserProfile> builder) => builder
        .WithInitialValues(() => new UserProfile(
            UserId: string.Empty,
            Name: "Unknown User",
            Email: string.Empty,
            Status: UserStatus.Inactive,
            CreatedAt: DateTimeOffset.MinValue,
            LastLogin: null,
            LoginCount: 0,
            IsVerified: false))
        .From<UserCreated>(_ => _
            .Set(m => m.Name).To(e => e.Name)
            .Set(m => m.Email).To(e => e.Email)
            .Set(m => m.Status).ToValue(UserStatus.Active)
            .Set(m => m.CreatedAt).ToEventContextProperty(c => c.Occurred))
        .From<UserLoggedIn>(_ => _
            .Set(m => m.LastLogin).ToEventContextProperty(c => c.Occurred)
            .Increment(m => m.LoginCount))
        .From<UserEmailVerified>(_ => _
            .Set(m => m.IsVerified).ToValue(true));
}

Why use initial values

Without initial values, properties that aren't set by events will be left as their default .NET values:

  • Reference types: null
  • Numbers: 0
  • Booleans: false
  • DateTime: DateTime.MinValue

Initial values allow you to:

Provide meaningful defaults

Instead of null or 0, set business-appropriate defaults:

.WithInitialValues(() => new OrderSummary(
    OrderId: string.Empty,
    Status: OrderStatus.Draft,
    TotalAmount: 0.0m,
    ItemCount: 0,
    CreatedDate: DateTimeOffset.MinValue,
    Notes: "No notes"))

Avoid null reference exceptions

Ensure collections and strings are never null:

.WithInitialValues(() => new CustomerRecord(
    CustomerId: string.Empty,
    Name: string.Empty,
    Addresses: new List<Address>(),
    Orders: new List<Order>(),
    Tags: new HashSet<string>(),
    Metadata: new Dictionary<string, string>()))

Set computed or calculated defaults

Initialize properties with calculated values:

.WithInitialValues(() => new ReportData(
    ReportId: string.Empty,
    GeneratedAt: DateTimeOffset.UtcNow,
    ExpiresAt: DateTimeOffset.UtcNow.AddDays(30),
    Version: "1.0",
    Status: ReportStatus.Pending))

Initial values with concepts

When using Cratis concepts, provide appropriate default concept values:

.WithInitialValues(() => new ProductCatalog(
    ProductId: ProductId.Empty,
    Name: ProductName.NotSet,
    Price: Price.Zero,
    Category: Category.Uncategorized,
    InStock: Quantity.Zero,
    IsActive: false))

Working with complex types

For read models with nested objects or collections:

.WithInitialValues(() => new OrderDetails(
    OrderId: string.Empty,
    Customer: new CustomerInfo(
        CustomerId: string.Empty,
        Name: "Guest Customer",
        Email: string.Empty),
    Items: new List<OrderItem>(),
    Shipping: new ShippingInfo(
        Address: "Not provided",
        Method: ShippingMethod.Standard,
        Cost: 0.0m),
    Payment: new PaymentInfo(
        Method: PaymentMethod.Unknown,
        Status: PaymentStatus.Pending,
        Amount: 0.0m)))

Initial values with events that don't cover all properties

This is especially useful when events only update specific properties:

public class InventoryProjection : IProjectionFor<InventoryItem>
{
    public void Define(IProjectionBuilderFor<InventoryItem> builder) => builder
        .WithInitialValues(() => new InventoryItem(
            ProductId: string.Empty,
            CurrentStock: 0,
            ReservedStock: 0,
            AvailableStock: 0,
            LastUpdated: DateTimeOffset.MinValue,
            MinimumLevel: 10,  // Business default
            MaximumLevel: 1000,  // Business default
            ReorderPoint: 20))  // Business default
        .From<StockReceived>(_ => _
            .Add(m => m.CurrentStock).With(e => e.Quantity)
            .Set(m => m.LastUpdated).ToEventContextProperty(c => c.Occurred))
        .From<StockReserved>(_ => _
            .Add(m => m.ReservedStock).With(e => e.Quantity)
            .Set(m => m.LastUpdated).ToEventContextProperty(c => c.Occurred));
}

In this example, MinimumLevel, MaximumLevel, and ReorderPoint are set by initial values since no events modify them.

Read model examples

public record UserProfile(
    string UserId,
    string Name,
    string Email,
    UserStatus Status,
    DateTimeOffset CreatedAt,
    DateTimeOffset? LastLogin,
    int LoginCount,
    bool IsVerified);

public record OrderSummary(
    string OrderId,
    OrderStatus Status,
    decimal TotalAmount,
    int ItemCount,
    DateTimeOffset CreatedDate,
    string Notes);

public record InventoryItem(
    string ProductId,
    int CurrentStock,
    int ReservedStock,
    int AvailableStock,
    DateTimeOffset LastUpdated,
    int MinimumLevel,
    int MaximumLevel,
    int ReorderPoint);

Event definitions

[EventType]
public record UserCreated(string Name, string Email);

[EventType]
public record UserLoggedIn(string UserId);

[EventType]
public record UserEmailVerified(string UserId);

[EventType]
public record StockReceived(string ProductId, int Quantity);

[EventType]
public record StockReserved(string ProductId, int Quantity);

Best practices

Use factory functions

Always provide a factory function rather than a static instance:

// ✅ Good - creates fresh instance each time
.WithInitialValues(() => new MyModel(...))

// ❌ Bad - would reuse same instance
.WithInitialValues(() => someStaticInstance)

Set business-meaningful defaults

Choose defaults that make sense in your domain:

// ✅ Good - meaningful business defaults
.WithInitialValues(() => new Account(
    Balance: 0.0m,
    Status: AccountStatus.PendingActivation,
    OpenedDate: DateTimeOffset.UtcNow))

// ❌ Less helpful - just .NET defaults
.WithInitialValues(() => new Account())

Initialize collections

Always initialize collections to avoid null reference exceptions:

.WithInitialValues(() => new ShoppingCart(
    Items: new List<CartItem>(),
    Coupons: new List<Coupon>(),
    Tags: new HashSet<string>()))

The WithInitialValues() method ensures your read models have consistent, meaningful default values, improving the reliability and usability of your projection data.