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
@EventTypedata class AutoMapUserCreated(val name: String, val email: String)
@EventTypedata 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) }}import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.IProjectionBuilderFor;import io.cratis.chronicle.projections.IProjectionFor;
@EventTyperecord AutoMapUserCreated(String name, String email) {}
@EventTyperecord AutoMapUserRenamed(String name) {}
class AutoMapUser { public String name = ""; public String email = "";}
class AutoMapUserProjection implements IProjectionFor<AutoMapUser> { @Override public void define(IProjectionBuilderFor<AutoMapUser> builder) { builder.from(AutoMapUserCreated.class); builder.from(AutoMapUserRenamed.class); }}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));}import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.IProjectionBuilderForimport io.cratis.chronicle.projections.IProjectionFor
@EventTypedata class AutoMapDisabledAccountRegistered(val accountName: String, val contactEmail: String)
data class AutoMapDisabledAccount( val name: String = "", val email: String = "", val createdAt: String = "")
class AutoMapDisabledAccountProjection : IProjectionFor<AutoMapDisabledAccount> { override fun define(builder: IProjectionBuilderFor<AutoMapDisabledAccount>) { builder .noAutoMap() .from(AutoMapDisabledAccountRegistered::class) { it.set(AutoMapDisabledAccount::name).toProperty("accountName") it.set(AutoMapDisabledAccount::email).toProperty("contactEmail") it.set(AutoMapDisabledAccount::createdAt).toEventContextProperty("occurred") } }}import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.IProjectionBuilderFor;import io.cratis.chronicle.projections.IProjectionFor;
@EventTyperecord AutoMapDisabledAccountRegistered(String accountName, String contactEmail) {}
class AutoMapDisabledAccount { public String name = ""; public String email = ""; public String createdAt = "";}
class AutoMapDisabledAccountProjection implements IProjectionFor<AutoMapDisabledAccount> { @Override public void define(IProjectionBuilderFor<AutoMapDisabledAccount> builder) { builder .noAutoMap() .from(AutoMapDisabledAccountRegistered.class, fb -> { fb.set("name").toProperty("accountName"); fb.set("email").toProperty("contactEmail"); fb.set("createdAt").toEventContextProperty("occurred"); }); }}defmodule MyApp.Events.AutoMapDisabledAccountRegistered do use Chronicle.Events.EventType, id: "auto-map-disabled-account-registered"
defstruct [:account_name, :contact_email]end
defmodule MyApp.ReadModels.AutoMapDisabledAccount do use Chronicle.ReadModels.ReadModel
defstruct name: nil, email: nil, created_at: nil
# Nothing is mapped by name - every property has to be stated. no_auto_map()
from MyApp.Events.AutoMapDisabledAccountRegistered, set: [ name: :account_name, email: :contact_email, created_at: :occurred ]endimport { 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)));}Kotlin does not support this workflow yet.`IProjectionBuilderFor` has no AutoMap toggle at all — no `noAutoMap()`/`autoMap()` method on thebuilder or on `IChildrenBuilderFor`, so there is no child scope to re-enable AutoMap on.Java does not support this workflow yet.`IProjectionBuilderFor` has no AutoMap toggle at all — no `noAutoMap()`/`autoMap()` method on thebuilder or on `IChildrenBuilderFor`, so there is no child scope to re-enable AutoMap on.Elixir does not support this workflow yet.import { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle';
@eventType()class AutoMapTeamFormed { constructor(readonly teamName: string) {}}
@eventType()class AutoMapMemberJoinedTeam { constructor(readonly memberId: string, readonly displayName: string) {}}
class AutoMapTeamMember { memberId = ''; displayName = '';}
class AutoMapTeam { name = ''; createdAt = new Date(); members: AutoMapTeamMember[] = [];}
@projection()class AutoMapTeamProjection implements IProjectionFor<AutoMapTeam> { define(builder: IProjectionBuilderFor<AutoMapTeam>): void { builder .noAutoMap() .from(AutoMapTeamFormed, _ => _ .set(m => m.name).to(e => e.teamName) .set(m => m.createdAt).toEventContextProperty('occurred')) .children<AutoMapTeamMember>(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>();}import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.IProjectionBuilderForimport io.cratis.chronicle.projections.IProjectionFor
@EventTypedata class AutoMapAccountOpened(val name: String, val email: String)
@EventTypedata class AutoMapAccountEmailChanged(val email: String)
data class AutoMapAccount( val name: String = "", val email: String = "", val status: String = "")
class AutoMapAccountProjection : IProjectionFor<AutoMapAccount> { override fun define(builder: IProjectionBuilderFor<AutoMapAccount>) { builder .from(AutoMapAccountOpened::class) { it.set(AutoMapAccount::status).to { "Active" } } .from(AutoMapAccountEmailChanged::class) }}import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.IProjectionBuilderFor;import io.cratis.chronicle.projections.IProjectionFor;
@EventTyperecord AutoMapAccountOpened(String name, String email) {}
@EventTyperecord AutoMapAccountEmailChanged(String email) {}
class AutoMapAccount { public String name = ""; public String email = ""; public String status = "";}
class AutoMapAccountProjection implements IProjectionFor<AutoMapAccount> { @Override public void define(IProjectionBuilderFor<AutoMapAccount> builder) { builder .from(AutoMapAccountOpened.class, fb -> { fb.<String>set("status").to(e -> "Active"); }) .from(AutoMapAccountEmailChanged.class); }}defmodule MyApp.Events.AutoMapAccountOpened do use Chronicle.Events.EventType, id: "auto-map-account-opened"
defstruct [:name, :email]end
defmodule MyApp.Events.AutoMapAccountEmailChanged do use Chronicle.Events.EventType, id: "auto-map-account-email-changed"
defstruct [:email]end
defmodule MyApp.ReadModels.AutoMapAccount do use Chronicle.ReadModels.ReadModel
defstruct [:name, :email, :status, :created_at]end
defmodule MyApp.Projections.AutoMapAccountProjection do use Chronicle.Projections.Projection, model: MyApp.ReadModels.AutoMapAccount
alias MyApp.Events.{AutoMapAccountEmailChanged, AutoMapAccountOpened}
# `name` and `email` are mapped automatically by AutoMap; only the # exceptions below need an explicit set:. from AutoMapAccountOpened, set: [ status: "$value(Active)", created_at: :occurred ]
# Uses AutoMap for `email` — no set: list needed. from AutoMapAccountEmailChangedendimport { 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));}import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.IProjectionBuilderForimport io.cratis.chronicle.projections.IProjectionFor
@EventTypedata class AutoMapEmployeeHired(val employeeName: String, val departmentId: String)
@EventTypedata class AutoMapDepartmentRenamed(val departmentName: String)
data class AutoMapEmployee( val employeeName: String = "", val departmentId: String = "", val departmentName: String = "")
class AutoMapEmployeeProjection : IProjectionFor<AutoMapEmployee> { override fun define(builder: IProjectionBuilderFor<AutoMapEmployee>) { builder .from(AutoMapEmployeeHired::class) .join(AutoMapDepartmentRenamed::class) { it.on(AutoMapEmployee::departmentId) } }}import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.IProjectionBuilderFor;import io.cratis.chronicle.projections.IProjectionFor;
@EventTyperecord AutoMapEmployeeHired(String employeeName, String departmentId) {}
@EventTyperecord AutoMapDepartmentRenamed(String departmentName) {}
class AutoMapEmployee { public String employeeName = ""; public String departmentId = ""; public String departmentName = "";}
class AutoMapEmployeeProjection implements IProjectionFor<AutoMapEmployee> { @Override public void define(IProjectionBuilderFor<AutoMapEmployee> builder) { builder .from(AutoMapEmployeeHired.class) .join(AutoMapDepartmentRenamed.class, jb -> { jb.on("departmentId"); }); }}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));}import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.IProjectionBuilderForimport io.cratis.chronicle.projections.IProjectionFor
@EventTypedata class DeclAggArrangementSet(val location: String)
@EventTypedata class DeclAggCandidateSubmitted(val name: String, val location: String)
data class DeclAggAssignmentSummary(val location: String = "", val candidateCount: Int = 0)
class DeclAggAssignmentProjection : IProjectionFor<DeclAggAssignmentSummary> { override fun define(builder: IProjectionBuilderFor<DeclAggAssignmentSummary>) { builder .from(DeclAggArrangementSet::class) .from(DeclAggCandidateSubmitted::class) { it.count(DeclAggAssignmentSummary::candidateCount) } }}import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.IProjectionBuilderFor;import io.cratis.chronicle.projections.IProjectionFor;
@EventTyperecord DeclAggArrangementSet(String location) {}
@EventTyperecord DeclAggCandidateSubmitted(String name, String location) {}
class DeclAggAssignmentSummary { public String location = ""; public int candidateCount = 0;}
class DeclAggAssignmentProjection implements IProjectionFor<DeclAggAssignmentSummary> { @Override public void define(IProjectionBuilderFor<DeclAggAssignmentSummary> builder) { builder .from(DeclAggArrangementSet.class) .from(DeclAggCandidateSubmitted.class, fb -> { fb.count("candidateCount"); }); }}defmodule MyApp.Events.DeclAggArrangementSet do use Chronicle.Events.EventType, id: "decl-agg-arrangement-set-v1"
defstruct [:location]end
defmodule MyApp.Events.DeclAggCandidateSubmitted do use Chronicle.Events.EventType, id: "decl-agg-candidate-submitted-v1"
defstruct [:name, :location]end
defmodule MyApp.ReadModels.DeclAggAssignmentSummary do use Chronicle.ReadModels.ReadModel
defstruct [:location, candidate_count: 0]end
defmodule MyApp.Projections.DeclAggAssignmentProjection do use Chronicle.Projections.Projection, model: MyApp.ReadModels.DeclAggAssignmentSummary
alias MyApp.Events.{DeclAggArrangementSet, DeclAggCandidateSubmitted}
from DeclAggArrangementSet
# DeclAggCandidateSubmitted is subscribed only to be counted, so its own # `location` field is never mapped over the value set from # DeclAggArrangementSet. from DeclAggCandidateSubmitted, count: :candidate_countendimport { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle';
@eventType()class DeclAggArrangementSet { constructor(readonly location: string) {}}
@eventType()class DeclAggCandidateSubmitted { constructor(readonly name: string, readonly location: string) {}}
class DeclAggAssignmentSummary { location = ''; candidateCount = 0;}
@projection()class DeclAggAssignmentProjection implements IProjectionFor<DeclAggAssignmentSummary> { define(builder: IProjectionBuilderFor<DeclAggAssignmentSummary>): void { 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: