@cratis/pi 0.0.1

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.
Files changed (125) hide show
  1. package/README.md +43 -0
  2. package/package.json +24 -0
  3. package/skills/cratis-application-react-specifications/LICENSE +2 -0
  4. package/skills/cratis-application-react-specifications/SKILL.md +192 -0
  5. package/skills/cratis-application-slice-specifications/LICENSE +2 -0
  6. package/skills/cratis-application-slice-specifications/SKILL.md +222 -0
  7. package/skills/cratis-arc-authentication-authorization-and-identity/LICENSE +2 -0
  8. package/skills/cratis-arc-authentication-authorization-and-identity/SKILL.md +233 -0
  9. package/skills/cratis-arc-authentication-authorization-and-identity/references/authentication.md +140 -0
  10. package/skills/cratis-arc-authentication-authorization-and-identity/references/authorization.md +112 -0
  11. package/skills/cratis-arc-authentication-authorization-and-identity/references/frontend.md +102 -0
  12. package/skills/cratis-arc-authentication-authorization-and-identity/references/local-development.md +85 -0
  13. package/skills/cratis-arc-authentication-authorization-and-identity/references/tenancy.md +87 -0
  14. package/skills/cratis-arc-command/LICENSE +2 -0
  15. package/skills/cratis-arc-command/SKILL.md +286 -0
  16. package/skills/cratis-arc-command/references/command-result.md +111 -0
  17. package/skills/cratis-arc-command/references/handler-shapes.md +135 -0
  18. package/skills/cratis-arc-command/references/proxy-generation.md +87 -0
  19. package/skills/cratis-arc-command/references/read-model-injection.md +97 -0
  20. package/skills/cratis-arc-command-execution/LICENSE +2 -0
  21. package/skills/cratis-arc-command-execution/SKILL.md +148 -0
  22. package/skills/cratis-arc-command-validation/LICENSE +2 -0
  23. package/skills/cratis-arc-command-validation/SKILL.md +230 -0
  24. package/skills/cratis-arc-ef-core-migration/LICENSE +2 -0
  25. package/skills/cratis-arc-ef-core-migration/SKILL.md +231 -0
  26. package/skills/cratis-arc-observable-query-http/LICENSE +2 -0
  27. package/skills/cratis-arc-observable-query-http/SKILL.md +157 -0
  28. package/skills/cratis-arc-query-paging/LICENSE +2 -0
  29. package/skills/cratis-arc-query-paging/SKILL.md +173 -0
  30. package/skills/cratis-arc-react-page/LICENSE +2 -0
  31. package/skills/cratis-arc-react-page/SKILL.md +291 -0
  32. package/skills/cratis-arc-react-page/references/data-page.md +125 -0
  33. package/skills/cratis-arc-react-page/references/data-tables.md +136 -0
  34. package/skills/cratis-arc-react-page/references/dialogs.md +238 -0
  35. package/skills/cratis-arc-react-page/references/mvvm.md +150 -0
  36. package/skills/cratis-arc-react-page/references/queries-and-commands.md +200 -0
  37. package/skills/cratis-chronicle-cli-operations/LICENSE +2 -0
  38. package/skills/cratis-chronicle-cli-operations/SKILL.md +199 -0
  39. package/skills/cratis-chronicle-client-dotnet/LICENSE +2 -0
  40. package/skills/cratis-chronicle-client-dotnet/SKILL.md +433 -0
  41. package/skills/cratis-chronicle-client-elixir/LICENSE +2 -0
  42. package/skills/cratis-chronicle-client-elixir/SKILL.md +429 -0
  43. package/skills/cratis-chronicle-client-kotlin/LICENSE +2 -0
  44. package/skills/cratis-chronicle-client-kotlin/SKILL.md +498 -0
  45. package/skills/cratis-chronicle-client-typescript/LICENSE +2 -0
  46. package/skills/cratis-chronicle-client-typescript/SKILL.md +396 -0
  47. package/skills/cratis-chronicle-compliance/LICENSE +2 -0
  48. package/skills/cratis-chronicle-compliance/SKILL.md +246 -0
  49. package/skills/cratis-chronicle-event-constraints/LICENSE +2 -0
  50. package/skills/cratis-chronicle-event-constraints/SKILL.md +227 -0
  51. package/skills/cratis-chronicle-event-modeling/LICENSE +2 -0
  52. package/skills/cratis-chronicle-event-modeling/SKILL.md +157 -0
  53. package/skills/cratis-chronicle-event-specifications/LICENSE +2 -0
  54. package/skills/cratis-chronicle-event-specifications/SKILL.md +200 -0
  55. package/skills/cratis-chronicle-event-type-migration/LICENSE +2 -0
  56. package/skills/cratis-chronicle-event-type-migration/SKILL.md +235 -0
  57. package/skills/cratis-chronicle-mcp-inspection/LICENSE +2 -0
  58. package/skills/cratis-chronicle-mcp-inspection/SKILL.md +79 -0
  59. package/skills/cratis-chronicle-mcp-inspection/references/blocked-tools.md +12 -0
  60. package/skills/cratis-chronicle-mcp-inspection/references/observational-tools.md +8 -0
  61. package/skills/cratis-chronicle-multi-tenancy/LICENSE +2 -0
  62. package/skills/cratis-chronicle-multi-tenancy/SKILL.md +198 -0
  63. package/skills/cratis-chronicle-projection/LICENSE +2 -0
  64. package/skills/cratis-chronicle-projection/SKILL.md +162 -0
  65. package/skills/cratis-chronicle-projection/references/fluent-builder.md +126 -0
  66. package/skills/cratis-chronicle-projection/references/model-bound-attributes.md +118 -0
  67. package/skills/cratis-chronicle-reactor/LICENSE +2 -0
  68. package/skills/cratis-chronicle-reactor/SKILL.md +344 -0
  69. package/skills/cratis-chronicle-read-model/LICENSE +2 -0
  70. package/skills/cratis-chronicle-read-model/SKILL.md +193 -0
  71. package/skills/cratis-chronicle-read-model/references/queries.md +142 -0
  72. package/skills/cratis-chronicle-read-model-specifications/LICENSE +2 -0
  73. package/skills/cratis-chronicle-read-model-specifications/SKILL.md +176 -0
  74. package/skills/cratis-chronicle-reducer/LICENSE +2 -0
  75. package/skills/cratis-chronicle-reducer/SKILL.md +173 -0
  76. package/skills/cratis-chronicle-web-workbench/LICENSE +2 -0
  77. package/skills/cratis-chronicle-web-workbench/SKILL.md +177 -0
  78. package/skills/cratis-cli-terminal-workbench/LICENSE +2 -0
  79. package/skills/cratis-cli-terminal-workbench/SKILL.md +156 -0
  80. package/skills/cratis-code-review/LICENSE +2 -0
  81. package/skills/cratis-code-review/SKILL.md +177 -0
  82. package/skills/cratis-components-accessibility/LICENSE +2 -0
  83. package/skills/cratis-components-accessibility/SKILL.md +176 -0
  84. package/skills/cratis-components-schema-editor/LICENSE +2 -0
  85. package/skills/cratis-components-schema-editor/SKILL.md +238 -0
  86. package/skills/cratis-components-stepper-command-dialog/LICENSE +2 -0
  87. package/skills/cratis-components-stepper-command-dialog/SKILL.md +209 -0
  88. package/skills/cratis-components-styling/LICENSE +2 -0
  89. package/skills/cratis-components-styling/SKILL.md +221 -0
  90. package/skills/cratis-components-toolbar/LICENSE +2 -0
  91. package/skills/cratis-components-toolbar/SKILL.md +259 -0
  92. package/skills/cratis-documentation-writing/LICENSE +2 -0
  93. package/skills/cratis-documentation-writing/SKILL.md +121 -0
  94. package/skills/cratis-event-model-diagram/LICENSE +2 -0
  95. package/skills/cratis-event-model-diagram/SKILL.md +142 -0
  96. package/skills/cratis-fundamentals-concept/LICENSE +2 -0
  97. package/skills/cratis-fundamentals-concept/SKILL.md +198 -0
  98. package/skills/cratis-fundamentals-type-discovery/LICENSE +2 -0
  99. package/skills/cratis-fundamentals-type-discovery/SKILL.md +152 -0
  100. package/skills/cratis-governed-release-methodology/LICENSE +2 -0
  101. package/skills/cratis-governed-release-methodology/SKILL.md +297 -0
  102. package/skills/cratis-lens-browser-extension/LICENSE +2 -0
  103. package/skills/cratis-lens-browser-extension/SKILL.md +239 -0
  104. package/skills/cratis-performance-review/LICENSE +2 -0
  105. package/skills/cratis-performance-review/SKILL.md +139 -0
  106. package/skills/cratis-screenplay-model-authoring/LICENSE +2 -0
  107. package/skills/cratis-screenplay-model-authoring/SKILL.md +231 -0
  108. package/skills/cratis-security-review/LICENSE +2 -0
  109. package/skills/cratis-security-review/SKILL.md +141 -0
  110. package/skills/cratis-specification-by-example/LICENSE +2 -0
  111. package/skills/cratis-specification-by-example/SKILL.md +188 -0
  112. package/skills/cratis-specifications-csharp/LICENSE +2 -0
  113. package/skills/cratis-specifications-csharp/SKILL.md +230 -0
  114. package/skills/cratis-specifications-csharp/references/application-scenarios.md +191 -0
  115. package/skills/cratis-specifications-csharp/references/csharp-patterns.md +142 -0
  116. package/skills/cratis-specifications-csharp/references/integration-specs.md +126 -0
  117. package/skills/cratis-specifications-typescript/LICENSE +2 -0
  118. package/skills/cratis-specifications-typescript/SKILL.md +202 -0
  119. package/skills/cratis-specifications-typescript/references/typescript-patterns.md +168 -0
  120. package/skills/cratis-stage-rendering-and-sandbox/LICENSE +2 -0
  121. package/skills/cratis-stage-rendering-and-sandbox/SKILL.md +230 -0
  122. package/skills/cratis-studio-mcp-safety-guidance/LICENSE +2 -0
  123. package/skills/cratis-studio-mcp-safety-guidance/SKILL.md +69 -0
  124. package/skills/cratis-studio-mcp-safety-guidance/references/blocked-tools.md +12 -0
  125. package/skills/cratis-studio-mcp-safety-guidance/references/observational-tools.md +8 -0
@@ -0,0 +1,87 @@
1
+ # Proxy generation
2
+
3
+ Verified against `Cratis.Arc.ProxyGenerator.Build` `22.10.4`.
4
+
5
+ ## Wiring
6
+
7
+ Reference the build package from the project that contains the commands,
8
+ queries and read models, and set an output path:
9
+
10
+ ```xml
11
+ <PackageReference Include="Cratis.Arc.ProxyGenerator.Build" Version="22.10.4" />
12
+
13
+ <PropertyGroup>
14
+ <CratisProxiesOutputPath>$(MSBuildThisFileDirectory)../<Web>/src/api</CratisProxiesOutputPath>
15
+ </PropertyGroup>
16
+ ```
17
+
18
+ Install `@cratis/arc` and `@cratis/arc.react` on the frontend side.
19
+
20
+ The package adds a `CratisProxyGenerator` target that runs `AfterTargets="AfterBuild"`,
21
+ and its condition is `'$(CratisProxiesOutputPath)' != ''`. **A missing output
22
+ path is not an error — the target simply never runs and nothing is generated.**
23
+ That is the first thing to check when a build succeeds but no proxies appear.
24
+
25
+ Output folders mirror the C# **namespace**, not the file path. A command in
26
+ `<Root>.<Feature>` lands in `<output>/<Feature>/`.
27
+
28
+ ## MSBuild properties
29
+
30
+ Every property below is declared by the package with the default shown.
31
+
32
+ | Property | Default | Effect |
33
+ | --- | --- | --- |
34
+ | `CratisProxiesOutputPath` | empty | Where to write. Empty disables generation entirely |
35
+ | `CratisProxiesInputAssembly` | empty | Overrides the assembly to read |
36
+ | `CratisProxiesSegmentsToSkip` | empty | Leading namespace segments to drop from the output path |
37
+ | `CratisProxiesLibraryMode` | `false` | Generates for a library rather than an application |
38
+ | `CratisProxiesSkipOutputDeletion` | **`true`** | Already the default — the generator does not wipe the output folder |
39
+ | `CratisProxiesSkipCommandNameInRoute` | `false` | Leaves the command name out of the generated route |
40
+ | `CratisProxiesSkipQueryNameInRoute` | `false` | Leaves the query name out of the generated route |
41
+ | `CratisProxiesApiPrefix` | `api` | Route prefix |
42
+ | `CratisProxiesSkipFileIndexTracking` | `false` | Disables tracking which files the generator owns |
43
+
44
+ ⚠️ `CratisProxiesSkipOutputDeletion` defaults to `true`. Guidance that tells you
45
+ to set it to `true` "so the generator does not delete your hand-written files"
46
+ is describing the default, not a fix.
47
+
48
+ Item groups the target also forwards: `AssemblyToPackageMapping`,
49
+ `ExcludeType`, `ExcludeNamespace`, `NamespaceRoot`, and `TypeToTsType`.
50
+
51
+ ## What the generator extracts for client-side validation
52
+
53
+ The generated command proxy carries the rules it could extract, so
54
+ `validateClientSide()` can run them without a round trip. Extraction reads
55
+ validators derived from `AbstractValidator<T>`, `BaseValidator<T>`,
56
+ `DiscoverableValidator<T>`, `CommandValidator<T>` and `QueryValidator<T>`.
57
+
58
+ Recognised FluentValidation rules:
59
+
60
+ - not null and not empty;
61
+ - email;
62
+ - length, minimum length, maximum length, exact length;
63
+ - comparison rules (greater/less than and their or-equal forms);
64
+ - regular expression.
65
+
66
+ Recognised data annotations: `[Required]`, `[StringLength]`, `[MinLength]`,
67
+ `[MaxLength]`, `[Range]`, `[RegularExpression]`, `[EmailAddress]`, `[Phone]`,
68
+ `[Url]`, `[CreditCard]`.
69
+
70
+ Rules declared on a `ConceptValidator<T>` are projected onto every property
71
+ typed as that concept, so declaring a value's format once validates it in the
72
+ browser too.
73
+
74
+ ⚠️ **`Must(...)` is not extracted.** Neither is any rule that needs an injected
75
+ dependency. Those run on the server only, which means
76
+ `validateClientSide()` can pass while `execute()` still fails validation.
77
+ FluentValidation rules win over data annotations where both describe the same
78
+ property.
79
+
80
+ ## Failure modes
81
+
82
+ | Symptom | Cause |
83
+ | --- | --- |
84
+ | Nothing is generated | `CratisProxiesOutputPath` is unset, or the project did not build |
85
+ | Files appear under an unexpected folder | The folder mirrors the namespace; change the namespace or use `NamespaceRoot`/`CratisProxiesSegmentsToSkip` |
86
+ | Stale proxies after a rename | `dotnet clean` then `dotnet build`; the generator does not delete by default |
87
+ | A rule validates on the server but not in the browser | The rule is not in the extractable set above |
@@ -0,0 +1,97 @@
1
+ # Injecting a read model into a command
2
+
3
+ Verified against `Cratis.Arc.Core` `22.10.4` and `Cratis.Chronicle` `16.39.1`.
4
+
5
+ A read model can be injected into a `CommandValidator<T>` constructor, into
6
+ `Provide()`, or into `Handle()`. Arc resolves it through an
7
+ `ICanResolveReadModelForCommand` provider, **by the command's resolved key** —
8
+ not by the read-model type, and not by "the property that looks like its key".
9
+
10
+ If the instance you need is keyed by anything else, direct injection hands you
11
+ the wrong instance or nothing at all. That is a correctness bug, not a compile
12
+ error. The three shapes it takes:
13
+
14
+ 1. you need a *referenced other* entity — act on A, check B;
15
+ 2. the command has more than one candidate identity property, so resolution is
16
+ ambiguous;
17
+ 3. the id is generated inside `Handle()` and does not exist yet.
18
+
19
+ In all three, read by explicit key in `Provide()`:
20
+
21
+ ```csharp
22
+ var other = await readModels.GetInstanceById<<Other>>((EventSourceId)<OtherId>);
23
+ ```
24
+
25
+ `ReadModelKey` converts implicitly from `EventSourceId`, `Guid` and `string`, so
26
+ the cast is all that is needed.
27
+
28
+ ## Nullability is the required/optional switch
29
+
30
+ | Parameter | No instance for the command's key |
31
+ | --- | --- |
32
+ | `TReadModel?` | `null` is injected — guard with `is null` |
33
+ | `TReadModel` | Rejected, not crashed: a **registered** read model with a usable resolved key raises `ReadModelDoesNotExistForCommand`, an `IValidationFailure` that surfaces as a validation failure (HTTP 400) with reason `DependencyUnavailable` and a message that does not name the type |
34
+
35
+ The server-fault path is only reached when the dependency is *not* a registered
36
+ read model, or when no usable key was resolved. Then it is
37
+ `CannotResolveCommandDependency` for a handler parameter and
38
+ `CannotResolveValidatorDependency` for a validator constructor parameter — both
39
+ of whose messages tell you to declare the parameter nullable or to inject
40
+ `IReadModels` and check existence explicitly.
41
+
42
+ A command that carries no usable key at all fails inside the resolver with
43
+ `UnableToResolveReadModelFromCommandContext`, which is also a client-input
44
+ failure rather than a server fault.
45
+
46
+ `ARC0006` warns when a read-model parameter is non-nullable, precisely because a
47
+ command-scoped read model can be missing. Make it nullable when absence is part
48
+ of the command's valid behavior; keep it non-nullable when absence really is a
49
+ rejection.
50
+
51
+ ## `null` is not always what absence looks like
52
+
53
+ Chronicle's `IReadModels.GetInstanceById<T>` is declared as a non-nullable
54
+ `Task<TReadModel>` yet hands back `default!` when nothing exists, so the compiler
55
+ gives callers no signal at all.
56
+
57
+ | Backing | Never created | Removed by a `[RemovedWith<T>]` event |
58
+ | --- | --- | --- |
59
+ | Materialized projection or reducer (the default) | `null` | `null` |
60
+ | `[Passive]` **projection** | ⚠️ a **default-valued instance**, never `null` | `null` |
61
+ | `[Passive]` **reducer** | `null` | `null` |
62
+
63
+ `[RemovedWith<T>]` is not the axis — a removed instance is `null` on every
64
+ backing. The single non-`null` case is a `[Passive]` projection that was never
65
+ created: Chronicle computes it on demand and seeds the initial state from the
66
+ read model's schema, filling every non-nullable value property with its type
67
+ default. Neither the `is null` guard nor the non-nullable "must exist" switch
68
+ fires there.
69
+
70
+ ⚠️ This is invisible when a status enum's `0` is a real state — absent becomes
71
+ byte-identical to freshly created, so a command reads never-invited as already
72
+ invited. Renumbering the enum from `1` is **not** the fix; the value is then
73
+ dropped from the payload and deserializes back to CLR `0` on the client anyway.
74
+ Carry an explicit existence flag instead: one
75
+ `[SetValue<TFirstEvent>(true)] bool Exists` per event that can be the first for
76
+ the stream, and check that rather than nullability.
77
+
78
+ ⚠️ A specification suite will not catch this by itself. The command scenario
79
+ harness answers `null` for an unseeded event-source id, which matches production
80
+ for every row above *except* the passive projection. Cover the absent case by
81
+ seeding a default-valued instance explicitly, not only `null`.
82
+
83
+ Write your own by-id accessors as `Task<T?>` so callers get the compiler signal
84
+ Chronicle's own signature withholds.
85
+
86
+ ## Which provider owns a read model type
87
+
88
+ More than one provider can resolve the same read model type, and the application
89
+ does not control registration order. `ReadModelForCommandOwnership` decides,
90
+ not order:
91
+
92
+ - `Declared` — something in the application says the provider owns the type: a
93
+ Chronicle projection or reducer targeting it, or a `DbSet<T>` on a read-model
94
+ `DbContext`. A declaring provider claims the type even when something else
95
+ already resolves it.
96
+ - `Fallback` — the provider *can* resolve anything it reports, but nothing says
97
+ it owns them. It claims only the types nothing else already resolves.
@@ -0,0 +1,2 @@
1
+ Copyright (c) Cratis. All rights reserved.
2
+ Licensed under the MIT license. See LICENSE file in the project root for full license information.
@@ -0,0 +1,148 @@
1
+ ---
2
+ name: cratis-arc-command-execution
3
+ description: Execute an existing Cratis Arc command from backend code through ICommandPipeline — the scopeless and scoped overloads, the typed Execute<TResult>, the pre-flight Validate, validation severity filtering, and how to read CommandResult. Use when a reactor, background job, scheduled task, or backend service must run a command without an HTTP request. Do not use to define a command.
4
+ license: MIT
5
+ ---
6
+
7
+ # Execute an Arc command from code
8
+
9
+ `ICommandPipeline` is the same entry point the HTTP boundary uses. Everything
10
+ downstream of the entry point is identical: authorization filters, validation
11
+ filters, argument resolution including `Provide()`, `Handle()`, and response
12
+ dispatch.
13
+
14
+ ## Verified product sources
15
+
16
+ | Package | Version | Purpose |
17
+ | --- | --- | --- |
18
+ | `Cratis.Arc.Core` | `22.10.4` | `Cratis.Arc.Commands.ICommandPipeline`, `CommandResult`, `ValidationResultSeverity` |
19
+ | `Cratis.Arc.Chronicle` | `22.10.4` | analyzer `ARCCHR0006` for reactor replay |
20
+
21
+ Reverify before claiming support for another version.
22
+
23
+ ## When this is the wrong tool
24
+
25
+ `ICommandPipeline` is for **command-shaped** work that needs authorization and
26
+ validation. It is not the way to append an event to another stream from inside a
27
+ handler — return an `EventForEventSourceId` from `Handle()` instead, and never
28
+ inject `IEventLog` (`ARCCHR0007`).
29
+
30
+ ## Inject and execute
31
+
32
+ ```csharp
33
+ public class <Name>Reactor(ICommandPipeline pipeline) : IReactor
34
+ {
35
+ [OnceOnly]
36
+ public Task <Something>Happened(<Something>Happened @event, EventContext context) =>
37
+ pipeline.Execute(new <TargetCommand>(@event.<Id>, @event.<Value>));
38
+ }
39
+ ```
40
+
41
+ `Execute(command)` is the scopeless overload: the pipeline creates its own DI
42
+ scope for the call. This is the normal case.
43
+
44
+ ## The overloads
45
+
46
+ | Overload | Use it when |
47
+ | --- | --- |
48
+ | `Execute(object command, ValidationResultSeverity? allowedSeverity = default)` | The normal case |
49
+ | `Execute(object command, IServiceProvider serviceProvider, ValidationResultSeverity? allowedSeverity = default)` | The command's dependencies must come from the caller's existing scope |
50
+ | `Execute<TResult>(object command, ...)` | The handler returns a response you need typed |
51
+ | `Validate(object command, ...)` | You need the verdict without any side effect |
52
+
53
+ Each has both the scopeless and the scoped form. Prefer the scopeless one: the
54
+ scoped overload exists for a caller that genuinely owns a scope the command's
55
+ dependencies must share.
56
+
57
+ ```csharp
58
+ var result = await pipeline.Execute<<ResponseType>>(new <CommandName>(<args>));
59
+ if (result.IsSuccess)
60
+ {
61
+ var response = result.Response;
62
+ }
63
+ ```
64
+
65
+ ⚠️ `Execute<TResult>` throws `InvalidCastException` when the handler returned a
66
+ response of a different type. When the handler returns no response, or the
67
+ command failed for any reason, `Response` is `default` — a failed result never
68
+ carries a response, because the pipeline clears it once the execution scopes
69
+ have completed.
70
+
71
+ ## Pre-flight `Validate`
72
+
73
+ `await pipeline.Validate(command)` runs the authorization and validation filters
74
+ and stops there — `Provide()` and `Handle()` never run, so there is no side
75
+ effect. Use it to decide whether to commit to work that surrounds the command.
76
+
77
+ ## Validation severity filtering
78
+
79
+ Both `Execute` and `Validate` take an optional
80
+ `ValidationResultSeverity? allowedSeverity` from `Cratis.Arc.Validation`. The
81
+ levels are `Unknown = 0`, `Information = 1`, `Warning = 2`, `Error = 3`.
82
+
83
+ - With no `allowedSeverity`, **only `Error` blocks**, and information and
84
+ warnings are filtered off the result entirely — they never reach the caller.
85
+ - With an `allowedSeverity`, anything **strictly greater** blocks. Passing
86
+ `Warning` lets warnings through onto the result while errors still block.
87
+
88
+ ## Read the result by the specific flag
89
+
90
+ `IsSuccess` is derived — `IsAuthorized && IsValid && !HasExceptions`. Asserting
91
+ only on it cannot distinguish a forbidden caller from a rejected input from a
92
+ crash.
93
+
94
+ | Member | Meaning |
95
+ | --- | --- |
96
+ | `IsAuthorized` | The identity satisfied the command's authorization; `AuthorizationFailureReason` says why not |
97
+ | `IsValid` | Derived: no blocking validation results survived. `ValidationResults` carries them |
98
+ | `HasExceptions` | Derived: `ExceptionMessages` is non-empty. `ExceptionStackTrace` has the detail |
99
+ | `CorrelationId` | The pipeline-wide id to correlate logs and traces on |
100
+
101
+ A command with no registered handler comes back with an exception message naming
102
+ the command type rather than throwing.
103
+
104
+ Surface these through the result. Do not convert `!IsValid` into a thrown
105
+ exception — that discards the structured failure the caller needs.
106
+
107
+ ## Reactors must decide what replay does
108
+
109
+ A reactor handler that executes a command is not idempotent: a replay
110
+ (redaction, revision, observer rewind) runs the handler again and executes the
111
+ command again. `ARCCHR0006` warns when a reactor handler invokes
112
+ `ICommandPipeline.Execute` without saying what replay should do. Mark the
113
+ handler `[OnceOnly]` so it is skipped during replay.
114
+
115
+ ⚠️ `[OnceOnly]` is **replay exclusion, not deduplication**. There is no ledger
116
+ of events a handler has already seen: recovering a failed partition re-delivers
117
+ the event as an ordinary observation, and a `[OnceOnly]` handler runs again.
118
+ Make the executed command idempotent regardless — and when replay exclusion is
119
+ the wrong tool, suppress the diagnostic with a justification rather than
120
+ mis-marking it to silence the warning.
121
+
122
+ ## Common mistakes
123
+
124
+ | Mistake | Why it hurts |
125
+ | --- | --- |
126
+ | Throwing on `!result.IsValid` | Loses `ValidationResults`, turns a 400-shaped rejection into a fault |
127
+ | Executing from a reactor without deciding about replay | Every replay re-fires the command |
128
+ | Reaching for the scoped overload by default | Couples the command to the caller's scope for no reason |
129
+ | Asserting only `IsSuccess` in a spec | A spec passes with every rule neutered |
130
+ | Reading `Response` without checking success first | It is always `default` on failure |
131
+
132
+ ## Verify
133
+
134
+ - The caller branches on `IsAuthorized`, `IsValid` and `HasExceptions`, not only
135
+ on `IsSuccess`.
136
+ - No expected failure is converted into a thrown exception.
137
+ - A reactor that executes a command has an explicit replay decision.
138
+ - The scoped overload is used only where the shared scope is required.
139
+ - `Execute<TResult>`'s type argument matches what the handler actually returns.
140
+ - `dotnet build` is clean in Debug and Release with `ARCCHR0006` and
141
+ `ARCCHR0007` silent or explicitly justified.
142
+
143
+ ## Route near misses
144
+
145
+ - Defining the command or choosing its `Handle()` return shape:
146
+ `cratis-arc-command`.
147
+ - Adding or changing a rule: the Arc command validation guidance.
148
+ - Designing what a reactor should observe: the Chronicle reactor guidance.
@@ -0,0 +1,2 @@
1
+ Copyright (c) Cratis. All rights reserved.
2
+ Licensed under the MIT license. See LICENSE file in the project root for full license information.
@@ -0,0 +1,230 @@
1
+ ---
2
+ name: cratis-arc-command-validation
3
+ description: Add a rejection rule to an existing Arc command — choosing between ConceptValidator, CommandValidator, a short-circuiting Provide, and a read-model-injected Handle returning Result<TEvent, ValidationResult>. Use when a command must refuse work under some condition. Do not use to define a new command, and do not use for append-time Chronicle constraints.
4
+ license: MIT
5
+ ---
6
+
7
+ # Arc command validation
8
+
9
+ Every rejection a command can produce has exactly one right home. Putting a rule
10
+ in the wrong one is not a style problem: a value invariant written per-command
11
+ stops travelling with the value, and a state-dependent rule written in a
12
+ validator loses the race it exists to win.
13
+
14
+ **Never throw for a normal business rejection.** An exception out of `Provide()`
15
+ or `Handle()` is caught by the pipeline and merged as `HasExceptions`, which the
16
+ HTTP layer maps to **500** — not a validation result. Recoverable, user-facing
17
+ rejections are validation. (The one exception: an exception type implementing
18
+ `IValidationFailure` is converted to a validation failure and returns 400. Arc
19
+ uses that for its own dependency-resolution failures; do not build application
20
+ rejection on it.)
21
+
22
+ ## Verified product sources
23
+
24
+ | Package | Version | Purpose |
25
+ | --- | --- | --- |
26
+ | `Cratis.Arc.Core` | `22.10.4` | `CommandValidator<T>`, `ConceptValidator<T>`, `ValidationResult`, `CommandResult`, the command pipeline, `ARC0004`–`ARC0013` |
27
+ | `Cratis.Fundamentals` | `7.18.2` | `Cratis.Monads.Result<TResult, TError>` |
28
+ | `Cratis.Chronicle` | `16.45.2` | Read-model resolution for command-side injection |
29
+ | `FluentValidation` | `12.1.1` | `RuleFor(...)`, `.WithMessage(...)` |
30
+
31
+ Reverify against the owning product repository before claiming support for
32
+ another version.
33
+
34
+ ## Route near misses
35
+
36
+ - Defining a new command, its `Handle()` return shape, or event-source id
37
+ resolution: use `cratis-arc-command`.
38
+ - Uniqueness, or any rule the **event store** must enforce at append time: use
39
+ `cratis-chronicle-event-constraints`. A read-model pre-check for uniqueness is
40
+ not race-safe and is the classic wrong answer here.
41
+ - Authorization: that is a filter and an attribute at the boundary, not a
42
+ validator. Use `cratis-arc-authentication-authorization-and-identity`.
43
+ - Writing the specification for the rejection: use
44
+ `cratis-application-slice-specifications`.
45
+
46
+ ## Step 1 — Choose the mechanism by what the decision *is*
47
+
48
+ | The rule is… | Put it in |
49
+ | --- | --- |
50
+ | A reusable value invariant — length, format, range | `ConceptValidator<T>` on the concept type |
51
+ | Command input, cross-field, or a pre-handler check | `CommandValidator<TCommand>` with `RuleFor(...)` |
52
+ | A dependency on data the handler must fetch first | `Provide()` — fetch, and short-circuit if unusable |
53
+ | State-dependent and must hold **under concurrency** | Inject the read model into `Handle()`, return `Result<TEvent, ValidationResult>` |
54
+ | Uniqueness | A Chronicle constraint, never a command-side read |
55
+ | A genuine defect or missing infrastructure | `throw` a domain exception |
56
+
57
+ The pipeline runs **authorization → validation → `Provide()` → `Handle()`**.
58
+ Authorization filters are explicitly ordered ahead of validation filters, so a
59
+ caller who may not act never learns whether their input was valid.
60
+
61
+ ## Step 2 — Value invariants: `ConceptValidator<T>`
62
+
63
+ ```csharp
64
+ public class EmailAddressValidator : ConceptValidator<EmailAddress>
65
+ {
66
+ public EmailAddressValidator() => RuleFor(_ => _.Value).EmailAddress();
67
+ }
68
+ ```
69
+
70
+ A `ConceptValidator<T>` is discovered automatically and applies **globally**.
71
+ Arc walks the whole object graph of every model it validates and runs a
72
+ validator resolved by each node's runtime type — so the rule fires on every
73
+ command *and every query* carrying that concept, wherever it appears. That is
74
+ the point: declare the invariant once and it cannot be forgotten on the next
75
+ command.
76
+
77
+ When one command legitimately needs the concept unchecked, opt that property out
78
+ in its own validator with `.IgnoreConceptRules()` rather than weakening the
79
+ concept rule for everyone.
80
+
81
+ `ARC0013` warns when a validator rule dereferences a possibly-null concept
82
+ member.
83
+
84
+ ## Step 3 — Command input rules: `CommandValidator<T>`
85
+
86
+ ```csharp
87
+ public class TransferFundsValidator : CommandValidator<TransferFunds>
88
+ {
89
+ public TransferFundsValidator() =>
90
+ RuleFor(c => c.Amount).GreaterThan(0).WithMessage("Amount must be positive.");
91
+ }
92
+ ```
93
+
94
+ - `CommandValidator<T>` is a FluentValidation `AbstractValidator<T>` underneath,
95
+ so `RuleFor`, the standard rule set, and `.WithMessage(...)` are
96
+ FluentValidation's. Arc adds one overload that unwraps a `ConceptAs<T>` to its
97
+ primitive so `RuleFor(c => c.SomeConcept)` reads naturally.
98
+ - The validator is discovered by type — no registration.
99
+ - Constructor dependencies (read models, services) are injected. A read model
100
+ injected here resolves the same way as one injected into `Handle()`; see
101
+ Step 5 for what "the same way" actually means.
102
+ - **Omit the validator entirely when there are no rules.** An empty subclass is
103
+ noise the reader has to check.
104
+ - Single-property intrinsic rules belong on `ConceptValidator<T>` instead, so
105
+ they travel with the value everywhere.
106
+ - A validator that itself throws does not become a 500 — Arc catches it and
107
+ emits a validation error with reason `ValidatorFailed`, returning 400.
108
+
109
+ ## Step 4 — Data the handler needs: `Provide()`
110
+
111
+ `Provide()` runs after authorization and validation and before `Handle()`. Its
112
+ parameters resolve from dependency injection; the command instance is `this`.
113
+
114
+ ```csharp
115
+ public async Task<Result<Customer, ValidationResult>> Provide(IReadModels readModels)
116
+ {
117
+ var customer = await readModels.GetInstanceById<Customer>((EventSourceId)CustomerId);
118
+ return customer is null ? ValidationResult.Error("Customer must exist.") : customer;
119
+ }
120
+ ```
121
+
122
+ - Returning a `ValidationResult`, an `IEnumerable<ValidationResult>`, an
123
+ `AuthorizationResult`, or a `CommandResult` short-circuits: `Handle()` is
124
+ never invoked.
125
+ - A `Result<TProvided, ValidationResult>` has its inner value unwrapped, so the
126
+ success arm flows to `Handle()` and the error arm short-circuits.
127
+ - A returned tuple is flattened and its elements bound to `Handle()` parameters
128
+ by type. **Every provided value must be consumed by a `Handle()` parameter** —
129
+ an unused one is `ARC0005`.
130
+ - Keep the fetch in `Provide()` and the decision in `Handle()`. Do not write a
131
+ pass-through `Provide()` that only forwards a read model `Handle()` could take
132
+ directly, and do not duplicate one rejection in both the validator and
133
+ `Provide()` — pick one owner.
134
+
135
+ ## Step 5 — Concurrency-sensitive rules: the read model in `Handle()`
136
+
137
+ Use this when the rule depends on accumulated event-sourced state — a count, a
138
+ running total — and must still hold when two requests arrive at once. Arc
139
+ resolves the read model before `Handle()` runs and the handler returns a
140
+ `Result<TEvent, ValidationResult>`: success carries the event, failure carries a
141
+ typed validation error.
142
+
143
+ ```csharp
144
+ using Cratis.Monads;
145
+
146
+ [Command]
147
+ public record AddItemToCart(CartId CartId, ItemId ItemId)
148
+ {
149
+ /// <summary>Adds the item; rejects when the cart already holds the maximum.</summary>
150
+ /// <param name="cart">The current cart summary, resolved by the command's event-source id.</param>
151
+ /// <returns>The event on success, or a validation error.</returns>
152
+ public Result<ItemAddedToCart, ValidationResult> Handle(CartSummary cart) =>
153
+ cart.ItemCount >= 3
154
+ ? ValidationResult.Error("A cart can hold at most 3 items.")
155
+ : new ItemAddedToCart(ItemId);
156
+ }
157
+ ```
158
+
159
+ - `Result<TSuccess, TError>` is `Cratis.Monads.Result<,>` from Cratis
160
+ Fundamentals — **success first, error second** — and needs
161
+ `using Cratis.Monads;`. Its implicit conversions are what let both `return`
162
+ statements above compile without wrapping.
163
+ - **Return the error; never throw it.**
164
+ - One parameter per logical read model; several reads mean several parameters.
165
+
166
+ ⚠️ **Two things the resolution does *not* do.** It does not key off the
167
+ `[ReadModel]` attribute — a read model becomes injectable because a Chronicle
168
+ projection or reducer, an EF Core `DbSet<T>`, or a MongoDB collection backs it.
169
+ And there is **no slice or feature restriction**: registration is global by
170
+ type, so "the read model must be in the same slice" is a convention with nothing
171
+ enforcing it.
172
+
173
+ The resolution is **by the command's resolved event-source id**, not by the
174
+ read-model type and not by "the property that looks like its key". If the
175
+ instance you need is keyed by anything else — a referenced other entity, a
176
+ command with more than one candidate identity, or an id generated inside
177
+ `Handle()` — direct injection hands you the wrong instance or none. Read by
178
+ explicit key in `Provide()` instead:
179
+
180
+ ```csharp
181
+ var other = await readModels.GetInstanceById<Other>((EventSourceId)otherId);
182
+ ```
183
+
184
+ A missing instance resolves to `null`, never to a default object. A nullable
185
+ parameter receives that `null`; a non-nullable one is **rejected as a validation
186
+ failure (400)**, not crashed. `ARC0006` warns on a non-nullable command-scoped
187
+ read-model parameter for exactly this reason. `cratis-arc-command` carries the
188
+ full resolution and nullability reference, including the passive-projection case
189
+ where absence is *not* `null`.
190
+
191
+ ## Step 6 — Specify the rejection
192
+
193
+ Every rule gets a specification of its failure case, using
194
+ `CommandScenario<TCommand>`. Assert **both** that the command did not succeed
195
+ and that it failed *as a validation*:
196
+
197
+ ```csharp
198
+ [Fact] void should_not_succeed() => _result.ShouldNotBeSuccessful();
199
+ [Fact] void should_have_validation_errors() => _result.ShouldHaveValidationErrors();
200
+ ```
201
+
202
+ `ShouldNotBeSuccessful()` alone cannot tell a rejection from an unhandled
203
+ exception, which is precisely the mistake this skill exists to prevent. Never
204
+ assert on a message string — it is presentation text. See
205
+ `cratis-application-slice-specifications` for the scenario mechanics.
206
+
207
+ ## What breaks
208
+
209
+ - **The rejection returns HTTP 500.** Something threw instead of returning a
210
+ validation result. Check `Provide()` and `Handle()` for a `throw` on a
211
+ recoverable path.
212
+ - **The rule reads the wrong instance.** The injected read model is keyed by the
213
+ command's event-source id and the command carries a different identity. Read
214
+ by explicit key in `Provide()`.
215
+ - **The rule never fires.** The read-model type has no backing Chronicle
216
+ projection, reducer, `DbSet<T>`, or MongoDB collection, so nothing registered
217
+ it for command-side resolution.
218
+ - **Two requests both win.** The rule is in a validator or a `Provide()` read
219
+ rather than in `Handle()` with `Result<,>`, or it is a uniqueness rule that
220
+ belongs in a Chronicle constraint.
221
+ - **`ARC0005` on build.** `Provide()` returns a value no `Handle()` parameter
222
+ consumes.
223
+
224
+ ## How it is proven
225
+
226
+ `dotnet build` in Debug and in Release, zero warnings and zero errors — the
227
+ analyzers above are how `ARC0005`, `ARC0006` and `ARC0013` surface. Then
228
+ `dotnet test` with a specification per rule, each asserting both
229
+ `ShouldNotBeSuccessful()` and `ShouldHaveValidationErrors()`. A rule with no
230
+ red-first specification has not been shown to reject anything.
@@ -0,0 +1,2 @@
1
+ Copyright (c) Cratis. All rights reserved.
2
+ Licensed under the MIT license. See LICENSE file in the project root for full license information.