Skip to content

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.

When event and read model property names match, declare the events and let AutoMap fill the matching properties.

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

AutoMap applies the same convention to every event handled by the projection:

RuleBehavior
Property namesEvent and read model property names must match after the client’s serialization naming policy is applied.
Property typesValues must be assignable to the read model property type.
Explicit mappingsExplicit mappings handle properties that do not match by convention.
ScopeA 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 when a projection should map only the properties you name explicitly.

Disable AutoMap
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));
}

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.

Re-enable AutoMap for children
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)));
}

AutoMap and explicit mappings can be used together. Let convention handle the properties that match, and map the exceptional properties directly.

AutoMap with explicit mappings
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>();
}

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.

AutoMap with a join
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));
}

An event a projection subscribes to only to aggregateCount, 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.

Aggregating an event does not map its other properties
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));
}

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.

Child and nested projections can have their own AutoMap behavior in clients that support those builder scopes. Use the dedicated pages for those shapes: