@eventmodelers/cli 1.0.72 → 1.0.74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -8
- package/RELEASE_NOTES.md +5 -0
- package/cli.js +119 -28
- package/package.json +2 -2
- package/shared/demo-slices/Understanding Eventsourcing/additem/slice.json +176 -0
- package/shared/demo-slices/Understanding Eventsourcing/archiveitem/slice.json +209 -0
- package/shared/demo-slices/Understanding Eventsourcing/cartitems/slice.json +236 -0
- package/shared/demo-slices/Understanding Eventsourcing/cartpublicationfailed/slice.json +65 -0
- package/shared/demo-slices/Understanding Eventsourcing/cartpublished/slice.json +65 -0
- package/shared/demo-slices/Understanding Eventsourcing/cartswithproducts/slice.json +105 -0
- package/shared/demo-slices/Understanding Eventsourcing/changedprices/slice.json +91 -0
- package/shared/demo-slices/Understanding Eventsourcing/changeinventory/slice.json +191 -0
- package/shared/demo-slices/Understanding Eventsourcing/changeprice/slice.json +220 -0
- package/shared/demo-slices/Understanding Eventsourcing/clearcart/slice.json +219 -0
- package/shared/demo-slices/Understanding Eventsourcing/config.json +2631 -0
- package/shared/demo-slices/Understanding Eventsourcing/context.json +3 -0
- package/shared/demo-slices/Understanding Eventsourcing/index.json +132 -0
- package/shared/demo-slices/Understanding Eventsourcing/inventories/slice.json +132 -0
- package/shared/demo-slices/Understanding Eventsourcing/itemadded/slice.json +137 -0
- package/shared/demo-slices/Understanding Eventsourcing/publishcart/slice.json +230 -0
- package/shared/demo-slices/Understanding Eventsourcing/removeitem/slice.json +239 -0
- package/shared/demo-slices/Understanding Eventsourcing/submitcart/slice.json +199 -0
- package/shared/demo-slices/Understanding Eventsourcing/submittedcartdata/slice.json +90 -0
- package/shared/demo-slices/current_context.json +3 -0
- package/stacks/blank/templates/.claude/skills/build-automation/SKILL.md +1 -1
- package/stacks/blank/templates/.claude/skills/build-state-change/SKILL.md +1 -1
- package/stacks/blank/templates/.claude/skills/build-state-view/SKILL.md +1 -1
- package/stacks/blank/templates/root/README.md +1 -1
- package/stacks/modeling-kit/templates/kit/CLAUDE-STANDALONE.md +8 -6
- package/stacks/modeling-kit/templates/kit/CLAUDE.md +12 -7
- package/stacks/modeling-kit/templates/kit/README.md +1 -1
- package/stacks/react/templates/.claude/skills/build-automation/SKILL.md +1 -1
- package/stacks/react/templates/.claude/skills/build-state-change/SKILL.md +1 -1
- package/stacks/react/templates/.claude/skills/build-state-view/SKILL.md +1 -1
- package/stacks/cratis-csharp/templates/.claude/skills/_shared/cratis-conventions.md +0 -251
- package/stacks/cratis-csharp/templates/.claude/skills/build-automation/SKILL.md +0 -122
- package/stacks/cratis-csharp/templates/.claude/skills/build-automation/references/patterns.md +0 -115
- package/stacks/cratis-csharp/templates/.claude/skills/build-state-change/SKILL.md +0 -191
- package/stacks/cratis-csharp/templates/.claude/skills/build-state-change/references/patterns.md +0 -234
- package/stacks/cratis-csharp/templates/.claude/skills/build-state-view/SKILL.md +0 -149
- package/stacks/cratis-csharp/templates/.claude/skills/build-state-view/references/patterns.md +0 -166
- package/stacks/cratis-csharp/templates/build-kit/CLAUDE.md +0 -78
- package/stacks/cratis-csharp/templates/build-kit/lib/AGENT.md +0 -59
- package/stacks/cratis-csharp/templates/build-kit/lib/backend-prompt.md +0 -140
- package/stacks/cratis-csharp/templates/build-kit/lib/prompt.md +0 -126
- package/stacks/cratis-csharp/templates/root/.frontend/index.css +0 -29
- package/stacks/cratis-csharp/templates/root/.frontend/index.html +0 -17
- package/stacks/cratis-csharp/templates/root/.frontend/main.tsx +0 -18
- package/stacks/cratis-csharp/templates/root/.frontend/tsconfig.json +0 -42
- package/stacks/cratis-csharp/templates/root/.frontend/tsconfig.node.json +0 -11
- package/stacks/cratis-csharp/templates/root/.frontend/vite.config.ts +0 -56
- package/stacks/cratis-csharp/templates/root/App.tsx +0 -23
- package/stacks/cratis-csharp/templates/root/CratisApp.csproj +0 -25
- package/stacks/cratis-csharp/templates/root/CratisApp.sln +0 -18
- package/stacks/cratis-csharp/templates/root/GlobalUsings.cs +0 -3
- package/stacks/cratis-csharp/templates/root/Home.tsx +0 -102
- package/stacks/cratis-csharp/templates/root/Program.cs +0 -26
- package/stacks/cratis-csharp/templates/root/README.md +0 -192
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/AllListings.ts +0 -47
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/Listing.cs +0 -11
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/Listing.ts +0 -12
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/ListingDataTable.tsx +0 -17
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/index.ts +0 -1
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/Register.ts +0 -51
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/RegisterDialog.tsx +0 -18
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/Registration.cs +0 -27
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/index.ts +0 -1
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/SomeFeature.tsx +0 -22
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/SomeName.cs +0 -3
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/index.ts +0 -1
- package/stacks/cratis-csharp/templates/root/appsettings.Development.json +0 -9
- package/stacks/cratis-csharp/templates/root/appsettings.json +0 -26
- package/stacks/cratis-csharp/templates/root/docker-compose.yml +0 -23
- package/stacks/cratis-csharp/templates/root/package.json +0 -33
- package/stacks/cratis-csharp/templates/root/tsconfig.json +0 -3
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: build-state-change
|
|
3
|
-
description: >
|
|
4
|
-
Implement Event Sourcing write slices the Cratis way — using Cratis Arc (CQRS) + Cratis Chronicle
|
|
5
|
-
(event sourcing) in a .NET / C# project. A write slice is: Command → Handle() → Event(s), with
|
|
6
|
-
optional validators, constraints, and DCB business rules. Use when: (1) implementing a new write
|
|
7
|
-
slice / command in a Cratis project, (2) a slice.json has a non-empty commands[] / events[] section,
|
|
8
|
-
(3) the user provides an Event Modeling artifact, specification, or natural-language description of a
|
|
9
|
-
command and asks to implement it, (4) the user says "implement", "create", "add" a write slice,
|
|
10
|
-
command, or state change in a Cratis Arc / Chronicle project.
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
# Cratis — Write Slice (State Change)
|
|
14
|
-
|
|
15
|
-
A write slice mutates state by recording events. In Cratis Arc the whole path lives in **one `.cs`
|
|
16
|
-
file**:
|
|
17
|
-
|
|
18
|
-
```
|
|
19
|
-
[Command] record + Handle() → validator/constraint → [EventType] record(s) → dotnet build → specs
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
> **Read first:** [../_shared/cratis-conventions.md](../_shared/cratis-conventions.md) — the
|
|
23
|
-
> non-negotiable Cratis rules. Everything below assumes them.
|
|
24
|
-
|
|
25
|
-
## Step 0 — Discover the target project's conventions
|
|
26
|
-
|
|
27
|
-
Before writing code, read the project's `.build-kit/CLAUDE.md` and **at least one existing slice** (the starter
|
|
28
|
-
ships one under `SomeModule/SomeFeature/`). Confirm:
|
|
29
|
-
|
|
30
|
-
- The namespace root (read the `.csproj` `<RootNamespace>` and existing `.cs` files — never
|
|
31
|
-
hard-code it; the namespace mirrors the folders and drops any `.Features.` segment).
|
|
32
|
-
- How existing commands return results (single event / tuple / `Result<,>` / `void`).
|
|
33
|
-
- How `ConceptAs<T>` identity types are declared and where they live.
|
|
34
|
-
- Whether existing `.cs` files use a file header (the starter uses none).
|
|
35
|
-
|
|
36
|
-
> **Comments & description:** each slice element carries a `comments: string[]` array and a
|
|
37
|
-
> `description`. Use them as implementation hints. When done, resolve each used comment:
|
|
38
|
-
> `POST <BASE_URL>/api/org/<ORG_ID>/boards/<BOARD_ID>/nodes/<nodeId>/comments/<commentId>/resolve`
|
|
39
|
-
> (get IDs first via GET on the same path).
|
|
40
|
-
|
|
41
|
-
## Step 1 — Understand the input (`slice.json` is the source of truth)
|
|
42
|
-
|
|
43
|
-
Extract, regardless of input format:
|
|
44
|
-
|
|
45
|
-
| Element | What to extract |
|
|
46
|
-
|---|---|
|
|
47
|
-
| **Command** | Name (imperative), fields, which field is the event source / `[Key]` |
|
|
48
|
-
| **Events** | Names (past tense), fields, which events this command appends |
|
|
49
|
-
| **Business rules** | Preconditions, invariants, idempotency — from `description` / `comments` only |
|
|
50
|
-
| **State needed for rules** | Which read model must be inspected (DCB) to evaluate a rule |
|
|
51
|
-
| **Specifications** | Each GWT / scenario maps 1:1 to an executable spec |
|
|
52
|
-
| **Storylines** (optional) | `storylines[]` — ordered walkthrough "beats"; a `COMMAND → EVENT` transition in one is a supplementary spec source, see Step 5a |
|
|
53
|
-
|
|
54
|
-
**If a field is not in `slice.json`, it does not go in the code.** If requirements are unclear, ask
|
|
55
|
-
the user before proceeding.
|
|
56
|
-
|
|
57
|
-
### Determine the trigger
|
|
58
|
-
If unclear how the command is dispatched, ask:
|
|
59
|
-
> - **UI / REST** — exposed automatically by Arc; add a React component + integration spec.
|
|
60
|
-
> - **Automation only** — dispatched internally by a reactor (no UI). The command still exists; no `.tsx`.
|
|
61
|
-
|
|
62
|
-
## Step 2 — Create concept types (if needed)
|
|
63
|
-
|
|
64
|
-
For any new identity / value, add one `ConceptAs<T>` per file in the feature folder. Use the canonical
|
|
65
|
-
Guid-identity pattern (with `NotSet`, `New()`, and the `EventSourceId` conversion) — see the shared
|
|
66
|
-
conventions doc and [references/patterns.md](references/patterns.md).
|
|
67
|
-
|
|
68
|
-
## Step 3 — Write the slice `.cs` file
|
|
69
|
-
|
|
70
|
-
`<Module>/<Feature>/<Slice>/<Slice>.cs` — ALL backend artifacts in this one file; namespace
|
|
71
|
-
`<Root>.<Module>.<Feature>.<Slice>` (no file header unless the project uses one).
|
|
72
|
-
|
|
73
|
-
### Events first
|
|
74
|
-
```csharp
|
|
75
|
-
[EventType] // NEVER any arguments
|
|
76
|
-
public record AuthorRegistered(AuthorName Name);
|
|
77
|
-
```
|
|
78
|
-
Past tense, no nullable properties, one purpose each. If the context's events already exist elsewhere,
|
|
79
|
-
reuse them — don't redefine.
|
|
80
|
-
|
|
81
|
-
### Command with `Handle()` on the record
|
|
82
|
-
```csharp
|
|
83
|
-
[Command]
|
|
84
|
-
public record RegisterAuthor(AuthorName Name)
|
|
85
|
-
{
|
|
86
|
-
public AuthorRegistered Handle() => new(Name); // Arc appends the returned event
|
|
87
|
-
}
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
Pick the return shape that matches the slice:
|
|
91
|
-
- **single event** → `EventName Handle()`
|
|
92
|
-
- **generate + return a new id** → `(NewId, EventName) Handle()` (Arc returns the id as
|
|
93
|
-
`CommandResult<T>.response`)
|
|
94
|
-
- **multiple events** → `IEnumerable<object> Handle() => [ new A(...), new B(...) ];`
|
|
95
|
-
- **success/error** → `Result<TSuccess, TError> Handle()`
|
|
96
|
-
- **side-effect only** → `void Handle()`
|
|
97
|
-
|
|
98
|
-
Event source: a `[Key]` parameter, an `EventSourceId`-convertible concept, or `ICanProvideEventSourceId`.
|
|
99
|
-
|
|
100
|
-
### Business rules (DCB) — inject the read model
|
|
101
|
-
When a rule depends on event-sourced state, take the read model as a `Handle()` parameter; Arc injects
|
|
102
|
-
current state. Throw / return an error for violations:
|
|
103
|
-
```csharp
|
|
104
|
-
[Command]
|
|
105
|
-
public record RegisterAuthor(AuthorName Name)
|
|
106
|
-
{
|
|
107
|
-
public Result<AuthorRegistered, RegistrationError> Handle(AuthorByName existing) =>
|
|
108
|
-
existing is not null
|
|
109
|
-
? RegistrationError.NameAlreadyTaken
|
|
110
|
-
: new AuthorRegistered(Name);
|
|
111
|
-
}
|
|
112
|
-
```
|
|
113
|
-
Only encode rules that appear in the slice `description` / `comments`. See
|
|
114
|
-
[references/patterns.md](references/patterns.md) for the full DCB and constraint patterns.
|
|
115
|
-
|
|
116
|
-
### Validation (optional but recommended)
|
|
117
|
-
```csharp
|
|
118
|
-
public class RegisterAuthorValidator : CommandValidator<RegisterAuthor>
|
|
119
|
-
{
|
|
120
|
-
public RegisterAuthorValidator() =>
|
|
121
|
-
RuleFor(c => c.Name).NotEmpty().WithMessage("Name is required").MaximumLength(100);
|
|
122
|
-
}
|
|
123
|
-
```
|
|
124
|
-
Extend `CommandValidator<T>` — auto-discovered, no registration. One `RuleFor` per validation in the slice.
|
|
125
|
-
|
|
126
|
-
## Step 4 — Build
|
|
127
|
-
|
|
128
|
-
From the project root: `dotnet build`. Fix ALL warnings and errors before continuing (warnings = errors).
|
|
129
|
-
This also regenerates the TypeScript proxies the frontend depends on.
|
|
130
|
-
|
|
131
|
-
## Step 5 — Write specs (one per scenario in `slice.json`)
|
|
132
|
-
|
|
133
|
-
Put integration specs in `<Module>/<Feature>/<Slice>/when_<behavior>/and_<scenario>.cs`. Cover, from the
|
|
134
|
-
slice's specifications:
|
|
135
|
-
- **Happy path** — command succeeds, correct event appended.
|
|
136
|
-
- **Each validation failure** — one spec per rule.
|
|
137
|
-
- **Each DCB business-rule violation** — one spec per read-model condition in `Handle()`.
|
|
138
|
-
- **Each constraint violation.**
|
|
139
|
-
|
|
140
|
-
Use the Cratis BDD pattern (`Specification`, `Establish` / `Because` / `[Fact] should_*`). For
|
|
141
|
-
Chronicle integration specs use `Given<context>` + `ChronicleOutOfProcessFixture` and assert with
|
|
142
|
-
`Context.ShouldHaveAppendedEvent<T>(...)`. See [references/patterns.md](references/patterns.md).
|
|
143
|
-
|
|
144
|
-
Run `dotnet test --filter "FullyQualifiedName~<SliceName>"`. Fix all failures.
|
|
145
|
-
|
|
146
|
-
## Step 5a — Storyline-derived specs (optional)
|
|
147
|
-
|
|
148
|
-
If `storylines[]` is non-empty — a storyline embedded in this slice's slice.json already belongs
|
|
149
|
-
entirely to this slice, no need to match beats against `commands[]` by id/title — look for a
|
|
150
|
-
`COMMAND → EVENT` transition: a `type: "COMMAND"` beat, followed by its resulting event beat(s) —
|
|
151
|
-
possibly followed by a READMODEL beat after that.
|
|
152
|
-
|
|
153
|
-
- **Establish** — append every event from the storyline's start up to (not including) the command
|
|
154
|
-
beat.
|
|
155
|
-
- **Because** — `Handle()` the command, built from the beat's `fields`.
|
|
156
|
-
- **should_\*** — assert the expected event(s) (the beat(s) immediately following the command).
|
|
157
|
-
|
|
158
|
-
Only the `COMMAND → EVENT` half lives here — if the storyline continues `EVENT → READMODEL`, that
|
|
159
|
-
half is a separate spec in **build-state-view** (its Step 4a). Don't chase a single spec across
|
|
160
|
-
both a command dispatch and a read-model assertion unless the project already has an established
|
|
161
|
-
pattern doing that — Chronicle integration specs assert appended events, not read-model state,
|
|
162
|
-
and vice versa for projection specs.
|
|
163
|
-
|
|
164
|
-
Place these alongside the `specifications[]`-derived specs but in their own
|
|
165
|
-
`and_<storyline-title>.cs` file, so a reader can tell at a glance which specs are exhaustive
|
|
166
|
-
coverage and which are one narrated walkthrough.
|
|
167
|
-
|
|
168
|
-
Skip silently (no spec) when the transition isn't isolable — e.g. the command beat has no
|
|
169
|
-
traceable preceding events.
|
|
170
|
-
|
|
171
|
-
## Step 6 — Frontend (only if the command is UI-triggered)
|
|
172
|
-
|
|
173
|
-
After `dotnet build` generated the proxy (co-located next to the `.cs`), add
|
|
174
|
-
`<Module>/<Feature>/<Slice>/<Component>.tsx` importing the proxy from `./`, using `CommandDialog` /
|
|
175
|
-
inline form. Add a barrel `index.ts`. Register it in the feature's composition
|
|
176
|
-
page. See the shared conventions doc's React section and [references/patterns.md](references/patterns.md).
|
|
177
|
-
|
|
178
|
-
## Final verification — does the implementation match `slice.json`?
|
|
179
|
-
|
|
180
|
-
- [ ] Every `commands[]` field → a Command record property (no invented, none missing).
|
|
181
|
-
- [ ] Every `events[]` entry → an `[EventType]` record; names match exactly; fields match.
|
|
182
|
-
- [ ] Every specification / GWT scenario → an executable spec.
|
|
183
|
-
- [ ] No business rule in `Handle()` that is absent from the slice `description` / `comments`.
|
|
184
|
-
- [ ] A `storylines[]` `COMMAND → EVENT` transition for this command has a spec, or was
|
|
185
|
-
deliberately skipped (not silently ignored).
|
|
186
|
-
- [ ] `dotnet build` is clean (0 warnings / 0 errors); slice specs pass.
|
|
187
|
-
|
|
188
|
-
## References
|
|
189
|
-
- [references/patterns.md](references/patterns.md) — full command/event/validator/constraint/DCB code,
|
|
190
|
-
integration specs, and the React command UI patterns.
|
|
191
|
-
- [../_shared/cratis-conventions.md](../_shared/cratis-conventions.md) — the Cratis conventions.
|
package/stacks/cratis-csharp/templates/.claude/skills/build-state-change/references/patterns.md
DELETED
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
# Write Slice Patterns — Cratis Arc + Chronicle
|
|
2
|
-
|
|
3
|
-
Complete code for state-change (write) slices. All artifacts go in a single
|
|
4
|
-
`<Module>/<Feature>/<Slice>/<Slice>.cs` file (the shipped starter uses `SomeModule/SomeFeature/...` —
|
|
5
|
-
match whatever the existing slices use). The example slices use no file header (match the project).
|
|
6
|
-
Namespace mirrors the folders and drops any `.Features.`. The examples below use short illustrative
|
|
7
|
-
names (`MyApp.Authors.Registration`); mirror your project's actual module/feature layout instead.
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## 1. Concept (one per file, in the feature folder)
|
|
12
|
-
|
|
13
|
-
```csharp
|
|
14
|
-
// Features/Authors/AuthorId.cs
|
|
15
|
-
namespace MyApp.Authors;
|
|
16
|
-
|
|
17
|
-
public record AuthorId(Guid Value) : ConceptAs<Guid>(Value)
|
|
18
|
-
{
|
|
19
|
-
public static readonly AuthorId NotSet = new(Guid.Empty);
|
|
20
|
-
public static implicit operator Guid(AuthorId id) => id.Value;
|
|
21
|
-
public static implicit operator AuthorId(Guid value) => new(value);
|
|
22
|
-
public static implicit operator EventSourceId(AuthorId id) => new(id.Value.ToString());
|
|
23
|
-
public static AuthorId New() => new(Guid.NewGuid());
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Features/Authors/AuthorName.cs
|
|
27
|
-
public record AuthorName(string Value) : ConceptAs<string>(Value)
|
|
28
|
-
{
|
|
29
|
-
public static readonly AuthorName NotSet = new(string.Empty);
|
|
30
|
-
public static implicit operator string(AuthorName name) => name.Value;
|
|
31
|
-
public static implicit operator AuthorName(string value) => new(value);
|
|
32
|
-
}
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
---
|
|
36
|
-
|
|
37
|
-
## 2. Simplest write slice — single event
|
|
38
|
-
|
|
39
|
-
```csharp
|
|
40
|
-
// Features/Authors/Registration/Registration.cs
|
|
41
|
-
namespace MyApp.Authors.Registration;
|
|
42
|
-
|
|
43
|
-
using Cratis.Arc.Commands.ModelBound;
|
|
44
|
-
using Cratis.Chronicle.Events;
|
|
45
|
-
|
|
46
|
-
[Command]
|
|
47
|
-
public record RegisterAuthor([Key] AuthorId Id, AuthorName Name)
|
|
48
|
-
{
|
|
49
|
-
public AuthorRegistered Handle() => new(Name);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
[EventType]
|
|
53
|
-
public record AuthorRegistered(AuthorName Name);
|
|
54
|
-
|
|
55
|
-
public class RegisterAuthorValidator : CommandValidator<RegisterAuthor>
|
|
56
|
-
{
|
|
57
|
-
public RegisterAuthorValidator() =>
|
|
58
|
-
RuleFor(c => c.Name).NotEmpty().WithMessage("Name is required").MaximumLength(100);
|
|
59
|
-
}
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
The `[Key]` parameter is the event source id. With a concept that converts to `EventSourceId`, you may
|
|
63
|
-
omit `[Key]` and let Arc resolve it.
|
|
64
|
-
|
|
65
|
-
---
|
|
66
|
-
|
|
67
|
-
## 3. Generate-and-return a new id
|
|
68
|
-
|
|
69
|
-
```csharp
|
|
70
|
-
[Command]
|
|
71
|
-
public record RegisterAuthor(AuthorName Name)
|
|
72
|
-
{
|
|
73
|
-
public (AuthorId, AuthorRegistered) Handle()
|
|
74
|
-
{
|
|
75
|
-
var id = AuthorId.New();
|
|
76
|
-
return (id, new AuthorRegistered(Name)); // id flows back as CommandResult<AuthorId>.response
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
---
|
|
82
|
-
|
|
83
|
-
## 4. Multiple events
|
|
84
|
-
|
|
85
|
-
```csharp
|
|
86
|
-
[Command]
|
|
87
|
-
public record TransferFunds(AccountId FromId, AccountId ToId, decimal Amount)
|
|
88
|
-
{
|
|
89
|
-
public IEnumerable<object> Handle() =>
|
|
90
|
-
[
|
|
91
|
-
new FundsWithdrawn(FromId, Amount),
|
|
92
|
-
new FundsDeposited(ToId, Amount),
|
|
93
|
-
];
|
|
94
|
-
}
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
---
|
|
98
|
-
|
|
99
|
-
## 5. Injected dependency / constraint
|
|
100
|
-
|
|
101
|
-
`Handle()` parameters are DI-resolved. Use for external services or constraints (e.g. uniqueness):
|
|
102
|
-
|
|
103
|
-
```csharp
|
|
104
|
-
[Command]
|
|
105
|
-
public record OpenDebitAccount([Key] AccountId Id, string Name, OwnerId OwnerId)
|
|
106
|
-
{
|
|
107
|
-
public async Task<DebitAccountOpened> Handle(IUniqueAccountConstraint constraint)
|
|
108
|
-
{
|
|
109
|
-
await constraint.Validate(Name); // throws on violation
|
|
110
|
-
return new DebitAccountOpened(Name, OwnerId);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
---
|
|
116
|
-
|
|
117
|
-
## 6. Business rule via DCB (inject a read model)
|
|
118
|
-
|
|
119
|
-
When a rule depends on event-sourced state, accept the relevant read model as a `Handle()` parameter.
|
|
120
|
-
Arc materializes current state from the event log and injects it. Return a `Result<,>` for a clean
|
|
121
|
-
success/error contract:
|
|
122
|
-
|
|
123
|
-
```csharp
|
|
124
|
-
[Command]
|
|
125
|
-
public record ReserveBook([Key] BookId Id, MemberId Member)
|
|
126
|
-
{
|
|
127
|
-
public Result<BookReserved, ReservationError> Handle(BookAvailability availability)
|
|
128
|
-
{
|
|
129
|
-
if (availability is null || !availability.IsAvailable)
|
|
130
|
-
return ReservationError.NotAvailable;
|
|
131
|
-
return new BookReserved(Member);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
public enum ReservationError { NotAvailable }
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
Only encode rules that appear in the slice `description` / `comments`.
|
|
139
|
-
|
|
140
|
-
---
|
|
141
|
-
|
|
142
|
-
## 7. Integration spec (Chronicle, full stack)
|
|
143
|
-
|
|
144
|
-
`Features/Authors/Registration/when_registering/and_there_are_no_authors.cs`
|
|
145
|
-
|
|
146
|
-
```csharp
|
|
147
|
-
using context = MyApp.Authors.Registration.when_registering.and_there_are_no_authors.context;
|
|
148
|
-
|
|
149
|
-
namespace MyApp.Authors.Registration.when_registering;
|
|
150
|
-
|
|
151
|
-
[Collection(ChronicleCollection.Name)]
|
|
152
|
-
public class and_there_are_no_authors(context context) : Given<context>(context)
|
|
153
|
-
{
|
|
154
|
-
public class context(ChronicleOutOfProcessFixture fixture) : given.an_http_client(fixture)
|
|
155
|
-
{
|
|
156
|
-
public CommandResult<AuthorId>? Result;
|
|
157
|
-
|
|
158
|
-
async Task Because() =>
|
|
159
|
-
Result = await Client.ExecuteCommand<RegisterAuthor, AuthorId>(
|
|
160
|
-
"/api/authors/register",
|
|
161
|
-
new RegisterAuthor(AuthorId.New(), new AuthorName("John Doe")));
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
[Fact] void should_be_successful() => Context.Result!.IsSuccess.ShouldBeTrue();
|
|
165
|
-
[Fact] void should_have_appended_one_event() =>
|
|
166
|
-
Context.ShouldHaveTailSequenceNumber(EventSequenceNumber.First);
|
|
167
|
-
[Fact] void should_append_author_registered_event() =>
|
|
168
|
-
Context.ShouldHaveAppendedEvent<AuthorRegistered>(
|
|
169
|
-
EventSequenceNumber.First, Context.Result!.Response,
|
|
170
|
-
evt => evt.Name.Value.ShouldEqual("John Doe"));
|
|
171
|
-
}
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
### Pure unit spec on `Handle()` (no I/O)
|
|
175
|
-
|
|
176
|
-
```csharp
|
|
177
|
-
namespace MyApp.Authors.Registration.for_RegisterAuthor;
|
|
178
|
-
|
|
179
|
-
public class when_registering : Specification
|
|
180
|
-
{
|
|
181
|
-
RegisterAuthor _command;
|
|
182
|
-
AuthorRegistered _event;
|
|
183
|
-
|
|
184
|
-
void Establish() => _command = new(AuthorId.New(), new AuthorName("John"));
|
|
185
|
-
void Because() => _event = _command.Handle();
|
|
186
|
-
|
|
187
|
-
[Fact] void should_carry_the_name() => _event.Name.Value.ShouldEqual("John");
|
|
188
|
-
}
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
---
|
|
192
|
-
|
|
193
|
-
## 8. React command UI (after `dotnet build` generated the proxy)
|
|
194
|
-
|
|
195
|
-
The proxy (`RegisterAuthor.ts`) is generated **next to** `Registration.cs` (the starter's `.csproj`
|
|
196
|
-
sets `CratisProxiesUseSourceFileAsOutputFile=true`), so import it from the **same folder**. Add a barrel
|
|
197
|
-
`index.ts` (`export * from './AddAuthor'`) like the shipped example slice.
|
|
198
|
-
|
|
199
|
-
```tsx
|
|
200
|
-
// Authors/Registration/AddAuthor.tsx
|
|
201
|
-
import { CommandDialog } from '@cratis/components/CommandDialog';
|
|
202
|
-
import { InputTextField } from '@cratis/components/CommandForm';
|
|
203
|
-
import { RegisterAuthor } from './RegisterAuthor'; // co-located generated proxy
|
|
204
|
-
|
|
205
|
-
export const AddAuthor = () => (
|
|
206
|
-
<CommandDialog command={RegisterAuthor} title="Add author" okLabel="Add" cancelLabel="Cancel">
|
|
207
|
-
<InputTextField<RegisterAuthor> value={c => c.name} title="Name" />
|
|
208
|
-
</CommandDialog>
|
|
209
|
-
);
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
Inline form alternative:
|
|
213
|
-
|
|
214
|
-
```tsx
|
|
215
|
-
const [command] = RegisterAuthor.use();
|
|
216
|
-
const submit = async () => {
|
|
217
|
-
const result = await command.execute();
|
|
218
|
-
if (result.isSuccess) onSuccess(result.response);
|
|
219
|
-
};
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
Never import `Dialog` from `primereact/dialog` — use the Cratis wrappers. PrimeReact CSS variables for
|
|
223
|
-
colors. No `any`. Full descriptive variable names.
|
|
224
|
-
|
|
225
|
-
---
|
|
226
|
-
|
|
227
|
-
## Checklist
|
|
228
|
-
|
|
229
|
-
- One `.cs` file, namespace without `.Features.` (no file header unless the project uses one).
|
|
230
|
-
- `[Command]` with `Handle()` on the record — no handler class.
|
|
231
|
-
- `[EventType]` with **no** arguments; past-tense names; no nullable fields.
|
|
232
|
-
- `ConceptAs<T>` for every identity / value — no raw `Guid` / `string` in the domain.
|
|
233
|
-
- `CommandValidator<T>` for validations; read-model parameter for DCB rules.
|
|
234
|
-
- One spec per scenario in `slice.json`; `dotnet build` clean; specs pass.
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: build-state-view
|
|
3
|
-
description: >
|
|
4
|
-
Implement read slices the Cratis way — a [ReadModel] record with static query methods, fed by a
|
|
5
|
-
Chronicle projection or reducer, plus the React component that renders it. Use when: (1) implementing
|
|
6
|
-
a new read slice / projection in a Cratis project, (2) a slice.json has a non-empty readModel /
|
|
7
|
-
projections / queries section, (3) the user provides a read-slice Event Modeling artifact or
|
|
8
|
-
specification and asks to implement it, (4) the user says "implement", "create", "add" a read slice,
|
|
9
|
-
read model, projection, reducer, or query in a Cratis Arc / Chronicle project.
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
# Cratis — Read Slice (State View)
|
|
13
|
-
|
|
14
|
-
A read slice projects events into a queryable read model. In Cratis the path lives in **one `.cs`
|
|
15
|
-
file**:
|
|
16
|
-
|
|
17
|
-
```
|
|
18
|
-
[ReadModel] record + static queries → projection or reducer → dotnet build → React
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
> **Read first:** [../_shared/cratis-conventions.md](../_shared/cratis-conventions.md). Everything
|
|
22
|
-
> below assumes those rules.
|
|
23
|
-
|
|
24
|
-
## Step 0 — Discover conventions
|
|
25
|
-
|
|
26
|
-
Read `.build-kit/CLAUDE.md` and one existing read slice. Confirm the namespace root, how existing read models
|
|
27
|
-
declare queries (snapshot vs observable), whether projections use model-bound attributes or
|
|
28
|
-
`IProjectionFor<T>`, and the MongoDB collection wiring. Resolve slice `comments` when done (see the
|
|
29
|
-
state-change skill's Step 0 for the resolve endpoint).
|
|
30
|
-
|
|
31
|
-
## Step 1 — Understand the input (`slice.json` is the source of truth)
|
|
32
|
-
|
|
33
|
-
| Element | What to extract |
|
|
34
|
-
|---|---|
|
|
35
|
-
| **Read model** | Name, fields, the key field |
|
|
36
|
-
| **Source events** | Which events feed each field (projections **join events, never read models**) |
|
|
37
|
-
| **Queries** | The queries to expose; whether each is snapshot or real-time (observable) |
|
|
38
|
-
| **Specifications** | Each scenario → an executable spec proving the projection result |
|
|
39
|
-
| **Storylines** (optional) | `storylines[]` — ordered walkthrough "beats"; a read-model chain in one is a supplementary spec source, see Step 4a |
|
|
40
|
-
|
|
41
|
-
If a field has no source event in `slice.json`, do not invent one. One read model per use case —
|
|
42
|
-
never reuse a model across slices.
|
|
43
|
-
|
|
44
|
-
## Step 2 — Write the slice `.cs` file
|
|
45
|
-
|
|
46
|
-
`<Module>/<Feature>/<Slice>/<Slice>.cs` — namespace `<Root>.<Module>.<Feature>.<Slice>` (mirror the folders; drop any `.Features.`).
|
|
47
|
-
|
|
48
|
-
### Read model + static query methods
|
|
49
|
-
```csharp
|
|
50
|
-
[ReadModel]
|
|
51
|
-
public record AuthorListItem(AuthorId Id, AuthorName Name, int BookCount)
|
|
52
|
-
{
|
|
53
|
-
public static async Task<IEnumerable<AuthorListItem>> AllAuthors(
|
|
54
|
-
IMongoCollection<AuthorListItem> collection)
|
|
55
|
-
=> await collection.Find(_ => true).ToListAsync();
|
|
56
|
-
|
|
57
|
-
public static async Task<AuthorListItem?> GetAuthor(
|
|
58
|
-
AuthorId id, IMongoCollection<AuthorListItem> collection)
|
|
59
|
-
=> await collection.Find(a => a.Id == id).FirstOrDefaultAsync();
|
|
60
|
-
|
|
61
|
-
// Real-time push — return ISubject<T> directly (never Task<ISubject<T>>)
|
|
62
|
-
public static ISubject<IEnumerable<AuthorListItem>> ObserveAllAuthors(
|
|
63
|
-
IMongoCollection<AuthorListItem> collection)
|
|
64
|
-
=> collection.Observe();
|
|
65
|
-
}
|
|
66
|
-
```
|
|
67
|
-
- `[ReadModel]` required. Query methods are **`public static`** on the record; the method name becomes
|
|
68
|
-
the TS query proxy class name. No controller, no `IReadModels`.
|
|
69
|
-
- Favor reactive (`ISubject<T>`) queries when the slice wants live updates.
|
|
70
|
-
|
|
71
|
-
### Choose projection vs reducer
|
|
72
|
-
- **Projection** — shaped read models, field mapping, joins, children (most read slices).
|
|
73
|
-
- **Reducer** — running aggregates (balances, counts, sums) where state accumulates across events.
|
|
74
|
-
|
|
75
|
-
### Projection (declarative; AutoMap on by default)
|
|
76
|
-
```csharp
|
|
77
|
-
public class AuthorListItemProjection : IProjectionFor<AuthorListItem>
|
|
78
|
-
{
|
|
79
|
-
public void Define(IProjectionBuilderFor<AuthorListItem> builder) => builder
|
|
80
|
-
.From<AuthorRegistered>(from => from.Set(m => m.Name).To(e => e.Name))
|
|
81
|
-
.From<BookAdded>(from => from.Add(m => m.BookCount).With(_ => 1));
|
|
82
|
-
}
|
|
83
|
-
```
|
|
84
|
-
Or model-bound shorthand for simple cases:
|
|
85
|
-
```csharp
|
|
86
|
-
[ReadModel]
|
|
87
|
-
public record AuthorInfo([Key] Guid Id, [FromEvent<AuthorRegistered>] string Name);
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
Projections/reducers are discovered automatically — no registration. See
|
|
91
|
-
[references/patterns.md](references/patterns.md) for joins, children, composite keys, and reducers.
|
|
92
|
-
|
|
93
|
-
## Step 3 — Build
|
|
94
|
-
|
|
95
|
-
`dotnet build` from the project root — zero warnings / errors. This generates the query proxy the
|
|
96
|
-
frontend imports.
|
|
97
|
-
|
|
98
|
-
## Step 4 — Specs
|
|
99
|
-
|
|
100
|
-
Write specs proving the projection/reducer produces the expected read model from a sequence of events —
|
|
101
|
-
one spec per scenario in `slice.json`. Run `dotnet test --filter "FullyQualifiedName~<SliceName>"`.
|
|
102
|
-
See [references/patterns.md](references/patterns.md).
|
|
103
|
-
|
|
104
|
-
## Step 4a — Storyline-derived specs (optional)
|
|
105
|
-
|
|
106
|
-
If the slice's `storylines[]` array (in `slice.json`, alongside `specifications[]`) is non-empty —
|
|
107
|
-
a storyline embedded in this slice's slice.json already belongs entirely to this slice, no need to
|
|
108
|
-
match beats against `readmodels[]` by id/title — look for a **read-model chain**: two beats of
|
|
109
|
-
`type: "READMODEL"`, with only `EVENT` beat(s) between them. That's a self-contained projection
|
|
110
|
-
spec:
|
|
111
|
-
|
|
112
|
-
- **Establish** — append every event from the storyline's start up through the intervening
|
|
113
|
-
event(s), in order.
|
|
114
|
-
- **Because** — run the projection.
|
|
115
|
-
- **should_\*** — assert against the *later* beat's `fields`/`examples`/`expectEmptyList`.
|
|
116
|
-
|
|
117
|
-
Put these in their own `for_<ReadModel>/when_<storyline-title>/` folder (not `when_<behavior>/`),
|
|
118
|
-
so they're never confused with the exhaustive `specifications[]` suite — a storyline is a
|
|
119
|
-
narrated walkthrough, not exhaustive coverage.
|
|
120
|
-
|
|
121
|
-
If the storyline's transition into this read model is instead `COMMAND → EVENT → READMODEL`, only
|
|
122
|
-
the `EVENT → READMODEL` half belongs here — the `COMMAND → EVENT` half is a separate spec in
|
|
123
|
-
**build-state-change** (see its Step 5a). Don't try to assert read-model state from a dispatched
|
|
124
|
-
command in one spec unless the project already has an established pattern for that.
|
|
125
|
-
|
|
126
|
-
Skip a beat sequence entirely — no spec, no placeholder — if it can't be isolated (e.g. a SCREEN
|
|
127
|
-
beat with no traceable event). A storyline is source material for tests, not a mandate to write
|
|
128
|
-
one for every beat.
|
|
129
|
-
|
|
130
|
-
## Step 5 — Frontend
|
|
131
|
-
|
|
132
|
-
Add `<Module>/<Feature>/<Slice>/<Component>.tsx` importing the co-located generated query proxy from
|
|
133
|
-
`./` (`MyQuery.use()` snapshot, `MyQuery.useWithPaging(pageSize)` for paging, observable proxy for live
|
|
134
|
-
data; or `DataTableForObservableQuery` for a table). Add a barrel `index.ts`. Register it in the
|
|
135
|
-
feature's composition page; add routing in `App.tsx` if it's a new page.
|
|
136
|
-
|
|
137
|
-
## Final verification — does the implementation match `slice.json`?
|
|
138
|
-
|
|
139
|
-
- [ ] Every read-model field → a record property fed by a projection/reducer mapping.
|
|
140
|
-
- [ ] Every source event used exists; projections map from **events**, never read models.
|
|
141
|
-
- [ ] Every query in the slice is exposed as a static method; observable where the slice wants live data.
|
|
142
|
-
- [ ] Every scenario → an executable spec.
|
|
143
|
-
- [ ] A `storylines[]` read-model chain for this read model has a spec, or was deliberately
|
|
144
|
-
skipped (not silently ignored).
|
|
145
|
-
- [ ] `dotnet build` clean; specs pass.
|
|
146
|
-
|
|
147
|
-
## References
|
|
148
|
-
- [references/patterns.md](references/patterns.md) — full projection/reducer/query/spec code + React.
|
|
149
|
-
- [../_shared/cratis-conventions.md](../_shared/cratis-conventions.md) — the Cratis conventions.
|