Skip to content

CHR0040: Several [SetFromContext] for the same event type on one member

One read model member carries more than one [SetFromContext<TEvent>] for the same event type. They write the same property in the projection definition, so only the last declared is kept — the earlier capture is silently discarded, with no build, registration, or runtime signal.

Allowing multiple [SetFromContext] on one member is deliberate and load-bearing: a property capturing context from several different event types is exactly what the attribute is for, and that use never triggers this rule. Two for the same event type is different — a single value cannot hold two context values under any configuration, naming policy, or event shape.

Keep the capture that was intended, or move the other onto its own property.

Every spelling of “one member” is covered. On a positional record the constructor parameter and the property generated from it are two distinct symbols — [property:] is the only way to reach the second — and both write the same key into the same event type’s definition. A duplicate split across the two spellings is reported exactly as one placed twice on a single symbol.

Warning

using Cratis.Chronicle.Events;
using Cratis.Chronicle.Keys;
using Cratis.Chronicle.Projections.ModelBound;
[EventType]
public record Chr0040OrderPlaced(decimal Amount);
// Warning CHR0040: 'PlacedAt' carries more than one [SetFromContext<Chr0040OrderPlaced>],
// and they map to the same property - only the last declared is kept. The CorrelationId
// capture is silently dropped: a single member cannot hold both context values. Move it
// onto its own property instead.
[FromEvent<Chr0040OrderPlaced>]
public record Chr0040Order(
[Key] Guid Id,
decimal Amount,
[SetFromContext<Chr0040OrderPlaced>(nameof(EventContext.CorrelationId))]
[SetFromContext<Chr0040OrderPlaced>(nameof(EventContext.Occurred))]
DateTimeOffset PlacedAt);

The failure is maximally quiet. The property is populated — with the other context value — so nothing is null, nothing throws, and a spec asserting the property has a value stays green. It reads as a projection written wrong, and the natural way to debug it — re-reading the attributes and finding both present and correctly spelled — confirms the wrong conclusion.

This is a warning where the neighboring AutoMap collision rule (CHR0025) deliberately only informs, and the difference is the point: that collision has two legitimate resolutions and only the author knows which was meant, while here there is no second reading to defer to.