@cratis/pi 7.60.1 → 7.62.0
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/package/corpus/agents/spec-writer.md +2 -2
- package/package/corpus/harnesses/copilot/instructions/application-profile.instructions.md +2 -2
- package/package/corpus/harnesses/copilot/instructions/glossary.instructions.md +2 -2
- package/package/corpus/harnesses/copilot/instructions/profiles.instructions.md +2 -0
- package/package/corpus/harnesses/copilot/instructions/specs.scenarios.csharp.instructions.md +7 -4
- package/package/corpus/harnesses/copilot/instructions/vertical-slices.instructions.md +22 -13
- package/package/corpus/product-sync.json +3 -2
- package/package/corpus/prompts/add-business-rule.prompt.md +1 -1
- package/package/corpus/rules/application-profile.md +2 -2
- package/package/corpus/rules/glossary.md +2 -2
- package/package/corpus/rules/issue-assignment.md +31 -0
- package/package/corpus/rules/profiles.md +2 -0
- package/package/corpus/rules/specs.scenarios.csharp.md +7 -4
- package/package/corpus/rules/vertical-slices.md +22 -13
- package/package/corpus/skills/cratis-application-slice-specifications/SKILL.md +4 -3
- package/package/corpus/skills/cratis-application-slice-specifications/references/query-and-decision-scenarios.md +12 -3
- package/package/corpus/skills/cratis-arc-authentication-authorization-and-identity/SKILL.md +12 -7
- package/package/corpus/skills/cratis-arc-authentication-authorization-and-identity/references/authorization.md +11 -11
- package/package/corpus/skills/cratis-arc-command/SKILL.md +3 -2
- package/package/corpus/skills/cratis-arc-command/references/decisions-and-routing.md +95 -51
- package/package/corpus/skills/cratis-arc-command/references/fresh-identifiers.md +5 -2
- package/package/corpus/skills/cratis-arc-command/references/read-model-injection.md +9 -5
- package/package/corpus/skills/cratis-arc-command-validation/SKILL.md +99 -52
- package/package/corpus/skills/cratis-chronicle-decision-reads/SKILL.md +37 -15
- package/package/corpus/skills/cratis-chronicle-decision-reads/references/admission-and-ownership.md +3 -2
- package/package/corpus/skills/cratis-chronicle-decision-reads/references/choosing-how-a-rule-holds.md +277 -138
- package/package/corpus/skills/cratis-chronicle-event-constraints/SKILL.md +4 -4
- package/package/corpus/skills/cratis-chronicle-event-modeling/SKILL.md +6 -4
- package/package/corpus/skills/cratis-chronicle-read-model/SKILL.md +2 -1
- package/package/corpus/skills/cratis-chronicle-reducer/SKILL.md +6 -3
- package/package/corpus/skills/cratis-code-review/SKILL.md +6 -3
- package/package/corpus/skills/cratis-screenplay-streams-and-consistency/references/consistency-and-concurrency.md +5 -3
- package/package/corpus/skills/cratis-specifications-csharp/SKILL.md +3 -2
- package/package/corpus/skills/cratis-specifications-csharp/references/application-scenarios.md +1 -1
- package/package.json +1 -1
|
@@ -15,61 +15,100 @@ Source: Arc. These features require `Cratis.Arc.Chronicle`.
|
|
|
15
15
|
|
|
16
16
|
## Pick the guarded read
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
Under read protection `On` (`<CratisReadProtection>On</CratisReadProtection>`, or
|
|
19
|
+
`[ReadProtection(ReadProtectionMode.On)]` on a command), a read model that is a
|
|
20
|
+
parameter of `Handle()` or of a `CommandValidator<T>` constructor is read fresh from
|
|
21
|
+
the event log and guards the command's append. `Off` is the default when nothing is
|
|
22
|
+
set; under `Off` and `Measure` those reads are plain, a hint that guards nothing.
|
|
23
|
+
A `Result<TEvent, ValidationResult>` rejects a request but does not enroll a guard.
|
|
24
|
+
Uniqueness and create-once belong to a Chronicle constraint, not to any read. For the
|
|
25
|
+
routing by rule shape, the matrix and the traps read
|
|
22
26
|
[choosing how a rule holds](../../cratis-chronicle-decision-reads/references/choosing-how-a-rule-holds.md).
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
`Cratis.Arc.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
(
|
|
50
|
-
|
|
28
|
+
Requires Arc 22.106.0 for read protection. Older Arc has only the per-command
|
|
29
|
+
`[ProtectedDecision]` path below.
|
|
30
|
+
|
|
31
|
+
- **One entity's state.** Put the rule in a validator with the read model as a
|
|
32
|
+
constructor parameter; `Handle()` returns the events. The key is
|
|
33
|
+
`[LookUpBy(nameof(Cmd.Prop))]` on the parameter (`Cratis.Arc.Commands`), else the
|
|
34
|
+
single command property whose type equals the model's `Id` type, else the
|
|
35
|
+
command's event source id. Several matching properties fail with
|
|
36
|
+
`AmbiguousReadModelLookup` (500): add `[LookUpBy]`.
|
|
37
|
+
- **Two entities.** Two read-model parameters, the second with `[LookUpBy]`;
|
|
38
|
+
`Handle()` returns `IEnumerable<EventForEventSourceId>`, one event per entity,
|
|
39
|
+
appended atomically. Use a distinct event type per entity.
|
|
40
|
+
- **One stream inside a source.** A per-stream read model
|
|
41
|
+
(`[ReadModel, EventStream<TSource>("Transactions")]`, `Cratis.Chronicle.ReadModels`)
|
|
42
|
+
as a plain parameter, on a command routed `[EventSource<TSource>("Transactions")]`
|
|
43
|
+
and `[EventStreamId("{Month}")]`; otherwise `ReadModelStreamNotOnCommandRoute`
|
|
44
|
+
(500, analyzer ARCCHR0037). It is folded from that one stream and key-only reads
|
|
45
|
+
are refused. For raw events of a short stream, inject `IStreamReads` into
|
|
46
|
+
`Provide`, call `ForCommand()`, and return `StreamDecision.Append(...)` from
|
|
47
|
+
`Handle` (`EventsWithConcurrencyScopes`); a competing append conflicts, including
|
|
48
|
+
two competing first appends.
|
|
49
|
+
- **Computed key or another stream or source.** Command-aware `IDecisionReads.Get<T>`
|
|
50
|
+
in `Provide()`, including the stream overloads.
|
|
51
|
+
- **Rich lifecycle.** An aggregate root, with `Commit()` returned last. Under `On`,
|
|
52
|
+
an aggregate and a protected read of the same source are refused
|
|
53
|
+
(`ReadAndAggregateOnSameSource`); a different source is fine.
|
|
54
|
+
- **Editing conflict.** Return `EventsWithConcurrencyScopes` built from the revision
|
|
55
|
+
the client sent. `concurrency: true` on the routing attributes is **not** a guard
|
|
56
|
+
for the decision: Arc reads the narrowed tail after `Handle`. Remove the flags
|
|
57
|
+
when returning exact scopes (ARCCHR0016). The default check also reads the tail
|
|
58
|
+
after `Handle`, and the first append into a scope is unchecked unless
|
|
59
|
+
`CheckFirstAppendIntoAScope` is set.
|
|
60
|
+
- **No guard needed.** A hint or a rare violation: mark a deliberate advisory read
|
|
61
|
+
`[Unprotected("why")]` (`Cratis.Arc.Chronicle.ReadModels`; on a method it covers
|
|
62
|
+
the body, not injected parameters), or repair with a reactor.
|
|
63
|
+
|
|
64
|
+
Under `On`, `IReadModels`, `IMongoCollection`, detached reads and immediate appends
|
|
65
|
+
through an injected `IEventLog` are refused; take the model as a parameter and
|
|
66
|
+
return the events. Admitted models: event-source-keyed model-bound or declarative
|
|
67
|
+
projections with a string or Guid `Id`, and unfiltered reducers without constructor
|
|
68
|
+
dependencies except `ILogger<T>` (self-reducing models are exempt). Joins and
|
|
69
|
+
children, event-property keys, Pinned delivery, filtered reducers, reducers with
|
|
70
|
+
dependencies and other event sequences fail with `ReadCannotBeProtected` (500)
|
|
71
|
+
unless marked `[Unprotected("why")]`. Validators with read models must be
|
|
72
|
+
convention-registered (transient).
|
|
73
|
+
|
|
74
|
+
When the command has two or more key candidates, implement `ICanProvideEventSourceId`:
|
|
75
|
+
`[Key]` does not silence `ARCCHR0002`, and Arc silently keys by the first eligible
|
|
76
|
+
property in declaration order.
|
|
77
|
+
|
|
78
|
+
A lost race is HTTP 400 with a validation result of reason `concurrencyViolation`,
|
|
79
|
+
`reasonDetail` `ReadChanged` and `state` `{ readModel, key }`. Arc and Chronicle do
|
|
80
|
+
not re-run the command; the client submits it again. Every other refusal is 500:
|
|
81
|
+
change the code.
|
|
82
|
+
|
|
83
|
+
### Per-command opt-in: `[ProtectedDecision]`
|
|
84
|
+
|
|
85
|
+
`[ProtectedDecision]` (`Cratis.Arc.Chronicle.ReadModels`) with `DecisionRead<T>`
|
|
86
|
+
(`Exists`/`Instance`) is the older per-command opt-in. It still works and is not
|
|
87
|
+
obsolete; do not describe `On` as making it redundant. Use it for a command in a
|
|
88
|
+
project that is not on `On`. Without `On`, only a parameterless validator runs: a
|
|
89
|
+
discoverable validator needs exactly one public parameterless constructor,
|
|
90
|
+
otherwise the command fails with HTTP 500. Keep decision state out of validators
|
|
91
|
+
there, including static members and service locators. `[Unprotected]` on the
|
|
92
|
+
command opts out; never combine it with `[ProtectedDecision]`
|
|
93
|
+
(`CommandCannotBeBothProtectedAndUnprotected`).
|
|
51
94
|
|
|
52
95
|
Requires Arc 22.81.0 for stream-scoped `IDecisionReads.Get<T>`,
|
|
53
96
|
`GetDetached<T>` and `AdmitForStream<T>` to participate in command protection,
|
|
54
|
-
caching and enrollment. Older Arc refuses these stream reads inside commands
|
|
55
|
-
check the Arc package version before choosing this path. `[Unprotected]` command
|
|
56
|
-
reads are unguarded, not refused.
|
|
97
|
+
caching and enrollment. Older Arc refuses these stream reads inside commands.
|
|
57
98
|
|
|
58
99
|
Do not manually append or commit to bypass the owner-capable unit of work.
|
|
59
100
|
The guard detects every matching event appended after its boundary, whoever wrote
|
|
60
101
|
it, but it protects only guarded decisions: an unguarded writer deciding on the
|
|
61
102
|
same facts can still append on stale state, so the rule holds only when every
|
|
62
|
-
deciding writer is guarded or the rule is a constraint.
|
|
63
|
-
`[Unprotected]`
|
|
64
|
-
migrations, changes to projection definitions
|
|
65
|
-
serialized append path.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
integration specification. In contrast, `StreamDecision.Append([])`
|
|
72
|
-
returns no scopes: use it only for an identical retry, not a rejected request.
|
|
103
|
+
deciding writer is guarded or the rule is a constraint. Outside the guard:
|
|
104
|
+
`[Unprotected]` reads, `Off` mode, external state, revisions and redactions,
|
|
105
|
+
migrations, changes to projection definitions and writes that bypass the kernel's
|
|
106
|
+
serialized append path. A protected command that stages no events commits an empty
|
|
107
|
+
guarded batch: current kernels validate the enrolled reads and write nothing, while
|
|
108
|
+
a kernel that rejects an empty batch raises `DecisionReadValidateOnlyNotSupported`
|
|
109
|
+
(HTTP 500), so cover it with an integration specification. In contrast,
|
|
110
|
+
`StreamDecision.Append([])` returns no scopes: use it only for an identical retry,
|
|
111
|
+
not a rejected request.
|
|
73
112
|
|
|
74
113
|
## Definition-backed routes and templates
|
|
75
114
|
|
|
@@ -125,12 +164,17 @@ source id/type and stream route; reactor tuples/collections cannot mix it with e
|
|
|
125
164
|
|
|
126
165
|
## Decision specifications
|
|
127
166
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
`
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
167
|
+
`new CommandScenario<T>()` selects the in-process decision log from the command's
|
|
168
|
+
read-protection mode and dependencies (under `On`, with a per-stream model or
|
|
169
|
+
`[ProtectedDecision]`); `UseDecisionReads()` is hidden and unnecessary. An `Off`
|
|
170
|
+
command with no protected dependency stays in the legacy mode with pinned read
|
|
171
|
+
models, which cannot prove a race. Seed with `Given.ForEventSource(id).Events(...)`,
|
|
172
|
+
`.OnRoute(route).Events(...)`, queue a competing append with
|
|
173
|
+
`AppendConcurrently(id, events...)` (or `AppendConcurrently(id, route, ...)`) and
|
|
174
|
+
assert `result.ShouldHaveValidationErrorBecauseOf(ValidationResultReason.ConcurrencyViolation)`.
|
|
175
|
+
There is no `scenario.AppendConditions`. Guarded state needs seeded events: protected reads ignore a pinned `.ReadModel(m)` (which only pins an advisory plain read) and `DecisionRead<T>` rejects it. Assert a
|
|
176
|
+
real nonempty decision conflicts on the intended route, and that an independent
|
|
177
|
+
route is not over-guarded. `ShouldHaveCompletedStream(type, id)` asserts completion;
|
|
134
178
|
`ShouldHaveCompletedStream(ClosedStreamScope)` asserts the exact owner and route;
|
|
135
179
|
`DecisionReadAdmissions.ShouldAdmitDecisionReadsIn(assembly)` checks projection
|
|
136
180
|
admission in assembly-level specs.
|
|
@@ -98,8 +98,11 @@ use an appropriate constraint or explicit concurrency scope for them.
|
|
|
98
98
|
|
|
99
99
|
## Specify the collision
|
|
100
100
|
|
|
101
|
-
Use `new CommandScenario<TCommand>()
|
|
102
|
-
|
|
101
|
+
Use `new CommandScenario<TCommand>()`; it selects the in-process log from the
|
|
102
|
+
command's read-protection mode and dependencies before seeding. If a seeded
|
|
103
|
+
collision throws `GeneratedIdentifierCollisionRequiresDecisionReads`, the
|
|
104
|
+
scenario stayed in legacy mode: call the compatibility switch `UseDecisionReads()`
|
|
105
|
+
before seeding. Then `PinGeneratedValue(name, id)` and `Given.ForEventSource(id).Events(existingEvent)`.
|
|
103
106
|
Assert the concurrency reason and collision detail, unchanged append tail and
|
|
104
107
|
one handler execution. Also cover an empty source receiving two events through
|
|
105
108
|
the chosen write API, an occupied source whose events belong to another stream,
|
|
@@ -7,10 +7,12 @@
|
|
|
7
7
|
- Which provider owns a read model type
|
|
8
8
|
|
|
9
9
|
Source: [read models in commands](https://github.com/Cratis/Arc/blob/main/Documentation/backend/csharp/chronicle/read-models/injecting-into-commands.md).
|
|
10
|
-
|
|
11
|
-
reads
|
|
12
|
-
`
|
|
13
|
-
|
|
10
|
+
Under read protection `Off` (the default) and `Measure`, injected models and keyed
|
|
11
|
+
reads are plain reads, not concurrency guards. Under `On`, a read-model parameter of
|
|
12
|
+
`Handle()` or a validator constructor is guarded; `IReadModels.GetInstanceById` in a
|
|
13
|
+
command is refused, so read another entity's model with a second parameter and
|
|
14
|
+
`[LookUpBy(nameof(Cmd.Prop))]`. See [decisions and routing](decisions-and-routing.md)
|
|
15
|
+
and, for per-command opt-in, `[ProtectedDecision]` with `DecisionRead<T>`.
|
|
14
16
|
|
|
15
17
|
A read model can be injected into a `CommandValidator<T>` constructor, into
|
|
16
18
|
`Provide()`, or into `Handle()`. Arc resolves it through an
|
|
@@ -26,7 +28,9 @@ error. The three shapes it takes:
|
|
|
26
28
|
ambiguous;
|
|
27
29
|
3. the id is generated inside `Handle()` and does not exist yet.
|
|
28
30
|
|
|
29
|
-
In all three, read by explicit key in `Provide()
|
|
31
|
+
In all three, under `Off` read by explicit key in `Provide()`; under `On` use a
|
|
32
|
+
second parameter with `[LookUpBy]`, or `IDecisionReads.Get` in `Provide()` for a
|
|
33
|
+
key computed there:
|
|
30
34
|
|
|
31
35
|
```csharp
|
|
32
36
|
var other = await readModels.GetInstanceById<<Other>>((EventSourceId)<OtherId>);
|
|
@@ -59,14 +59,15 @@ another version.
|
|
|
59
59
|
Which mechanism makes a rule hold under concurrent writers (constraint, protected
|
|
60
60
|
decision, stream decision, aggregate, scope, reactor repair) is routed in
|
|
61
61
|
[choosing how a rule holds](../cratis-chronicle-decision-reads/references/choosing-how-a-rule-holds.md).
|
|
62
|
-
A validator is
|
|
62
|
+
A validator holds a state rule only when the project is on read protection `On`;
|
|
63
|
+
under `Off` (the default) it is a hint, never the mechanism.
|
|
63
64
|
|
|
64
65
|
| The rule is… | Put it in |
|
|
65
66
|
| --- | --- |
|
|
66
67
|
| A reusable value invariant — length, format, range | `ConceptValidator<T>` on the concept type |
|
|
67
68
|
| Command input, cross-field, or a pre-handler check | `CommandValidator<TCommand>` with `RuleFor(...)` |
|
|
68
69
|
| A dependency on data the handler must fetch first | `Provide()` — fetch, and short-circuit if unusable |
|
|
69
|
-
| State-dependent and must hold **under concurrency** |
|
|
70
|
+
| State-dependent and must hold **under concurrency** | Under `On`, a `CommandValidator<T>` with the read model as a constructor parameter (Step 5); or `IStreamReads`/`StreamDecision`; return events through Arc. `[ProtectedDecision]` + `DecisionRead<T>` is the older per-command opt-in |
|
|
70
71
|
| Uniqueness | A Chronicle constraint, never a command-side read |
|
|
71
72
|
| A genuine defect or missing infrastructure | `throw` a domain exception |
|
|
72
73
|
|
|
@@ -119,11 +120,27 @@ public class TransferFundsValidator : CommandValidator<TransferFunds>
|
|
|
119
120
|
- The validator is discovered by type — no registration.
|
|
120
121
|
- Constructor dependencies (read models, services) are injected. A read model
|
|
121
122
|
injected here resolves the same way as one injected into `Handle()`; see
|
|
122
|
-
Step 5
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
123
|
+
Step 5. **State rules ("may this proceed") belong here**, with the read model as a
|
|
124
|
+
constructor parameter. Under read protection `On` that read is fresh from the log
|
|
125
|
+
and guards the append: if an event of the model's types for that key arrives
|
|
126
|
+
after the read, nothing from the command is written. Under `Off` (the default
|
|
127
|
+
when nothing is set) and `Measure` the same validator reads a plain model: a hint
|
|
128
|
+
with a friendly message that guards nothing. Under `On` the validator must be
|
|
129
|
+
convention-registered (transient); hand-registered scoped, singleton, factory or
|
|
130
|
+
instance validators are refused. Under `[ProtectedDecision]` without `On`, a
|
|
131
|
+
discoverable validator needs exactly one public parameterless constructor
|
|
132
|
+
(otherwise HTTP 500).
|
|
133
|
+
- **The caller can lower the severity threshold.** Model-bound commands accept
|
|
134
|
+
`X-Allowed-Severity` (`allowedSeverity`). Under `Off` and `Measure`, a caller
|
|
135
|
+
sending `3` (Error) removes Error results from validators, command filters and
|
|
136
|
+
`Provide()`, so `Handle()` runs and a validator's state rule can be skipped
|
|
137
|
+
unless the command declares
|
|
138
|
+
`[BlockOnValidationSeverity(ValidationResultSeverity.Error)]`
|
|
139
|
+
(`Cratis.Arc.Commands.ModelBound`). Requires Arc 22.108.0 for the `On` floor: a
|
|
140
|
+
command declared `On` blocks `Error` and `Unknown` whatever the caller sends; the
|
|
141
|
+
caller can relax only `Warning` and `Information`. Lowering
|
|
142
|
+
`ReadProtection:MaximumMode` at runtime does not remove the floor, and an explicit
|
|
143
|
+
attribute wins. Severity is not authorization.
|
|
127
144
|
- **Omit the validator entirely when there are no rules.** An empty subclass is
|
|
128
145
|
noise the reader has to check.
|
|
129
146
|
- Single-property intrinsic rules belong on `ConceptValidator<T>` instead, so
|
|
@@ -159,53 +176,77 @@ public async Task<Result<Customer, ValidationResult>> Provide(IReadModels readMo
|
|
|
159
176
|
|
|
160
177
|
## Step 5 — Guard a concurrency-sensitive decision
|
|
161
178
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
179
|
+
Set read protection `On` for new code: `<CratisReadProtection>On</CratisReadProtection>`
|
|
180
|
+
in the project (or `[ReadProtection(ReadProtectionMode.On)]` on one command). Under
|
|
181
|
+
`On`, the read-model parameters of `Handle()` and of the validator constructor are
|
|
182
|
+
the guard, so the state rule is a validator and `Handle()` returns the events.
|
|
183
|
+
Requires Arc 22.106.0. Under `Off` (the default) a plain read guards nothing, even
|
|
184
|
+
inside `Handle()` returning `Result<,>`. Choose the first boundary that fits:
|
|
185
|
+
|
|
186
|
+
1. A unique value or create-once: a Chronicle constraint, not a read.
|
|
187
|
+
2. One entity's state: a read-model parameter in the validator (this step's example).
|
|
188
|
+
Several entities: one parameter each, with `[LookUpBy(nameof(Command.Prop))]` on
|
|
189
|
+
a parameter whose key is not the command's event source id, and `Handle()`
|
|
190
|
+
returning `IEnumerable<EventForEventSourceId>` with a distinct event type per
|
|
191
|
+
entity.
|
|
192
|
+
3. A limit inside one stream: a per-stream read model (`[EventStream<TSource>("…")]`)
|
|
193
|
+
on a command routed `[EventSource<TSource>("…")]` + `[EventStreamId("{Prop}")]`,
|
|
194
|
+
or `Provide(IStreamReads reads) => reads.ForCommand()` returning `StreamDecision`.
|
|
195
|
+
4. An editing conflict: exact returned scopes from the revision the client sent.
|
|
196
|
+
`concurrency: true` on routing attributes is **not** a guard: like the default
|
|
197
|
+
check it reads the tail after `Handle`.
|
|
198
|
+
5. Rich lifecycle: an aggregate; do not also read the same source.
|
|
199
|
+
|
|
200
|
+
A read under `On` is refused, with HTTP 500, when it cannot be guarded exactly:
|
|
201
|
+
`IReadModels`, `IMongoCollection`, detached reads, an injected `IEventLog.Append`,
|
|
202
|
+
and models that are joins, event-property keyed, filtered or dependency-bearing
|
|
203
|
+
reducers, Pinned or on another sequence (`ReadCannotBeProtected`; mark a deliberate
|
|
204
|
+
advisory read `[Unprotected("why")]`). Several command properties with the model's
|
|
205
|
+
`Id` type are `AmbiguousReadModelLookup`: add `[LookUpBy]`. A lost race is HTTP 400
|
|
206
|
+
`concurrencyViolation` (`ReadChanged`): the client resubmits; Arc does not re-run
|
|
207
|
+
the command. `[ProtectedDecision]` with `DecisionRead<T>` is the older per-command
|
|
208
|
+
opt-in; it still works and is not obsolete. A guard protects only guarded
|
|
209
|
+
decisions: every writer that decides on the rule needs one, or the rule must be a
|
|
210
|
+
constraint.
|
|
211
|
+
|
|
212
|
+
`ARCCHR0011` (Info, `Off` only) and `ARCCHR0012` flag plain reads and immediate
|
|
213
|
+
append bypasses. Return events, never append them directly. Read
|
|
187
214
|
[protected and stream decisions](../cratis-arc-command/references/decisions-and-routing.md)
|
|
188
|
-
|
|
189
|
-
|
|
215
|
+
and
|
|
216
|
+
[choosing how a rule holds](../cratis-chronicle-decision-reads/references/choosing-how-a-rule-holds.md)
|
|
217
|
+
when choosing the guard. The example assumes `CartSummary` is a model-bound
|
|
218
|
+
projection with a `CartId Id`; domain types are the slice's existing types.
|
|
190
219
|
|
|
191
220
|
```csharp
|
|
192
|
-
using Cratis.
|
|
193
|
-
using Cratis.Arc.
|
|
221
|
+
using Cratis.Arc.Commands;
|
|
222
|
+
using Cratis.Arc.Commands.ModelBound;
|
|
223
|
+
using FluentValidation;
|
|
194
224
|
|
|
195
225
|
[Command]
|
|
196
|
-
[ProtectedDecision]
|
|
197
226
|
public record AddItemToCart(CartId CartId, ItemId ItemId)
|
|
198
227
|
{
|
|
199
|
-
/// <summary>Adds the item
|
|
200
|
-
/// <
|
|
201
|
-
|
|
202
|
-
public Result<ItemAddedToCart, ValidationResult> Handle(DecisionRead<CartSummary> cart) =>
|
|
203
|
-
!cart.Exists
|
|
204
|
-
? ValidationResult.Error("Cart must exist.")
|
|
205
|
-
: cart.Instance.ItemCount >= 3
|
|
206
|
-
? ValidationResult.Error("A cart can hold at most 3 items.")
|
|
207
|
-
: new ItemAddedToCart(ItemId);
|
|
228
|
+
/// <summary>Adds the item.</summary>
|
|
229
|
+
/// <returns>The event.</returns>
|
|
230
|
+
public ItemAddedToCart Handle() => new(ItemId);
|
|
208
231
|
}
|
|
232
|
+
|
|
233
|
+
public class AddItemToCartValidator : CommandValidator<AddItemToCart>
|
|
234
|
+
{
|
|
235
|
+
public AddItemToCartValidator(CartSummary? cart) =>
|
|
236
|
+
RuleFor(c => c.CartId)
|
|
237
|
+
.Must(_ => cart is { ItemCount: < 3 })
|
|
238
|
+
.WithMessage("The cart must exist and hold fewer than 3 items.");
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Use `Result<TEvent, ValidationResult>` from `Handle()` only when the rule needs a value
|
|
243
|
+
computed there:
|
|
244
|
+
|
|
245
|
+
```csharp
|
|
246
|
+
public Result<ItemAddedToCart, ValidationResult> Handle(CartSummary? cart) =>
|
|
247
|
+
cart is not { ItemCount: < 3 }
|
|
248
|
+
? ValidationResult.Error("A cart can hold at most 3 items.")
|
|
249
|
+
: new ItemAddedToCart(ItemId);
|
|
209
250
|
```
|
|
210
251
|
|
|
211
252
|
- `Result<TSuccess, TError>` is `Cratis.Monads.Result<,>` from Cratis
|
|
@@ -214,6 +255,8 @@ public record AddItemToCart(CartId CartId, ItemId ItemId)
|
|
|
214
255
|
statements above compile without wrapping.
|
|
215
256
|
- **Return the error; never throw it.**
|
|
216
257
|
- One parameter per logical read model; several reads mean several parameters.
|
|
258
|
+
- Keep the fetch for a *computed* key in `Provide()` with `IDecisionReads.Get`;
|
|
259
|
+
under `On`, `IReadModels` there is refused.
|
|
217
260
|
|
|
218
261
|
⚠️ **Two things the resolution does *not* do.** It does not key off the
|
|
219
262
|
`[ReadModel]` attribute — a read model becomes injectable because a Chronicle
|
|
@@ -245,9 +288,13 @@ where absence is *not* `null`.
|
|
|
245
288
|
## Step 6 — Specify the rejection
|
|
246
289
|
|
|
247
290
|
Every rule gets a specification of its failure case, using
|
|
248
|
-
`CommandScenario<TCommand>`. For
|
|
249
|
-
|
|
250
|
-
|
|
291
|
+
`CommandScenario<TCommand>`. For guarded reads, `new CommandScenario<T>()`
|
|
292
|
+
selects the in-process log from the command's mode and dependencies (do not call
|
|
293
|
+
`UseDecisionReads()`). Seed real events with `Given.ForEventSource(id).Events(...)`
|
|
294
|
+
(pinned read models cannot supply a guard), queue a competing
|
|
295
|
+
`AppendConcurrently(id, events...)` and assert
|
|
296
|
+
`ShouldHaveValidationErrorBecauseOf(ValidationResultReason.ConcurrencyViolation)`.
|
|
297
|
+
For the ordinary rejection, assert **both** that the command did not succeed
|
|
251
298
|
and that it failed *as a validation*:
|
|
252
299
|
|
|
253
300
|
```csharp
|
|
@@ -273,9 +320,9 @@ the fact. See
|
|
|
273
320
|
- **The rule never fires.** The read-model type has no backing Chronicle
|
|
274
321
|
projection, reducer, `DbSet<T>`, or MongoDB collection, so nothing registered
|
|
275
322
|
it for command-side resolution.
|
|
276
|
-
- **Two requests both win.**
|
|
277
|
-
moving it to `Handle()` or returning `Result<,>`
|
|
278
|
-
|
|
323
|
+
- **Two requests both win.** The project is on `Off` (the default) or `Measure`, so
|
|
324
|
+
the validator read is a hint; moving it to `Handle()` or returning `Result<,>`
|
|
325
|
+
does not guard it. Set `On`, or use a stream decision or a constraint.
|
|
279
326
|
- **`ARC0005` on build.** `Provide()` returns a value no `Handle()` parameter
|
|
280
327
|
consumes.
|
|
281
328
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cratis-chronicle-decision-reads
|
|
3
|
-
description:
|
|
3
|
+
description: Choose which mechanism makes a write-side rule hold (constraint, Arc read protection with read-model parameters and validators, per-stream read model, stream decision, aggregate, explicit scope, reactor repair or an advisory read), and guard a Chronicle decision directly with IDecisionReads, projection admission, ambient unit-of-work enrollment and commit conflict handling. Use when an append must remain valid for the facts read, when a rule must hold under concurrent writers, when choosing whole-source versus stream-scoped reads, or diagnosing ReadChanged, DecisionReadRefused and scope conflicts. Do not use for ordinary query reads or append-time uniqueness alone; Arc command shapes belong to cratis-arc-command.
|
|
4
4
|
license: MIT
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -17,9 +17,22 @@ consistency boundary (DCB), not authorization or a replacement for unique constr
|
|
|
17
17
|
|
|
18
18
|
Read [choosing how a rule holds](references/choosing-how-a-rule-holds.md) first
|
|
19
19
|
when the question is which mechanism to use, or before writing code that reads
|
|
20
|
-
state and then appends. It carries the routing
|
|
21
|
-
use,
|
|
22
|
-
|
|
20
|
+
state and then appends. It carries the routing by rule shape, the matrix with what
|
|
21
|
+
not to use, the refusals, rollout with Measure, testing and the traps.
|
|
22
|
+
|
|
23
|
+
In an Arc application new code sets read protection `On`
|
|
24
|
+
(`<CratisReadProtection>On</CratisReadProtection>`); `Off` is the default when
|
|
25
|
+
nothing is set. Under `On` the read model is a parameter of `Handle()` or of the
|
|
26
|
+
`CommandValidator<T>` constructor, and the read guards the append; under `Off` and
|
|
27
|
+
`Measure` a validator over a plain read is a hint that guards nothing. A plain read under
|
|
28
|
+
`Off`, `Result<,>` and `concurrency: true` are never the guard.
|
|
29
|
+
`[ProtectedDecision]` with `DecisionRead<T>` is the older per-command opt-in; it
|
|
30
|
+
still works and is not obsolete.
|
|
31
|
+
|
|
32
|
+
Requires Arc 22.106.0 (read protection) with Chronicle client and kernel 19.71.0.
|
|
33
|
+
Check `dotnet list package --include-transitive`; older Arc has only the
|
|
34
|
+
per-command `[ProtectedDecision]` path, and the choosing reference names where it
|
|
35
|
+
differs.
|
|
23
36
|
|
|
24
37
|
## Verified product sources
|
|
25
38
|
|
|
@@ -60,10 +73,12 @@ for the covered APIs, scope lifetime, rollback and immediate-append boundary.
|
|
|
60
73
|
|
|
61
74
|
## Route the work
|
|
62
75
|
|
|
63
|
-
- For Arc `[
|
|
64
|
-
use **cratis-arc-command** and
|
|
65
|
-
their unit-of-work ownership in
|
|
66
|
-
below is
|
|
76
|
+
- For Arc read protection, `[LookUpBy]`, `[Unprotected]`, `[ProtectedDecision]`,
|
|
77
|
+
command/handler shapes and Arc `IStreamReads`, use **cratis-arc-command** and
|
|
78
|
+
**cratis-arc-command-validation**; do not recreate their unit-of-work ownership in
|
|
79
|
+
a handler. The Chronicle `IDecisionReads` below is for code outside an Arc
|
|
80
|
+
command (endpoints, services, `Provide()` for a computed key), not a license to
|
|
81
|
+
invent an Arc wrapper shape.
|
|
67
82
|
- For a unique value claimed atomically or a closed stream, use
|
|
68
83
|
**cratis-chronicle-event-constraints**. A decision read guards one key's
|
|
69
84
|
history, not a value across keys: it is the wrong tool for uniqueness.
|
|
@@ -81,8 +96,12 @@ for the covered APIs, scope lifetime, rollback and immediate-append boundary.
|
|
|
81
96
|
`Admit<T>()` / `AdmitForStream<T>()` check shape **without I/O**, returning
|
|
82
97
|
`DecisionReadAdmission(IsAdmitted, Reason)`. The model needs an `Id` property
|
|
83
98
|
(string, or a Guid in lowercase hyphenated form) and an event-source-keyed flat
|
|
84
|
-
projection
|
|
85
|
-
|
|
99
|
+
projection, or an unfiltered reducer on the event log with no constructor
|
|
100
|
+
dependencies other than `ILogger<T>` (self-reducing models are exempt). Requires
|
|
101
|
+
Chronicle 19.68.0 for reducers. A filtered reducer refuses with `ReducerFilters`,
|
|
102
|
+
one with dependencies with `ReducerDependencies`, an event type at two generations
|
|
103
|
+
with `ReducerGeneration`, and a reducer in a stream-scoped read with
|
|
104
|
+
`StreamScopeNotSupported`. A missing `Id` or an unconvertible key refuses with `KeyConversion`. A successful shape check is not
|
|
86
105
|
runtime read success: definitions, key conversion and fold completion are also
|
|
87
106
|
checked while reading.
|
|
88
107
|
|
|
@@ -112,9 +131,12 @@ projection also needs an explicit stream id, not the default wildcard.
|
|
|
112
131
|
`AdmitForStream<T>()` checks this shape; unsupported readers/kernels refuse with
|
|
113
132
|
`StreamScopeNotSupported`. Do not fall back to an unguarded query on refusal.
|
|
114
133
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
134
|
+
In Arc, prefer a per-stream read model
|
|
135
|
+
(`[ReadModel, EventStream<TSource>("Transactions")]` on a command routed with
|
|
136
|
+
`[EventSource<TSource>("Transactions")]` and `[EventStreamId("{Month}")]`); use
|
|
137
|
+
the stream overloads of `IDecisionReads` in `Provide()` for another stream or
|
|
138
|
+
source. Requires Arc 22.81.0 for the stream overloads inside Arc commands to use
|
|
139
|
+
command protection, caching and enrollment; older Arc refuses them.
|
|
118
140
|
|
|
119
141
|
## Commit and inspect conflicts
|
|
120
142
|
|
|
@@ -158,8 +180,8 @@ deciding writer is guarded or the rule is a constraint.
|
|
|
158
180
|
- The unit commits in the ASP.NET Core middleware: it commits after the response
|
|
159
181
|
and reports no concurrency violation. Call
|
|
160
182
|
`IUnitOfWorkCompletionFeature.CommitAsync()` before responding.
|
|
161
|
-
-
|
|
162
|
-
|
|
183
|
+
- Arc under `Off` (the default when nothing is set): a command with a read model
|
|
184
|
+
is unguarded unless it carries `[ProtectedDecision]`. Set `On` for new code.
|
|
163
185
|
|
|
164
186
|
## Verify
|
|
165
187
|
|
package/package/corpus/skills/cratis-chronicle-decision-reads/references/admission-and-ownership.md
CHANGED
|
@@ -10,12 +10,13 @@ and `UnitOfWorkLifecyclePolicy.cs`.
|
|
|
10
10
|
|
|
11
11
|
## Why a shape is refused
|
|
12
12
|
|
|
13
|
-
Admission requires exactly one supported projection on the event log. It is not
|
|
13
|
+
Admission requires exactly one supported projection, or an unfiltered dependency-free reducer, on the event log. It is not
|
|
14
14
|
an adapter for arbitrary read-model storage:
|
|
15
15
|
|
|
16
16
|
| Reason | Shape to inspect |
|
|
17
17
|
| --- | --- |
|
|
18
|
-
| Reducer / AmbiguousProjection / NotEventLog |
|
|
18
|
+
| Reducer / AmbiguousProjection / NotEventLog | a model with no admitted definition, several projections, or another sequence |
|
|
19
|
+
| ReducerFilters / ReducerDependencies / ReducerGeneration | a reducer with an event filter, constructor dependencies other than `ILogger<T>` (self-reducing models are exempt), or an event type at two generations |
|
|
19
20
|
| Join / Hierarchy | joins, children or nested projections |
|
|
20
21
|
| OpenEndedEventTypes / Derivatives / FromEventProperty | all-events, FromEvery, or event-property routing |
|
|
21
22
|
| NotEventSourceKeyed / KeyConversion | keys not convertible to the admitted raw source key |
|