CHR0025: Explicitly sourced read model property may be overwritten by AutoMap
Rule Description
Section titled “Rule Description”AutoMap runs per event across every event a model-bound projection references — it does not defer to your explicit setters. A property set with [SetFrom<A>] (or [SetValue], [AddFrom], [SubtractFrom], [SetFromContext], [Join]) can therefore be overwritten when another referenced event B carries a property of the same name, because AutoMap writes B’s value on top of the value you mapped explicitly.
This is reported as information, not a warning: the collision has two legitimate resolutions and only you know which was intended.
Severity
Section titled “Severity”Info
Example
Section titled “Example”using Cratis.Chronicle.Events;using Cratis.Chronicle.Keys;using Cratis.Chronicle.Projections;using Cratis.Chronicle.Projections.ModelBound;
[FromEvent<Chr0025Opened>]public record Chr0025Account( [Key] Guid Id,
// Info CHR0025: Location is set explicitly from Chr0025Opened, but Chr0025WorkModeSet — also // referenced by this projection (for WorkMode) — carries a Location that AutoMap writes on top of it. [SetFrom<Chr0025Opened>] string Location,
[SetFrom<Chr0025WorkModeSet>] string WorkMode);
[EventType]public record Chr0025Opened(string Location);
[EventType]public record Chr0025WorkModeSet(string WorkMode, string Location);Resolving
Section titled “Resolving”There are two valid resolutions, depending on intent:
-
The other event should not overwrite it — fence the property with property-level
[NoAutoMap]so only your explicit setter writes it:[SetFrom<Chr0025Opened>][NoAutoMap]string Location, -
Updates from the other event are intended — for example a later event legitimately updating the value by name-match (the create-then-rename pattern). In that case leave it as is; no change is needed.
Because either can be correct, CHR0025 is informational and offers no code fix — it must not push you toward one of two valid answers.
Why This Rule Exists
Section titled “Why This Rule Exists”In a model-bound projection, Chronicle wires each referenced event’s properties onto identically named read-model properties (AutoMap). It does this for every referenced event, and it does not know that a property is “owned” by an explicit [SetFrom]. So when two referenced events both carry a Location, the one that runs last wins — and that can silently be the event you did not mean, producing an actively wrong value rather than a missing one.
The escape hatch is property-level [NoAutoMap], which is deliberately opt-in: leaving AutoMap on is exactly what enables the common create-then-rename pattern, where a later event updates a property by name. A blanket “explicit beats implicit” rule was tried and reverted for breaking that pattern.
An event referenced only through an aggregate ([Count], [Increment], [Decrement]) does not auto-map its other properties, so it cannot collide and is not flagged. Name matching compares the C# member names case-insensitively and does not account for explicit [SetFrom<T>("eventProperty")] targets or a custom naming policy, so the diagnostic is an approximation.
Related Rules
Section titled “Related Rules”- CHR0030 — the mirror image: a
[ChildrenFrom]child collection that auto-maps to nothing. - Model-bound projections — the full reference.