Projection AutoMap
AutoMap maps event properties to read model properties when their names and types are compatible. It is the default behavior for declarative projections, so the usual projection only needs to say which events it consumes.
Use explicit mapping when the event shape and read model shape intentionally differ. Use the AutoMap controls when a projection should opt out of convention-based mapping for part of its definition and then opt back in later.
Basic Usage
Section titled “Basic Usage”When event and read model property names match, declare the events and let AutoMap fill the matching properties.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Projections;
[EventType]public record AutoMapUserCreated(string Name, string Email);
[EventType]public record AutoMapUserRenamed(string Name);
public record AutoMapUser(string Name, string Email);
public class AutoMapUserProjection : IProjectionFor<AutoMapUser>{ public void Define(IProjectionBuilderFor<AutoMapUser> builder) => builder .From<AutoMapUserCreated>() .From<AutoMapUserRenamed>();}import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.IProjectionBuilderForimport io.cratis.chronicle.projections.IProjectionFor
@EventType(id = "auto-map-user-created")data class AutoMapUserCreated(val name: String, val email: String)
@EventType(id = "auto-map-user-renamed")data class AutoMapUserRenamed(val name: String)
data class AutoMapUser(val name: String = "", val email: String = "")
class AutoMapUserProjection : IProjectionFor<AutoMapUser> { override fun define(builder: IProjectionBuilderFor<AutoMapUser>) { builder .from(AutoMapUserCreated::class) .from(AutoMapUserRenamed::class) }}Java does not support this workflow yet.defmodule MyApp.Events.AutoMapUserCreated do use Chronicle.Events.EventType, id: "auto-map-user-created"
defstruct [:name, :email]end
defmodule MyApp.Events.AutoMapUserRenamed do use Chronicle.Events.EventType, id: "auto-map-user-renamed"
defstruct [:name]end
defmodule MyApp.ReadModels.AutoMapUser do use Chronicle.ReadModels.ReadModel
defstruct [:name, :email]end
defmodule MyApp.Projections.AutoMapUserProjection do use Chronicle.Projections.Projection, model: MyApp.ReadModels.AutoMapUser
alias MyApp.Events.{AutoMapUserCreated, AutoMapUserRenamed}
from AutoMapUserCreated from AutoMapUserRenamedendimport { eventType, IProjectionBuilderFor, IProjectionFor, projection, readModel } from '@cratis/chronicle';
@eventType()class AutoMapUserCreated { constructor(readonly name: string, readonly email: string) {}}
@eventType()class AutoMapUserRenamed { constructor(readonly name: string) {}}
@readModel()class AutoMapUser { name = ''; email = '';}
@projection('', AutoMapUser)class AutoMapUserProjection implements IProjectionFor<AutoMapUser> { define(builder: IProjectionBuilderFor<AutoMapUser>): void { builder .from(AutoMapUserCreated) .from(AutoMapUserRenamed); }}How AutoMap Works
Section titled “How AutoMap Works”AutoMap applies the same convention to every event handled by the projection:
| Rule | Behavior |
|---|---|
| Property names | Event and read model property names must match after the client’s serialization naming policy is applied. |
| Property types | Values must be assignable to the read model property type. |
| Explicit mappings | Explicit mappings handle properties that do not match by convention. |
| Scope | A projection can disable or re-enable AutoMap for the builder scope that the client supports. |
AutoMap is evaluated when the projection definition is built. It does not add a per-event reflection step while projections process events.
Disable AutoMap
Section titled “Disable AutoMap”Disable AutoMap when a projection should map only the properties you name explicitly.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Projections;
[EventType]public record AutoMapDisabledAccountRegistered(string AccountName, string ContactEmail);
public record AutoMapDisabledAccount( string Name, string Email, DateTimeOffset CreatedAt);
public class AutoMapDisabledAccountProjection : IProjectionFor<AutoMapDisabledAccount>{ public void Define(IProjectionBuilderFor<AutoMapDisabledAccount> builder) => builder .NoAutoMap() .From<AutoMapDisabledAccountRegistered>(_ => _ .Set(m => m.Name).To(e => e.AccountName) .Set(m => m.Email).To(e => e.ContactEmail) .Set(m => m.CreatedAt).ToEventContextProperty(c => c.Occurred));}Kotlin does not support this workflow yet.Java does not support this workflow yet.Elixir does not support this workflow yet.import { eventType, IProjectionBuilderFor, IProjectionFor, projection, readModel } from '@cratis/chronicle';
@eventType()class AutoMapDisabledAccountRegistered { constructor(readonly accountName: string, readonly contactEmail: string) {}}
@readModel()class AutoMapDisabledAccount { name = ''; email = ''; createdAt = new Date(0);}
@projection('', AutoMapDisabledAccount)class AutoMapDisabledAccountProjection implements IProjectionFor<AutoMapDisabledAccount> { define(builder: IProjectionBuilderFor<AutoMapDisabledAccount>): void { builder .noAutoMap() .from(AutoMapDisabledAccountRegistered, _ => _ .set(m => m.name).to(e => e.accountName) .set(m => m.email).to(e => e.contactEmail) .set(m => m.createdAt).toEventContextProperty('occurred')); }}Re-enable AutoMap In A Child Scope
Section titled “Re-enable AutoMap In A Child Scope”Calling the AutoMap enable method is redundant at the top level. It is useful in a child builder scope when the parent projection has disabled AutoMap and the child scope should opt back in.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Projections;
[EventType]public record AutoMapTeamFormed(string TeamName);
[EventType]public record AutoMapMemberJoinedTeam(string MemberId, string DisplayName);
public record AutoMapTeamMember(string MemberId, string DisplayName);
public record AutoMapTeam( string Name, DateTimeOffset CreatedAt, IEnumerable<AutoMapTeamMember> Members);
public class AutoMapTeamProjection : IProjectionFor<AutoMapTeam>{ public void Define(IProjectionBuilderFor<AutoMapTeam> builder) => builder .NoAutoMap() .From<AutoMapTeamFormed>(_ => _ .Set(m => m.Name).To(e => e.TeamName) .Set(m => m.CreatedAt).ToEventContextProperty(c => c.Occurred)) .Children(m => m.Members, children => children .IdentifiedBy(m => m.MemberId) .AutoMap() .From<AutoMapMemberJoinedTeam>(_ => _ .UsingKey(e => e.MemberId)));}Combine With Explicit Mappings
Section titled “Combine With Explicit Mappings”AutoMap and explicit mappings can be used together. Let convention handle the properties that match, and map the exceptional properties directly.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Projections;
[EventType]public record AutoMapAccountOpened(string Name, string Email);
[EventType]public record AutoMapAccountEmailChanged(string Email);
public record AutoMapAccount( string Name, string Email, string Status, DateTimeOffset CreatedAt);
public class AutoMapAccountProjection : IProjectionFor<AutoMapAccount>{ public void Define(IProjectionBuilderFor<AutoMapAccount> builder) => builder .From<AutoMapAccountOpened>(_ => _ .Set(m => m.Status).ToValue("Active") .Set(m => m.CreatedAt).ToEventContextProperty(c => c.Occurred)) .From<AutoMapAccountEmailChanged>();}Kotlin does not support this workflow yet.Java does not support this workflow yet.Elixir does not support this workflow yet.import { eventType, IProjectionBuilderFor, IProjectionFor, projection, readModel } from '@cratis/chronicle';
@eventType()class AutoMapAccountOpened { constructor(readonly name: string, readonly email: string) {}}
@eventType()class AutoMapAccountEmailChanged { constructor(readonly email: string) {}}
@readModel()class AutoMapAccount { name = ''; email = ''; status = ''; createdAt = new Date(0);}
@projection('', AutoMapAccount)class AutoMapAccountProjection implements IProjectionFor<AutoMapAccount> { define(builder: IProjectionBuilderFor<AutoMapAccount>): void { builder .from(AutoMapAccountOpened, _ => _ .set(m => m.status).toValue('Active') .set(m => m.createdAt).toEventContextProperty('occurred')) .from(AutoMapAccountEmailChanged); }}AutoMap With Joins
Section titled “AutoMap With Joins”Joined events can also contribute matching properties through AutoMap. Use the join condition to connect the joined event to the read model; matching joined-event properties can then flow into the model.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Projections;
[EventType]public record AutoMapEmployeeHired(string EmployeeName, string DepartmentId);
[EventType]public record AutoMapDepartmentRenamed(string DepartmentName);
public record AutoMapEmployee( string EmployeeName, string DepartmentId, string DepartmentName);
public class AutoMapEmployeeProjection : IProjectionFor<AutoMapEmployee>{ public void Define(IProjectionBuilderFor<AutoMapEmployee> builder) => builder .From<AutoMapEmployeeHired>() .Join<AutoMapDepartmentRenamed>(_ => _ .On(m => m.DepartmentId));}Kotlin does not support this workflow yet.Java does not support this workflow yet.defmodule MyApp.Events.AutoMapEmployeeHired do use Chronicle.Events.EventType, id: "auto-map-employee-hired"
defstruct [:employee_name, :department_id]end
defmodule MyApp.Events.AutoMapDepartmentRenamed do use Chronicle.Events.EventType, id: "auto-map-department-renamed"
defstruct [:department_name]end
defmodule MyApp.ReadModels.AutoMapEmployee do use Chronicle.ReadModels.ReadModel
defstruct [:employee_name, :department_id, :department_name]end
defmodule MyApp.Projections.AutoMapEmployeeProjection do use Chronicle.Projections.Projection, model: MyApp.ReadModels.AutoMapEmployee
alias MyApp.Events.{AutoMapEmployeeHired, AutoMapDepartmentRenamed}
from AutoMapEmployeeHired
join AutoMapDepartmentRenamed, on: :department_idendimport { eventType, IProjectionBuilderFor, IProjectionFor, projection, readModel } from '@cratis/chronicle';
@eventType()class AutoMapEmployeeHired { constructor(readonly employeeName: string, readonly departmentId: string) {}}
@eventType()class AutoMapDepartmentRenamed { constructor(readonly departmentName: string) {}}
@readModel()class AutoMapEmployee { employeeName = ''; departmentId = ''; departmentName = '';}
@projection('', AutoMapEmployee)class AutoMapEmployeeProjection implements IProjectionFor<AutoMapEmployee> { define(builder: IProjectionBuilderFor<AutoMapEmployee>): void { builder .from(AutoMapEmployeeHired) .join(AutoMapDepartmentRenamed, _ => _ .on(m => m.departmentId)); }}Aggregate-Only Events
Section titled “Aggregate-Only Events”An event a projection subscribes to only to aggregate — Count, Increment, Decrement, Add, or Subtract — does not contribute its other properties to AutoMap. Aggregating an event does not copy its unrelated fields onto the read model, so a property on it that happens to share a name with one you set from another event cannot overwrite that value. You do not need to disable AutoMap for this — it is the default behavior.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Projections;
[EventType]public record DeclAggArrangementSet(string Location);
[EventType]public record DeclAggCandidateSubmitted(string Name, string Location);
public record DeclAggAssignmentSummary(string Location, int CandidateCount);
public class DeclAggAssignmentProjection : IProjectionFor<DeclAggAssignmentSummary>{ public void Define(IProjectionBuilderFor<DeclAggAssignmentSummary> builder) => builder .From<DeclAggArrangementSet>() .From<DeclAggCandidateSubmitted>(_ => _ .Count(m => m.CandidateCount));}When To Use AutoMap
Section titled “When To Use AutoMap”Use AutoMap when:
- Event property names match read model property names.
- The property types are directly compatible.
- The projection follows stable naming conventions.
- The read model should reflect the event shape without transformation.
Use explicit mappings when:
- Property names differ.
- A constant, event context field, or event source id should be mapped.
- The projection increments, decrements, counts, or combines values.
- You want a projection definition that documents every mapped property.
Related Projection Shapes
Section titled “Related Projection Shapes”Child and nested projections can have their own AutoMap behavior in clients that support those builder scopes. Use the dedicated pages for those shapes: