Table of Contents

Projection AutoMap

AutoMap is a powerful feature that automatically maps properties with matching names between events and read models. This eliminates the need for explicit property mappings when property names and types are compatible.

Important: AutoMap is now enabled by default at the top level for all projections. You only need to explicitly call .AutoMap() when you want to re-enable it in contexts where it was previously disabled (e.g., within a specific event handler or child projection).

Basic AutoMap usage

Since AutoMap is enabled by default, you can simply use .From<>() without explicitly calling .AutoMap():

using Cratis.Chronicle.Projections;

public class UserProjection : IProjectionFor<User>
{
    public void Define(IProjectionBuilderFor<User> builder) => builder
        .From<UserCreated>()
        .From<UserUpdated>();
}

This automatically maps all properties with matching names from both UserCreated and UserUpdated events to the User read model.

If you want to explicitly enable AutoMap (for clarity or to override a previous .NoAutoMap() call), you can still call it:

public void Define(IProjectionBuilderFor<User> builder) => builder
    .AutoMap()  // Explicit (but redundant at top level)
    .From<UserCreated>()
    .From<UserUpdated>();
}

How AutoMap works

AutoMap performs name-based matching:

  1. Property name matching: Looks for properties with identical names (case-sensitive) in both event and read model
  2. Type compatibility: Ensures the property types are compatible for assignment
  3. Recursive mapping: For nested objects, AutoMap recursively maps nested properties
  4. Collection handling: Arrays and collections are automatically handled

Example:

// Event
[EventType]
public record UserCreated(string Name, string Email, Address HomeAddress);

// Nested type
public record Address(string Street, string City, string ZipCode);

// Read model
public record User(string Name, string Email, Address HomeAddress);

With AutoMap(), all properties including the nested Address object are automatically mapped.

AutoMap at different levels

AutoMap is enabled by default at the top level and can be controlled at three different levels in a projection:

1. Top-level AutoMap (Default Enabled)

At the top level, AutoMap is enabled by default and affects all event handlers:

public void Define(IProjectionBuilderFor<Account> builder) => builder
    .From<AccountOpened>()   // AutoMap enabled by default
    .From<AccountUpdated>()  // AutoMap enabled by default
    .Join<CustomerUpdated>(j => j.On(m => m.CustomerId));

If you need to disable AutoMap at the top level, use .NoAutoMap():

public void Define(IProjectionBuilderFor<Account> builder) => builder
    .NoAutoMap()  // Disables default AutoMap behavior
    .From<AccountOpened>(_ => _
        .Set(m => m.Name).To(e => e.AccountName));  // Explicit mapping required

2. Per-event AutoMap

You can override the top-level AutoMap setting for specific event handlers:

public void Define(IProjectionBuilderFor<Account> builder) => builder
    .From<AccountOpened>()  // Uses default AutoMap
    .From<AccountClosed>(_ => _
        .NoAutoMap()  // Disables AutoMap for this event only
        .Set(m => m.IsActive).To(false)
        .Set(m => m.ClosedAt).ToEventContextProperty(c => c.Occurred));

3. Children AutoMap (Inherits by default)

Child projections inherit AutoMap behavior from their parent by default:

public void Define(IProjectionBuilderFor<Order> builder) => builder
    .From<OrderCreated>()  // AutoMap enabled by default
    .Children(m => m.Items, children => children
        .IdentifiedBy(e => e.ProductId)
        // AutoMap is inherited from parent (enabled)
        .From<ItemAddedToOrder>(_ => _
            .UsingKey(e => e.ProductId)));

You can explicitly control AutoMap for children:

public void Define(IProjectionBuilderFor<Order> builder) => builder
    .NoAutoMap()  // Disabled at top level
    .From<OrderCreated>(_ => _
        .Set(m => m.OrderNumber).To(e => e.Number))
    .Children(m => m.Items, children => children
        .IdentifiedBy(e => e.ProductId)
        .AutoMap()  // Explicitly enable AutoMap for children only
        .From<ItemAddedToOrder>(_ => _
            .UsingKey(e => e.ProductId)));

AutoMap with joins

AutoMap works with joins to automatically map properties from joined events:

public void Define(IProjectionBuilderFor<Employee> builder) => builder
    .AutoMap()
    .From<EmployeeHired>()
    .From<EmployeeAssignedToDepartment>(_ => _
        .UsingKey(e => e.EmployeeId)
        .Set(m => m.DepartmentId).ToEventSourceId())
    .Join<DepartmentCreated>(j => j
        .On(m => m.DepartmentId));  // AutoMap applies to joined properties

When DepartmentCreated has properties like DepartmentName and DepartmentCode, and the Employee read model has matching properties, they are automatically mapped.

AutoMap in child joins

AutoMap can be applied to joins within child collections:

public void Define(IProjectionBuilderFor<Project> builder) => builder
    .AutoMap()
    .From<ProjectCreated>()
    .Children(m => m.TeamMembers, children => children
        .IdentifiedBy(e => e.EmployeeId)
        .AutoMap()
        .From<EmployeeAssignedToProject>(_ => _
            .UsingKey(e => e.EmployeeId))
        .Join<EmployeeProfileUpdated>(j => j
            .On(m => m.EmployeeId)));  // AutoMap applies here too

Combining AutoMap with explicit mappings

AutoMap and explicit mappings work together seamlessly. AutoMap handles matching properties, while explicit mappings handle special cases:

public void Define(IProjectionBuilderFor<Account> builder) => builder
    .AutoMap()  // Maps matching properties automatically
    .From<AccountOpened>(_ => _
        // Explicit mappings override/extend AutoMap
        .Set(m => m.Status).To("Active")
        .Set(m => m.Balance).To(0m)
        .Set(m => m.CreatedAt).ToEventContextProperty(c => c.Occurred))
    .From<MoneyDeposited>();  // Uses only AutoMap

In this example:

  • AccountOpened uses AutoMap for matching properties, plus explicit mappings for Status, Balance, and CreatedAt
  • MoneyDeposited relies entirely on AutoMap

AutoMap with nested children

AutoMap cascades through multiple levels of hierarchy:

public void Define(IProjectionBuilderFor<Company> builder) => builder
    .AutoMap()  // Cascades to all levels
    .From<CompanyRegistered>()
    .Children(m => m.Departments, departments => departments
        .IdentifiedBy(e => e.DepartmentId)
        // AutoMap inherited from parent
        .From<DepartmentCreated>(_ => _
            .UsingKey(e => e.DepartmentId))
        // No parent key specified - uses EventSourceId (CompanyId) by default
        .Children(dm => dm.Employees, employees => employees
            .IdentifiedBy(e => e.EmployeeId)
            // AutoMap still applies at this nested level
            .From<EmployeeAssignedToDepartment>(_ => _
                .UsingParentKey(e => e.DepartmentId)  // Extracts from event content
                .UsingKey(e => e.EmployeeId))));

When to use AutoMap

Use AutoMap when:

  • Property names match exactly between events and read models
  • Types are directly compatible
  • You want to reduce boilerplate code
  • You're following naming conventions consistently

Avoid AutoMap when:

  • Property names differ between events and read models
  • Complex transformations are needed
  • You need fine-grained control over mapping logic
  • Properties require calculations or aggregations

Best practices

  1. Consistent naming: Use consistent property names across events and read models to maximize AutoMap effectiveness
  2. Combine approaches: Use AutoMap for simple mappings and explicit Set() calls for complex transformations
  3. Be explicit when needed: If clarity matters more than brevity, use explicit mappings even when AutoMap would work
  4. Document custom logic: When mixing AutoMap with explicit mappings, document why specific properties need custom handling
  5. Nested structures: Ensure nested types also follow consistent naming for recursive AutoMap to work effectively

Performance considerations

AutoMap performs name matching at projection definition time, not during event processing. There is no runtime performance penalty for using AutoMap compared to explicit mappings - both compile to the same internal representation.

Examples

Simple AutoMap

public class ProductProjection : IProjectionFor<Product>
{
    public void Define(IProjectionBuilderFor<Product> builder) => builder
        .AutoMap()
        .From<ProductCreated>()
        .From<ProductUpdated>();
}

AutoMap with children

public class OrderProjection : IProjectionFor<Order>
{
    public void Define(IProjectionBuilderFor<Order> builder) => builder
        .AutoMap()
        .From<OrderPlaced>()
        .Children(m => m.Items, children => children
            .IdentifiedBy(e => e.LineItemId)
            .From<LineItemAdded>(_ => _
                .UsingKey(e => e.LineItemId)));
}

AutoMap with explicit overrides

public class CustomerProjection : IProjectionFor<Customer>
{
    public void Define(IProjectionBuilderFor<Customer> builder) => builder
        .AutoMap()
        .From<CustomerRegistered>(_ => _
            .Set(m => m.Status).To("Active")
            .Set(m => m.MemberSince).ToEventContextProperty(c => c.Occurred))
        .From<CustomerDetailsUpdated>();  // Pure AutoMap
}

Selective AutoMap for children only

public class TeamProjection : IProjectionFor<Team>
{
    public void Define(IProjectionBuilderFor<Team> builder) => builder
        .From<TeamFormed>(_ => _
            .Set(m => m.Name).To(e => e.TeamName)
            .Set(m => m.CreatedAt).ToEventContextProperty(c => c.Occurred))
        .Children(m => m.Members, children => children
            .IdentifiedBy(e => e.MemberId)
            .AutoMap()  // Only children use AutoMap
            .From<MemberJoinedTeam>(_ => _
                .UsingKey(e => e.MemberId)));
}