@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.
- package/README.md +43 -0
- package/package.json +24 -0
- package/skills/cratis-application-react-specifications/LICENSE +2 -0
- package/skills/cratis-application-react-specifications/SKILL.md +192 -0
- package/skills/cratis-application-slice-specifications/LICENSE +2 -0
- package/skills/cratis-application-slice-specifications/SKILL.md +222 -0
- package/skills/cratis-arc-authentication-authorization-and-identity/LICENSE +2 -0
- package/skills/cratis-arc-authentication-authorization-and-identity/SKILL.md +233 -0
- package/skills/cratis-arc-authentication-authorization-and-identity/references/authentication.md +140 -0
- package/skills/cratis-arc-authentication-authorization-and-identity/references/authorization.md +112 -0
- package/skills/cratis-arc-authentication-authorization-and-identity/references/frontend.md +102 -0
- package/skills/cratis-arc-authentication-authorization-and-identity/references/local-development.md +85 -0
- package/skills/cratis-arc-authentication-authorization-and-identity/references/tenancy.md +87 -0
- package/skills/cratis-arc-command/LICENSE +2 -0
- package/skills/cratis-arc-command/SKILL.md +286 -0
- package/skills/cratis-arc-command/references/command-result.md +111 -0
- package/skills/cratis-arc-command/references/handler-shapes.md +135 -0
- package/skills/cratis-arc-command/references/proxy-generation.md +87 -0
- package/skills/cratis-arc-command/references/read-model-injection.md +97 -0
- package/skills/cratis-arc-command-execution/LICENSE +2 -0
- package/skills/cratis-arc-command-execution/SKILL.md +148 -0
- package/skills/cratis-arc-command-validation/LICENSE +2 -0
- package/skills/cratis-arc-command-validation/SKILL.md +230 -0
- package/skills/cratis-arc-ef-core-migration/LICENSE +2 -0
- package/skills/cratis-arc-ef-core-migration/SKILL.md +231 -0
- package/skills/cratis-arc-observable-query-http/LICENSE +2 -0
- package/skills/cratis-arc-observable-query-http/SKILL.md +157 -0
- package/skills/cratis-arc-query-paging/LICENSE +2 -0
- package/skills/cratis-arc-query-paging/SKILL.md +173 -0
- package/skills/cratis-arc-react-page/LICENSE +2 -0
- package/skills/cratis-arc-react-page/SKILL.md +291 -0
- package/skills/cratis-arc-react-page/references/data-page.md +125 -0
- package/skills/cratis-arc-react-page/references/data-tables.md +136 -0
- package/skills/cratis-arc-react-page/references/dialogs.md +238 -0
- package/skills/cratis-arc-react-page/references/mvvm.md +150 -0
- package/skills/cratis-arc-react-page/references/queries-and-commands.md +200 -0
- package/skills/cratis-chronicle-cli-operations/LICENSE +2 -0
- package/skills/cratis-chronicle-cli-operations/SKILL.md +199 -0
- package/skills/cratis-chronicle-client-dotnet/LICENSE +2 -0
- package/skills/cratis-chronicle-client-dotnet/SKILL.md +433 -0
- package/skills/cratis-chronicle-client-elixir/LICENSE +2 -0
- package/skills/cratis-chronicle-client-elixir/SKILL.md +429 -0
- package/skills/cratis-chronicle-client-kotlin/LICENSE +2 -0
- package/skills/cratis-chronicle-client-kotlin/SKILL.md +498 -0
- package/skills/cratis-chronicle-client-typescript/LICENSE +2 -0
- package/skills/cratis-chronicle-client-typescript/SKILL.md +396 -0
- package/skills/cratis-chronicle-compliance/LICENSE +2 -0
- package/skills/cratis-chronicle-compliance/SKILL.md +246 -0
- package/skills/cratis-chronicle-event-constraints/LICENSE +2 -0
- package/skills/cratis-chronicle-event-constraints/SKILL.md +227 -0
- package/skills/cratis-chronicle-event-modeling/LICENSE +2 -0
- package/skills/cratis-chronicle-event-modeling/SKILL.md +157 -0
- package/skills/cratis-chronicle-event-specifications/LICENSE +2 -0
- package/skills/cratis-chronicle-event-specifications/SKILL.md +200 -0
- package/skills/cratis-chronicle-event-type-migration/LICENSE +2 -0
- package/skills/cratis-chronicle-event-type-migration/SKILL.md +235 -0
- package/skills/cratis-chronicle-mcp-inspection/LICENSE +2 -0
- package/skills/cratis-chronicle-mcp-inspection/SKILL.md +79 -0
- package/skills/cratis-chronicle-mcp-inspection/references/blocked-tools.md +12 -0
- package/skills/cratis-chronicle-mcp-inspection/references/observational-tools.md +8 -0
- package/skills/cratis-chronicle-multi-tenancy/LICENSE +2 -0
- package/skills/cratis-chronicle-multi-tenancy/SKILL.md +198 -0
- package/skills/cratis-chronicle-projection/LICENSE +2 -0
- package/skills/cratis-chronicle-projection/SKILL.md +162 -0
- package/skills/cratis-chronicle-projection/references/fluent-builder.md +126 -0
- package/skills/cratis-chronicle-projection/references/model-bound-attributes.md +118 -0
- package/skills/cratis-chronicle-reactor/LICENSE +2 -0
- package/skills/cratis-chronicle-reactor/SKILL.md +344 -0
- package/skills/cratis-chronicle-read-model/LICENSE +2 -0
- package/skills/cratis-chronicle-read-model/SKILL.md +193 -0
- package/skills/cratis-chronicle-read-model/references/queries.md +142 -0
- package/skills/cratis-chronicle-read-model-specifications/LICENSE +2 -0
- package/skills/cratis-chronicle-read-model-specifications/SKILL.md +176 -0
- package/skills/cratis-chronicle-reducer/LICENSE +2 -0
- package/skills/cratis-chronicle-reducer/SKILL.md +173 -0
- package/skills/cratis-chronicle-web-workbench/LICENSE +2 -0
- package/skills/cratis-chronicle-web-workbench/SKILL.md +177 -0
- package/skills/cratis-cli-terminal-workbench/LICENSE +2 -0
- package/skills/cratis-cli-terminal-workbench/SKILL.md +156 -0
- package/skills/cratis-code-review/LICENSE +2 -0
- package/skills/cratis-code-review/SKILL.md +177 -0
- package/skills/cratis-components-accessibility/LICENSE +2 -0
- package/skills/cratis-components-accessibility/SKILL.md +176 -0
- package/skills/cratis-components-schema-editor/LICENSE +2 -0
- package/skills/cratis-components-schema-editor/SKILL.md +238 -0
- package/skills/cratis-components-stepper-command-dialog/LICENSE +2 -0
- package/skills/cratis-components-stepper-command-dialog/SKILL.md +209 -0
- package/skills/cratis-components-styling/LICENSE +2 -0
- package/skills/cratis-components-styling/SKILL.md +221 -0
- package/skills/cratis-components-toolbar/LICENSE +2 -0
- package/skills/cratis-components-toolbar/SKILL.md +259 -0
- package/skills/cratis-documentation-writing/LICENSE +2 -0
- package/skills/cratis-documentation-writing/SKILL.md +121 -0
- package/skills/cratis-event-model-diagram/LICENSE +2 -0
- package/skills/cratis-event-model-diagram/SKILL.md +142 -0
- package/skills/cratis-fundamentals-concept/LICENSE +2 -0
- package/skills/cratis-fundamentals-concept/SKILL.md +198 -0
- package/skills/cratis-fundamentals-type-discovery/LICENSE +2 -0
- package/skills/cratis-fundamentals-type-discovery/SKILL.md +152 -0
- package/skills/cratis-governed-release-methodology/LICENSE +2 -0
- package/skills/cratis-governed-release-methodology/SKILL.md +297 -0
- package/skills/cratis-lens-browser-extension/LICENSE +2 -0
- package/skills/cratis-lens-browser-extension/SKILL.md +239 -0
- package/skills/cratis-performance-review/LICENSE +2 -0
- package/skills/cratis-performance-review/SKILL.md +139 -0
- package/skills/cratis-screenplay-model-authoring/LICENSE +2 -0
- package/skills/cratis-screenplay-model-authoring/SKILL.md +231 -0
- package/skills/cratis-security-review/LICENSE +2 -0
- package/skills/cratis-security-review/SKILL.md +141 -0
- package/skills/cratis-specification-by-example/LICENSE +2 -0
- package/skills/cratis-specification-by-example/SKILL.md +188 -0
- package/skills/cratis-specifications-csharp/LICENSE +2 -0
- package/skills/cratis-specifications-csharp/SKILL.md +230 -0
- package/skills/cratis-specifications-csharp/references/application-scenarios.md +191 -0
- package/skills/cratis-specifications-csharp/references/csharp-patterns.md +142 -0
- package/skills/cratis-specifications-csharp/references/integration-specs.md +126 -0
- package/skills/cratis-specifications-typescript/LICENSE +2 -0
- package/skills/cratis-specifications-typescript/SKILL.md +202 -0
- package/skills/cratis-specifications-typescript/references/typescript-patterns.md +168 -0
- package/skills/cratis-stage-rendering-and-sandbox/LICENSE +2 -0
- package/skills/cratis-stage-rendering-and-sandbox/SKILL.md +230 -0
- package/skills/cratis-studio-mcp-safety-guidance/LICENSE +2 -0
- package/skills/cratis-studio-mcp-safety-guidance/SKILL.md +69 -0
- package/skills/cratis-studio-mcp-safety-guidance/references/blocked-tools.md +12 -0
- 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,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,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.
|