--- name: add-projection description: Use this skill when asked to add a Chronicle projection to a Cratis-based project. Favor model-bound projections by default, and only fall back to declarative/fluent `IProjectionFor` projections when model-bound attributes cannot express the behavior cleanly. Enforces the AutoMap-first rule and Chronicle-specific join semantics. --- Add a Chronicle **projection** that populates a read model from events. > For **reactors** (automation / translation), see the `add-reactor` skill instead. ## Projection — Model-Bound (preferred) Put projection metadata directly on the read model using attributes. No separate class needed. ```csharp [ReadModel] [FromEvent] // auto-maps all matching property names public record ( [Key] Id, // marks the primary key ) // auto-mapped from SomeEventHappened { public static ISubject>> All(IMongoCollection<> collection) => collection.Observe(); } ``` **Attribute reference:** | Attribute | Purpose | |-----------|---------| | `[FromEvent]` | Maps event `T` onto the read model; matching property names map automatically (AutoMap is on by default — never call `.AutoMap()`) | | `[FromEvent(key: nameof(T.Prop))]` | Same, but uses `Prop` as the read model key instead of EventSourceId | | `[Key]` | Marks the primary key property | | `[SetFrom(nameof(T.Prop))]` | Explicitly maps one property from event T | | `[AddFrom(nameof(T.Prop))]` | Adds event property value to the read model property | | `[SubtractFrom(nameof(T.Prop))]` | Subtracts event property value | | `[ChildrenFrom(key: nameof(T.Prop))]` | Projects into a nested child collection | | `[Join(on: nameof(Prop), eventPropertyName: nameof(T.EProp))]` | Joins data from a related event | | `[RemovedWith]` | Marks the instance as removed when event T is appended | **Critical rules:** - Joins must be on Chronicle **events** — NEVER join on a read model type - If property names between event and read model match, `[FromEvent]` alone is sufficient - Child types also support all attributes recursively ## Projection — Fluent / declarative (fallback for complex cases) Use `IProjectionFor` only when the projection logic is too complex for model-bound attributes or would become less clear if forced into attributes. ```csharp public class Projection : IProjectionFor<> { public void Define(IProjectionBuilderFor<> builder) => builder .From(b => b.UsingKey(e => e.SomeId)) .RemovedWith(); } ``` **Critical rules:** - AutoMap is on by default — just call `.From<>()` directly. Only call `.AutoMap()` if you previously used `.NoAutoMap()`. - Joins are on Chronicle **events** only — NEVER join on the read model - There is NO `Identifier` / `ProjectionId` property — do not add one ## Advanced patterns & startup-crash gotchas - **`[Nested]`** projects a single child object onto a nested type. Put `[FromEvent]` on the **nested type** (or use property-level `[SetFrom]` on the parent when the parent already declares the event). `[NoAutoMap]` and explicit `[SetFrom]` work inside the nested type; `[Nested]` can recurse inside a `[ChildrenFrom]` item. - ⚠️ **Duplicate-`[FromEvent]` crash:** declaring class-level `[FromEvent]` (no `key:`) on **both** the parent and a nested/child type for the **same** event throws a `Key: +N` duplicate-key exception at startup. Fix: keep `[FromEvent]` on the nested type only, or switch the nested type to property-level `[SetFrom]`. - ⚠️ **Duplicate-`[SetFromContext]` crash:** two properties with `[SetFromContext]` on the same read model crash at startup (`Key: +1`). Merge them or use `[FromEvery]`. - **`[FromAll]` vs `[FromEvery]`:** `[FromAll]` (class-level) subscribes to **every** event type system-wide (audit/log models — pair with `[NotRewindable]`). `[FromEvery]` is a property-level capture across the events the model **already** declares via `[FromEvent]` (e.g. to stamp `EventContext` data) — it does *not* subscribe to new event types. - **Constant-key counters:** `[Count(ConstantKey="metrics")]` / `[Increment(ConstantKey=...)]` route all matching events into **one** aggregating document at the constant key (distinct from `.UsingConstantKey("...")` on the fluent builder). - **Children with different key names:** when child events use different key properties, use the fluent form — `.From(b => b.UsingKey(e => e.Email)).From(b => b.UsingKey(e => e.OriginalEmail))` — which model-bound `[ChildrenFrom]` (single key) can't express. - **Source selection:** class-level `[EventSequence("name")]`, `[EventLog]`, or `[EventStore("name")]` choose where the projection reads from. ⚠️ `[FromEventSequence]` is **removed** — use `[EventSequence("name")]`. - **Cross-stream specs:** in `ReadModelScenario`, seed each contributing stream with its own `Given.ForEventSource(...)`. Don't pre-emptively `[Fact(Skip=...)]` a cross-stream assertion — only skip on a reproduced harness gap, with the reason in the skip message. ## After creating Run `dotnet build`. Fix all errors before completing. ## Appended event metadata and filtering Chronicle correlates appended metadata in two different ways: - **Projections** select input through event types, joins, and event sequence configuration - **Reducers and reactors** can additionally filter by appended tags, event source type, and event stream type Use append metadata like this: ```csharp await eventLog.Append( EventSourceId.New(), new OrderPlaced(42m), eventStreamType: "fulfillment", eventSourceType: "order", tags: ["priority"]); ``` If you need metadata-based filtering for downstream processing, pair the projection with a reducer or reactor using `[FilterEventsByTag]`, `[EventSourceType]`, or `[EventStreamType]`. Projection `[Tag]` and `[Tags]` attributes label the projection definition; they do not filter appended events. For examples, see `Documentation/events/filtering/`. --- For the full model-bound projection attribute reference and fluent builder API, see [references/CHRONICLE-API.md](references/CHRONICLE-API.md).