Convention-Based Mapping
Convention-based model-bound mapping lets a client say: “this read model is projected from this event, and matching property names should map automatically.” It is the model-bound counterpart to AutoMap in a declarative projection.
Use it when event properties and read model properties share names and compatible shapes. Add explicit mappings only for the properties that need a different source name, a context value, or a different operation.
How It Works
Section titled “How It Works”Convention-based mapping follows the same projection rules as AutoMap:
- The event and read model property names must match.
- The property types must be compatible.
- Nested objects can be mapped when their internal property names match.
- Collections can be mapped when the element shape is compatible.
- Properties that do not exist on the event are skipped for that event.
The client syntax differs, but the Chronicle projection definition still contains the event type, key expression, read model, and AutoMap behavior.
Basic Convention Mapping
Section titled “Basic Convention Mapping”Apply the client’s model-bound “from event” marker at the read model level. Matching properties are then mapped without per-property set mappings.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Keys;using Cratis.Chronicle.Projections.ModelBound;
[EventType]public record ConventionUserRegistered( string Name, string Email, DateTimeOffset RegisteredAt);
[FromEvent<ConventionUserRegistered>]public record ConventionUser( [Key] Guid Id, string Name, string Email, DateTimeOffset RegisteredAt);import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.readModels.ReadModel
@EventType(id = "convention-user-registered")data class ConventionUserRegistered( val name: String, val email: String, val registeredAt: String)
@ReadModel@FromEvent(ConventionUserRegistered::class)data class ConventionUser( val name: String = "", val email: String = "", val registeredAt: String = "")import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.readModels.ReadModel;
@EventType(id = "convention-user-registered")record ConventionUserRegistered(String name, String email, String registeredAt) {}
@ReadModel@FromEvent(eventType = ConventionUserRegistered.class)class ConventionUser { public String name = ""; public String email = ""; public String registeredAt = "";}defmodule MyApp.Events.ConventionUserRegistered do use Chronicle.Events.EventType, id: "convention-user-registered-v1"
defstruct [:name, :email, :registered_at]end
defmodule MyApp.ReadModels.ConventionUser do use Chronicle.ReadModels.ReadModel
alias MyApp.Events.ConventionUserRegistered
defstruct [:name, :email, :registered_at]
# No `set:` list — matching field names are mapped automatically. from ConventionUserRegisteredendimport { eventType, fromEvent, readModel } from '@cratis/chronicle';
@eventType()class ConventionUserRegistered { constructor( readonly name: string, readonly email: string, readonly registeredAt: Date ) {}}
@readModel()@fromEvent(ConventionUserRegistered)class ConventionUser { name = ''; email = ''; registeredAt = new Date();}This is equivalent to writing explicit set mappings for every property:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Keys;using Cratis.Chronicle.Projections.ModelBound;
[EventType]public record ExplicitConventionUserRegistered( string Name, string Email, DateTimeOffset RegisteredAt);
public record ExplicitConventionUser( [Key] Guid Id,
[SetFrom<ExplicitConventionUserRegistered>(nameof(ExplicitConventionUserRegistered.Name))] string Name,
[SetFrom<ExplicitConventionUserRegistered>(nameof(ExplicitConventionUserRegistered.Email))] string Email,
[SetFrom<ExplicitConventionUserRegistered>(nameof(ExplicitConventionUserRegistered.RegisteredAt))] DateTimeOffset RegisteredAt);import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.projections.SetFromimport io.cratis.chronicle.readModels.ReadModel
@EventType(id = "explicit-convention-user-registered")data class ExplicitConventionUserRegistered( val name: String, val email: String, val registeredAt: String)
@ReadModel@FromEvent(ExplicitConventionUserRegistered::class)data class ExplicitConventionUser( @SetFrom("name") val name: String = "",
@SetFrom("email") val email: String = "",
@SetFrom("registeredAt") val registeredAt: String = "")import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.projections.SetFrom;import io.cratis.chronicle.readModels.ReadModel;
@EventType(id = "explicit-convention-user-registered")record ExplicitConventionUserRegistered(String name, String email, String registeredAt) {}
@ReadModel@FromEvent(eventType = ExplicitConventionUserRegistered.class)class ExplicitConventionUser { @SetFrom(propertyPath = "name") public String name = "";
@SetFrom(propertyPath = "email") public String email = "";
@SetFrom(propertyPath = "registeredAt") public String registeredAt = "";}defmodule MyApp.Events.ExplicitConventionUserRegistered do use Chronicle.Events.EventType, id: "explicit-convention-user-registered-v1"
defstruct [:name, :email, :registered_at]end
defmodule MyApp.ReadModels.ExplicitConventionUser do use Chronicle.ReadModels.ReadModel
alias MyApp.Events.ExplicitConventionUserRegistered
defstruct [:name, :email, :registered_at]
from ExplicitConventionUserRegistered, set: [name: :name, email: :email, registered_at: :registered_at]endimport { eventType, fromEvent, readModel, setFrom } from '@cratis/chronicle';
@eventType()class ExplicitConventionUserRegistered { constructor( readonly name: string, readonly email: string, readonly registeredAt: Date ) {}}
@readModel()@fromEvent(ExplicitConventionUserRegistered)class ExplicitConventionUser { @setFrom(ExplicitConventionUserRegistered, 'name') name = '';
@setFrom(ExplicitConventionUserRegistered, 'email') email = '';
@setFrom(ExplicitConventionUserRegistered, 'registeredAt') registeredAt = new Date();}Multiple Events
Section titled “Multiple Events”A read model can use convention mapping from more than one event type. Each event updates the matching properties it contains and leaves the rest unchanged.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Keys;using Cratis.Chronicle.Projections.ModelBound;
[EventType]public record ConventionUserProfileCreated(string Name, string Email);
[EventType]public record ConventionUserProfileUpdated(string Name, string Email, string Phone);
[FromEvent<ConventionUserProfileCreated>][FromEvent<ConventionUserProfileUpdated>]public record ConventionUserProfile( [Key] Guid Id, string Name, string Email, string Phone);import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.readModels.ReadModel
@EventType(id = "convention-user-profile-created")data class ConventionUserProfileCreated( val name: String, val email: String)
@EventType(id = "convention-user-profile-updated")data class ConventionUserProfileUpdated( val name: String, val email: String, val phone: String)
@ReadModel@FromEvent(ConventionUserProfileCreated::class)@FromEvent(ConventionUserProfileUpdated::class)data class ConventionUserProfile( val name: String = "", val email: String = "", val phone: String = "")import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.readModels.ReadModel;
@EventType(id = "convention-user-profile-created")record ConventionUserProfileCreated(String name, String email) {}
@EventType(id = "convention-user-profile-updated")record ConventionUserProfileUpdated(String name, String email, String phone) {}
@ReadModel@FromEvent(eventType = ConventionUserProfileCreated.class)@FromEvent(eventType = ConventionUserProfileUpdated.class)class ConventionUserProfile { public String name = ""; public String email = ""; public String phone = "";}defmodule MyApp.Events.ConventionUserProfileCreated do use Chronicle.Events.EventType, id: "convention-user-profile-created-v1"
defstruct [:name, :email]end
defmodule MyApp.Events.ConventionUserProfileUpdated do use Chronicle.Events.EventType, id: "convention-user-profile-updated-v1"
defstruct [:name, :email, :phone]end
defmodule MyApp.ReadModels.ConventionUserProfile do use Chronicle.ReadModels.ReadModel
alias MyApp.Events.{ConventionUserProfileCreated, ConventionUserProfileUpdated}
defstruct [:name, :email, :phone]
from ConventionUserProfileCreated from ConventionUserProfileUpdatedendimport { eventType, fromEvent, readModel } from '@cratis/chronicle';
@eventType()class ConventionUserProfileCreated { constructor(readonly name: string, readonly email: string) {}}
@eventType()class ConventionUserProfileUpdated { constructor( readonly name: string, readonly email: string, readonly phone: string ) {}}
@readModel()@fromEvent(ConventionUserProfileCreated)@fromEvent(ConventionUserProfileUpdated)class ConventionUserProfile { name = ''; email = ''; phone = '';}Custom Keys
Section titled “Custom Keys”By default, convention-based mappings use the event source id as the read model key. Use a custom key when the event carries the identifier of the read model instance in its content.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Keys;using Cratis.Chronicle.Projections.ModelBound;
[EventType]public record ConventionUserRegisteredWithKey( Guid UserId, string Name, string Email);
[FromEvent<ConventionUserRegisteredWithKey>(key: nameof(ConventionUserRegisteredWithKey.UserId))]public record ConventionUserById( [Key] Guid Id, string Name, string Email);import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.readModels.ReadModel
@EventType(id = "convention-user-registered-with-key")data class ConventionUserRegisteredWithKey( val userId: String, val name: String, val email: String)
@ReadModel@FromEvent(ConventionUserRegisteredWithKey::class, key = "userId")data class ConventionUserById( val name: String = "", val email: String = "")import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.readModels.ReadModel;
@EventType(id = "convention-user-registered-with-key")record ConventionUserRegisteredWithKey(String userId, String name, String email) {}
@ReadModel@FromEvent(eventType = ConventionUserRegisteredWithKey.class, key = "userId")class ConventionUserById { public String name = ""; public String email = "";}defmodule MyApp.Events.ConventionUserRegisteredWithKey do use Chronicle.Events.EventType, id: "convention-user-registered-with-key-v1"
defstruct [:user_id, :name, :email]end
defmodule MyApp.ReadModels.ConventionUserById do use Chronicle.ReadModels.ReadModel
alias MyApp.Events.ConventionUserRegisteredWithKey
defstruct [:name, :email]
from ConventionUserRegisteredWithKey, key: :user_idendimport { eventType, fromEvent, readModel } from '@cratis/chronicle';
@eventType()class ConventionUserRegisteredWithKey { constructor( readonly userId: string, readonly name: string, readonly email: string ) {}}
@readModel()@fromEvent(ConventionUserRegisteredWithKey, { key: 'userId' })class ConventionUserById { name = ''; email = '';}Custom keys are useful when:
- the event source id is not the read model id
- one event source can update multiple read model instances
- an event from one source updates a cross-source or cross-aggregate view
Relationship To Declarative AutoMap
Section titled “Relationship To Declarative AutoMap”Convention-based model-bound mapping and declarative AutoMap describe the same mapping behavior through different APIs.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Keys;using Cratis.Chronicle.Projections;using Cratis.Chronicle.Projections.ModelBound;
[EventType]public record ConventionEquivalentUserRegistered(string Name, string Email);
[FromEvent<ConventionEquivalentUserRegistered>]public record ConventionEquivalentUser( [Key] Guid Id, string Name, string Email);
public class ConventionEquivalentProjection : IProjectionFor<ConventionEquivalentUser>{ public void Define(IProjectionBuilderFor<ConventionEquivalentUser> builder) => builder.From<ConventionEquivalentUserRegistered>();}import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.projections.IProjectionBuilderForimport io.cratis.chronicle.projections.IProjectionForimport io.cratis.chronicle.readModels.ReadModel
@EventType(id = "convention-equivalent-user-registered")data class ConventionEquivalentUserRegistered( val name: String, val email: String)
@ReadModel@FromEvent(ConventionEquivalentUserRegistered::class)data class ConventionEquivalentUser( val name: String = "", val email: String = "")
class ConventionEquivalentProjection : IProjectionFor<ConventionEquivalentUser> { override fun define(builder: IProjectionBuilderFor<ConventionEquivalentUser>) { builder.from(ConventionEquivalentUserRegistered::class) }}import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.projections.IProjectionBuilderFor;import io.cratis.chronicle.projections.IProjectionFor;import io.cratis.chronicle.readModels.ReadModel;import kotlin.jvm.JvmClassMappingKt;
@EventType(id = "convention-equivalent-user-registered")record ConventionEquivalentUserRegistered(String name, String email) {}
@ReadModel@FromEvent(eventType = ConventionEquivalentUserRegistered.class)class ConventionEquivalentUser { public String name = ""; public String email = "";}
class ConventionEquivalentProjection implements IProjectionFor<ConventionEquivalentUser> { @Override public void define(IProjectionBuilderFor<ConventionEquivalentUser> builder) { builder.from(JvmClassMappingKt.getKotlinClass(ConventionEquivalentUserRegistered.class), null); }}defmodule MyApp.Events.ConventionEquivalentUserRegistered do use Chronicle.Events.EventType, id: "convention-equivalent-user-registered-v1"
defstruct [:name, :email]end
defmodule MyApp.ReadModels.ConventionEquivalentUser do use Chronicle.ReadModels.ReadModel
defstruct [:name, :email]end
defmodule MyApp.Projections.ConventionEquivalentProjection do use Chronicle.Projections.Projection, model: MyApp.ReadModels.ConventionEquivalentUser
alias MyApp.Events.ConventionEquivalentUserRegistered
# No `set:` list — matching field names are mapped automatically, the same # way the model-bound `from` declaration on the read model itself would. from ConventionEquivalentUserRegisteredendimport { eventType, fromEvent, IProjectionBuilderFor, IProjectionFor, projection, readModel } from '@cratis/chronicle';
@eventType()class ConventionEquivalentUserRegistered { constructor(readonly name: string, readonly email: string) {}}
@readModel()@fromEvent(ConventionEquivalentUserRegistered)class ConventionEquivalentUser { name = ''; email = '';}
@projection('', ConventionEquivalentUser)class ConventionEquivalentProjection implements IProjectionFor<ConventionEquivalentUser> { define(builder: IProjectionBuilderFor<ConventionEquivalentUser>): void { builder.from(ConventionEquivalentUserRegistered); }}Use the model-bound form when the mapping belongs naturally on the read model. Use a declarative projection when you need joins, richer key selection, or a projection definition that should stay separate from the read model shape.
Matching Structures
Section titled “Matching Structures”Convention mapping also works for nested values and collections when the names and shapes match.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Keys;using Cratis.Chronicle.Projections.ModelBound;
public record ConventionAddress(string Street, string City, string PostalCode);
public record ConventionLineItem(string ProductName, decimal UnitPrice, int Quantity);
[EventType]public record ConventionCustomerRegistered( string FirstName, string LastName, ConventionAddress BillingAddress, ConventionAddress ShippingAddress);
[EventType]public record ConventionOrderCreated( string CustomerEmail, ConventionLineItem[] Items, string[] Tags);
[FromEvent<ConventionCustomerRegistered>]public record ConventionCustomer( [Key] Guid Id, string FirstName, string LastName, ConventionAddress BillingAddress, ConventionAddress ShippingAddress);
[FromEvent<ConventionOrderCreated>]public record ConventionOrder( [Key] Guid Id, string CustomerEmail, ConventionLineItem[] Items, string[] Tags);import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.readModels.ReadModel
data class ConventionAddress( val street: String = "", val city: String = "", val postalCode: String = "")
data class ConventionLineItem( val productName: String = "", val unitPrice: Double = 0.0, val quantity: Int = 0)
@EventType(id = "convention-customer-registered")data class ConventionCustomerRegistered( val firstName: String, val lastName: String, val billingAddress: ConventionAddress, val shippingAddress: ConventionAddress)
@EventType(id = "convention-order-created")data class ConventionOrderCreated( val customerEmail: String, val items: List<ConventionLineItem>, val tags: List<String>)
@ReadModel@FromEvent(ConventionCustomerRegistered::class)data class ConventionCustomer( val firstName: String = "", val lastName: String = "", val billingAddress: ConventionAddress = ConventionAddress(), val shippingAddress: ConventionAddress = ConventionAddress())
@ReadModel@FromEvent(ConventionOrderCreated::class)data class ConventionOrder( val customerEmail: String = "", val items: List<ConventionLineItem> = emptyList(), val tags: List<String> = emptyList())import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.readModels.ReadModel;import java.util.List;
record ConventionAddress(String street, String city, String postalCode) {}
record ConventionLineItem(String productName, double unitPrice, int quantity) {}
@EventType(id = "convention-customer-registered")record ConventionCustomerRegistered( String firstName, String lastName, ConventionAddress billingAddress, ConventionAddress shippingAddress) {}
@EventType(id = "convention-order-created")record ConventionOrderCreated(String customerEmail, List<ConventionLineItem> items, List<String> tags) {}
@ReadModel@FromEvent(eventType = ConventionCustomerRegistered.class)class ConventionCustomer { public String firstName = ""; public String lastName = ""; public ConventionAddress billingAddress; public ConventionAddress shippingAddress;}
@ReadModel@FromEvent(eventType = ConventionOrderCreated.class)class ConventionOrder { public String customerEmail = ""; public List<ConventionLineItem> items = List.of(); public List<String> tags = List.of();}defmodule MyApp.Events.ConventionCustomerRegistered do use Chronicle.Events.EventType, id: "convention-customer-registered-v1"
defstruct [:first_name, :last_name, :billing_address, :shipping_address]end
defmodule MyApp.Events.ConventionOrderCreated do use Chronicle.Events.EventType, id: "convention-order-created-v1"
defstruct [:customer_email, :items, :tags]end
defmodule MyApp.ReadModels.ConventionCustomer do use Chronicle.ReadModels.ReadModel
alias MyApp.Events.ConventionCustomerRegistered
# Nested maps and lists are plain Elixir terms — matching field names on the # event are copied across as-is, structure and all. defstruct [:first_name, :last_name, :billing_address, :shipping_address]
from ConventionCustomerRegisteredend
defmodule MyApp.ReadModels.ConventionOrder do use Chronicle.ReadModels.ReadModel
alias MyApp.Events.ConventionOrderCreated
defstruct [:customer_email, :items, :tags]
from ConventionOrderCreatedendimport { eventType, fromEvent, readModel } from '@cratis/chronicle';
class ConventionAddress { street = ''; city = ''; postalCode = '';}
class ConventionLineItem { productName = ''; unitPrice = 0; quantity = 0;}
@eventType()class ConventionCustomerRegistered { constructor( readonly firstName: string, readonly lastName: string, readonly billingAddress: ConventionAddress, readonly shippingAddress: ConventionAddress ) {}}
@eventType()class ConventionOrderCreated { constructor( readonly customerEmail: string, readonly items: ConventionLineItem[], readonly tags: string[] ) {}}
@readModel()@fromEvent(ConventionCustomerRegistered)class ConventionCustomer { firstName = ''; lastName = ''; billingAddress = new ConventionAddress(); shippingAddress = new ConventionAddress();}
@readModel()@fromEvent(ConventionOrderCreated)class ConventionOrder { customerEmail = ''; items: ConventionLineItem[] = []; tags: string[] = [];}Partial Events
Section titled “Partial Events”Events do not need to contain every read model property. Chronicle maps the properties that exist on each event.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Keys;using Cratis.Chronicle.Projections.ModelBound;
[EventType]public record ConventionPartialUserRegistered(string Email);
[EventType]public record ConventionPartialUserCompleted( string FirstName, string LastName, string Phone);
[FromEvent<ConventionPartialUserRegistered>][FromEvent<ConventionPartialUserCompleted>]public record ConventionPartialUser( [Key] Guid Id, string Email, string FirstName, string LastName, string Phone);import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.readModels.ReadModel
@EventType(id = "convention-partial-user-registered")data class ConventionPartialUserRegistered( val email: String)
@EventType(id = "convention-partial-user-completed")data class ConventionPartialUserCompleted( val firstName: String, val lastName: String, val phone: String)
@ReadModel@FromEvent(ConventionPartialUserRegistered::class)@FromEvent(ConventionPartialUserCompleted::class)data class ConventionPartialUser( val email: String = "", val firstName: String = "", val lastName: String = "", val phone: String = "")import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.readModels.ReadModel;
@EventType(id = "convention-partial-user-registered")record ConventionPartialUserRegistered(String email) {}
@EventType(id = "convention-partial-user-completed")record ConventionPartialUserCompleted(String firstName, String lastName, String phone) {}
@ReadModel@FromEvent(eventType = ConventionPartialUserRegistered.class)@FromEvent(eventType = ConventionPartialUserCompleted.class)class ConventionPartialUser { public String email = ""; public String firstName = ""; public String lastName = ""; public String phone = "";}defmodule MyApp.Events.ConventionPartialUserRegistered do use Chronicle.Events.EventType, id: "convention-partial-user-registered-v1"
defstruct [:email]end
defmodule MyApp.Events.ConventionPartialUserCompleted do use Chronicle.Events.EventType, id: "convention-partial-user-completed-v1"
defstruct [:first_name, :last_name, :phone]end
defmodule MyApp.ReadModels.ConventionPartialUser do use Chronicle.ReadModels.ReadModel
alias MyApp.Events.{ConventionPartialUserRegistered, ConventionPartialUserCompleted}
defstruct [:email, :first_name, :last_name, :phone]
from ConventionPartialUserRegistered from ConventionPartialUserCompletedendimport { eventType, fromEvent, readModel } from '@cratis/chronicle';
@eventType()class ConventionPartialUserRegistered { constructor(readonly email: string) {}}
@eventType()class ConventionPartialUserCompleted { constructor( readonly firstName: string, readonly lastName: string, readonly phone: string ) {}}
@readModel()@fromEvent(ConventionPartialUserRegistered)@fromEvent(ConventionPartialUserCompleted)class ConventionPartialUser { email = ''; firstName = ''; lastName = ''; phone = '';}Excluding A Property From Mapping
Section titled “Excluding A Property From Mapping”Convention mapping applies to every event the read model subscribes to — including an event pulled in only to [Count], [Increment], or [Join]. If such an event carries a property whose name matches one you set explicitly, convention mapping would otherwise overwrite your value with the unrelated event’s. Chronicle handles the common form of this automatically and gives you an attribute for the rest.
Aggregate-Only Events
Section titled “Aggregate-Only Events”An event a read model subscribes to only to aggregate — [Count], [Increment], [Decrement], [Add], or [Subtract] — does not contribute its other properties to convention mapping. Counting an event does not copy its unrelated fields onto the read model, so a same-named property on it cannot overwrite an explicit value. No annotation is needed.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Keys;using Cratis.Chronicle.Projections.ModelBound;
[EventType]public record AggOnlyArrangementSet(string Location);
[EventType]public record AggOnlyCandidateSubmitted(string Name, string Location);
[FromEvent<AggOnlyArrangementSet>]public record AggOnlyAssignmentSummary( [Key] Guid Id,
// AggOnlyCandidateSubmitted is subscribed only to be counted, so its identically named // Location is not auto-mapped over the value sourced from AggOnlyArrangementSet. [SetFrom<AggOnlyArrangementSet>(nameof(AggOnlyArrangementSet.Location))] string Location,
[Count<AggOnlyCandidateSubmitted>] int CandidateCount);[NoAutoMap] On A Property
Section titled “[NoAutoMap] On A Property”For the collisions Chronicle cannot infer — a value-mapped ([SetFrom]) or [Join]ed event that carries an identically named property — apply [NoAutoMap] to the read model property (or record parameter). That property is then set only from its explicit source; convention mapping never touches it, while every other property keeps mapping.
using Cratis.Chronicle.Events;using Cratis.Chronicle.Keys;using Cratis.Chronicle.Projections;using Cratis.Chronicle.Projections.ModelBound;
[EventType]public record NoAutoMapWorkArrangementSet(string Location, int WorkMode);
[EventType]public record NoAutoMapCandidateSubmitted(string Name, string Location);
[FromEvent<NoAutoMapWorkArrangementSet>]public record NoAutoMapAssignmentSummary( [Key] Guid Id,
// Location is sourced only from NoAutoMapWorkArrangementSet. NoAutoMapCandidateSubmitted is // value-mapped (for CandidateName) and also carries a Location; [NoAutoMap] stops that Location // from being auto-mapped over the explicit value, while every other property keeps mapping. [SetFrom<NoAutoMapWorkArrangementSet>(nameof(NoAutoMapWorkArrangementSet.Location))] [NoAutoMap] string Location,
[SetFrom<NoAutoMapCandidateSubmitted>(nameof(NoAutoMapCandidateSubmitted.Name))] string CandidateName);[NoAutoMap] also works at the read model (class) level to disable convention mapping for the whole read model. Use the property form when you only need to protect a single property and want convention mapping everywhere else.
When To Use It
Section titled “When To Use It”Use convention-based mapping when:
- the event and read model use consistent names
- most properties are simple set mappings
- the read model is easiest to understand next to its event mappings
- you want to keep explicit attributes or decorators for the exceptions only
Use explicit mappings when:
- names differ between the event and read model
- the property comes from event context
- the property needs add, subtract, count, or another operation
- the mapping needs a custom key for only one event
- being explicit is clearer than relying on naming
Convention-based mapping is evaluated when the projection definition is built. Event processing uses the compiled projection definition, so there is no per-event reflection penalty compared with explicit set mappings.