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
Section titled “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
Section titled “How AutoMap works”AutoMap performs name-based matching:
- Property name matching: Looks for properties with identical names (case-sensitive) in both event and read model
- Type compatibility: Ensures the property types are compatible for assignment
- Recursive mapping: For nested objects, AutoMap recursively maps nested properties
- Collection handling: Arrays and collections are automatically handled
Example:
// Event[EventType]public record UserCreated(string Name, string Email, Address HomeAddress);
// Nested typepublic record Address(string Street, string City, string ZipCode);
// Read modelpublic record User(string Name, string Email, Address HomeAddress);With AutoMap(), all properties including the nested Address object are automatically mapped.
AutoMap at different levels
Section titled “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)
Section titled “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 required2. Per-event AutoMap
Section titled “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).ToValue(false) .Set(m => m.ClosedAt).ToEventContextProperty(c => c.Occurred));3. Children AutoMap (Inherits by default)
Section titled “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
Section titled “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 propertiesWhen DepartmentCreated has properties like DepartmentName and DepartmentCode, and the Employee read model has matching properties, they are automatically mapped.
AutoMap in child joins
Section titled “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 tooCombining AutoMap with explicit mappings
Section titled “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).ToValue("Active") .Set(m => m.Balance).ToValue(0m) .Set(m => m.CreatedAt).ToEventContextProperty(c => c.Occurred)) .From<MoneyDeposited>(); // Uses only AutoMapIn this example:
AccountOpeneduses AutoMap for matching properties, plus explicit mappings forStatus,Balance, andCreatedAtMoneyDepositedrelies entirely on AutoMap
AutoMap with nested children
Section titled “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
Section titled “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
Section titled “Best practices”- Consistent naming: Use consistent property names across events and read models to maximize AutoMap effectiveness
- Combine approaches: Use AutoMap for simple mappings and explicit
Set()calls for complex transformations - Be explicit when needed: If clarity matters more than brevity, use explicit mappings even when AutoMap would work
- Document custom logic: When mixing AutoMap with explicit mappings, document why specific properties need custom handling
- Nested structures: Ensure nested types also follow consistent naming for recursive AutoMap to work effectively
Performance considerations
Section titled “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
Section titled “Examples”Simple AutoMap
Section titled “Simple AutoMap”public class ProductProjection : IProjectionFor<Product>{ public void Define(IProjectionBuilderFor<Product> builder) => builder .AutoMap() .From<ProductCreated>() .From<ProductUpdated>();}AutoMap with children
Section titled “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
Section titled “AutoMap with explicit overrides”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}Selective AutoMap for children only
Section titled “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)));}