Skip to content

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

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

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 is enabled by default at the top level and can be controlled at three different levels in a projection:

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

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).ToValue(false)
.Set(m => m.ClosedAt).ToEventContextProperty(c => c.Occurred));

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

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).ToValue("Active")
.Set(m => m.Balance).ToValue(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 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))));

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

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.

public class ProductProjection : IProjectionFor<Product>
{
public void Define(IProjectionBuilderFor<Product> builder) => builder
.AutoMap()
.From<ProductCreated>()
.From<ProductUpdated>();
}
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)));
}
public class CustomerProjection : IProjectionFor<Customer>
{
public void Define(IProjectionBuilderFor<Customer> builder) => builder
.AutoMap()
.From<CustomerRegistered>(_ => _
.Set(m => m.Status).ToValue("Active")
.Set(m => m.MemberSince).ToEventContextProperty(c => c.Occurred))
.From<CustomerDetailsUpdated>(); // Pure AutoMap
}
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)));
}