@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
|
+
# Tenancy
|
|
2
|
+
|
|
3
|
+
Verified against `Cratis.Arc.Core` `22.10.4`. Types are in `Cratis.Arc.Tenancy`.
|
|
4
|
+
|
|
5
|
+
Arc tenancy is **request-scoped tenant resolution**. It answers "which tenant is
|
|
6
|
+
this request for". It is not Chronicle's tenant namespace isolation, and it does
|
|
7
|
+
not by itself scope any storage.
|
|
8
|
+
|
|
9
|
+
## Constants
|
|
10
|
+
|
|
11
|
+
```csharp
|
|
12
|
+
public const string DefaultTenantIdHeader = "x-cratis-tenant-id";
|
|
13
|
+
public const string TenantIdItemKey = "TenantId";
|
|
14
|
+
public const string DefaultFixedTenantId = "development";
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Options and defaults
|
|
18
|
+
|
|
19
|
+
`ArcOptions.Tenancy` is a `TenancyOptions`:
|
|
20
|
+
|
|
21
|
+
| Property | Default |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| `ResolverType` | `TenantResolverType.Header` |
|
|
24
|
+
| `HttpHeader` | `x-cratis-tenant-id` |
|
|
25
|
+
| `BaseDomain` | empty |
|
|
26
|
+
| `QueryParameter` | `tenantId` |
|
|
27
|
+
| `ClaimType` | `tenant_id` |
|
|
28
|
+
| `FixedTenantId` | `development` |
|
|
29
|
+
| `DevelopmentTenantId` | an alias property over `FixedTenantId` |
|
|
30
|
+
|
|
31
|
+
`TenantResolverType` is `Header = 0`, `Query = 1`, `Claim = 2`,
|
|
32
|
+
`Development = 3`, `Subdomain = 4`, `Fixed = 5`. **Header is the default.**
|
|
33
|
+
|
|
34
|
+
Configure through the extension methods on `ArcOptions`: `UseHeaderTenancy`,
|
|
35
|
+
`UseQueryTenancy`, `UseClaimTenancy`, `UseDevelopmentTenancy`, `UseFixedTenancy`,
|
|
36
|
+
and `UseSubdomainTenancy(baseDomain, fallbackHeaderName?)`. Options are validated
|
|
37
|
+
by `TenancyOptionsValidator`.
|
|
38
|
+
|
|
39
|
+
## The resolvers
|
|
40
|
+
|
|
41
|
+
| Resolver | Reads |
|
|
42
|
+
| --- | --- |
|
|
43
|
+
| `HeaderTenantIdResolver` | `Tenancy.HttpHeader` |
|
|
44
|
+
| `QueryTenantIdResolver` | `Tenancy.QueryParameter` |
|
|
45
|
+
| `ClaimTenantIdResolver` | the first claim of `Tenancy.ClaimType` |
|
|
46
|
+
| `SubdomainTenantIdResolver` | the host label in front of `BaseDomain` |
|
|
47
|
+
| `FixedTenantIdResolver` | `Tenancy.FixedTenantId` |
|
|
48
|
+
| `DevelopmentTenantIdResolver` | deprecated alias over `FixedTenantIdResolver` |
|
|
49
|
+
|
|
50
|
+
⚠️ `SubdomainTenantIdResolver` **falls back to the configured HTTP header** when
|
|
51
|
+
the host does not match. A request that misses the subdomain can therefore still
|
|
52
|
+
name its own tenant through a header it controls. Clear `Tenancy.HttpHeader` if
|
|
53
|
+
that fallback is not wanted.
|
|
54
|
+
|
|
55
|
+
Its suffix is built once from the base domain at construction and never rebuilt.
|
|
56
|
+
Writing `BaseDomain` after construction does not take effect — and would silently
|
|
57
|
+
send every request down the header fallback. `BaseDomainIsNotADomainName` guards
|
|
58
|
+
the configured value.
|
|
59
|
+
|
|
60
|
+
## Reading the tenant
|
|
61
|
+
|
|
62
|
+
`ITenantIdAccessor.Current` gives the current `TenantId`, cached into an
|
|
63
|
+
`AsyncLocal`. An empty resolution is `TenantId.NotSet`, whose value is the
|
|
64
|
+
literal string `"[NotSet]"`. `TenantId.Default` is `"Default"` and `IsDefault`
|
|
65
|
+
tests for it. `CurrentTenantIdIsNotSet.ThrowIfNotSet(...)` is the guard for code
|
|
66
|
+
that requires a tenant.
|
|
67
|
+
|
|
68
|
+
⚠️ `NotSet` is a real string, not `null`. A check like
|
|
69
|
+
`string.IsNullOrEmpty(tenantId)` never fires for an unresolved tenant. Compare
|
|
70
|
+
against `TenantId.NotSet`.
|
|
71
|
+
|
|
72
|
+
## Development listing
|
|
73
|
+
|
|
74
|
+
`ICanProvideTenants` (`Task<IEnumerable<Tenant>> Provide()`) feeds
|
|
75
|
+
`GET /.cratis/tenants`, where `Tenant` is `record (TenantId Id, TenantName Name)`.
|
|
76
|
+
The endpoint is `AllowAnonymous` and mapped unconditionally, so an implementation
|
|
77
|
+
that returns real tenants publishes them. Return development fixtures only.
|
|
78
|
+
|
|
79
|
+
## Where the boundary is
|
|
80
|
+
|
|
81
|
+
Tenant resolution reads a value the **caller** supplied — a header, a query
|
|
82
|
+
parameter, a claim, or a host. Only the claim resolver reads something the
|
|
83
|
+
authentication step produced. The other four are caller-controlled input.
|
|
84
|
+
|
|
85
|
+
Anything that must be isolated per tenant has to enforce that isolation itself,
|
|
86
|
+
downstream of resolution. Never treat a resolved tenant id as an authorization
|
|
87
|
+
decision.
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cratis-arc-command
|
|
3
|
+
description: Define a Cratis Arc command — the [Command] record, its Handle() method and return shape, the optional Provide() step, and the generated TypeScript proxy. Use when adding a command, choosing what Handle() should return, deciding which values may reach the causation chain, or wiring a form or button to an Arc backend. Do not use for a validation-only change or merely to execute an existing command.
|
|
4
|
+
license: MIT
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Define an Arc command
|
|
8
|
+
|
|
9
|
+
A command is a record that carries the user's intent and owns its own handler.
|
|
10
|
+
Arc discovers it, runs authorization and validation, calls `Handle()`, and turns
|
|
11
|
+
whatever `Handle()` returns into appended events, a response, or both.
|
|
12
|
+
|
|
13
|
+
## Verified product sources
|
|
14
|
+
|
|
15
|
+
| Package | Version | Purpose |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| `Cratis.Arc.Core` | `22.10.4` | `Cratis.Arc.Commands.ModelBound.CommandAttribute`, `CommandResult`, `CommandValidator<T>` |
|
|
18
|
+
| `Cratis.Arc.Chronicle` | `22.10.4` | event append handling, `Cratis.Arc.Chronicle.Commands.NotAuditedAttribute` |
|
|
19
|
+
| `Cratis.Arc.ProxyGenerator.Build` | `22.10.4` | `CratisProxiesOutputPath` MSBuild integration |
|
|
20
|
+
| `Cratis.Fundamentals` | `7.18.2` | `Cratis.Monads.Result<TResult, TError>` |
|
|
21
|
+
| `Cratis.Chronicle` | `16.39.1` | `EventTypeAttribute`, `EventForEventSourceId`, `ICanProvideEventSourceId` |
|
|
22
|
+
| `@cratis/arc` | `22.10.4` | `ICommand`, `CommandResult`, `ValidationResult` |
|
|
23
|
+
|
|
24
|
+
Reverify before claiming support for another version. Arc without Chronicle is a
|
|
25
|
+
supported setup; everything on this page that appends events needs Chronicle.
|
|
26
|
+
|
|
27
|
+
## Declare the command
|
|
28
|
+
|
|
29
|
+
```csharp
|
|
30
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
31
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
32
|
+
|
|
33
|
+
using Cratis.Arc.Commands.ModelBound;
|
|
34
|
+
using Cratis.Chronicle.Events;
|
|
35
|
+
|
|
36
|
+
namespace <NamespaceRoot>.<Feature>;
|
|
37
|
+
|
|
38
|
+
[Command]
|
|
39
|
+
public record <ImperativeName>(<ConceptType> <Name>, <ConceptType> <Name>)
|
|
40
|
+
{
|
|
41
|
+
public <EventType> Handle() => new(<Name>);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
[EventType]
|
|
45
|
+
public record <EventType>(<ConceptType> <Name>);
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Rules the framework actually enforces:
|
|
49
|
+
|
|
50
|
+
- `[Command]` is `Cratis.Arc.Commands.ModelBound.CommandAttribute` and applies to
|
|
51
|
+
a class declaration only. A type counts as a command when it carries
|
|
52
|
+
`[Command]` **and** declares a `Handle` method; either one alone is not enough.
|
|
53
|
+
- `Handle` is found by name on the command type itself. `ARC0003` is an error
|
|
54
|
+
when another class defines `Handle` for a command, and `ARC0004` is an error
|
|
55
|
+
when a `[Command]` type has no public instance `Handle`.
|
|
56
|
+
- `ARC0002` warns when a type has `Handle` but no `[Command]`; `ARC0007` warns
|
|
57
|
+
when a command is a `class` rather than a `record`.
|
|
58
|
+
- Name the command as the action — `OpenDebitAccount`, not
|
|
59
|
+
`OpenDebitAccountCommand`.
|
|
60
|
+
- `Handle` parameters are resolved from DI, so ask for services there rather
|
|
61
|
+
than through a constructor.
|
|
62
|
+
- Never inject `IEventLog` into `Handle` to append the command's own events —
|
|
63
|
+
`ARCCHR0007` warns, and it bypasses Arc's append pipeline. Express the append
|
|
64
|
+
through the return type.
|
|
65
|
+
|
|
66
|
+
Use concept types rather than raw primitives for the command's values, and give
|
|
67
|
+
Chronicle-backed identities the `EventSourceId<T>` base — see the
|
|
68
|
+
`cratis-fundamentals-concept` skill for the exact shapes.
|
|
69
|
+
|
|
70
|
+
## Choose the return shape
|
|
71
|
+
|
|
72
|
+
`Handle()`'s return value is dispatched by its runtime type. The shape decides
|
|
73
|
+
what is appended and what the caller gets back.
|
|
74
|
+
|
|
75
|
+
| Return | What Arc does |
|
|
76
|
+
| --- | --- |
|
|
77
|
+
| A registered event record | Appends it to the command's event source id |
|
|
78
|
+
| `IEnumerable<object>` of registered events | Appends each to the command's event source id |
|
|
79
|
+
| `EventForEventSourceId(id, @event)` | Appends that one event to `id` |
|
|
80
|
+
| A collection mixing events and `EventForEventSourceId` | Appends each to its own target, wrappers to their id and plain events to the command's |
|
|
81
|
+
| A tuple | Each element is dispatched on its own; the one element nothing can handle becomes the response |
|
|
82
|
+
| Anything else | Becomes the response payload |
|
|
83
|
+
| `Result<TEvent, ValidationResult>` | Success appends the event; failure becomes a validation failure |
|
|
84
|
+
| `Task<T>` / `ValueTask<T>` of any of the above | Awaited first, then dispatched |
|
|
85
|
+
|
|
86
|
+
See [handler shapes](references/handler-shapes.md) for the dispatch order, the
|
|
87
|
+
tuple rule, and the exact failure modes.
|
|
88
|
+
|
|
89
|
+
Two consequences worth knowing before writing the first command:
|
|
90
|
+
|
|
91
|
+
- **The tuple is how a create command returns its new id.** An
|
|
92
|
+
`EventSourceId`-derived value in the tuple is not appendable, so it becomes the
|
|
93
|
+
response — and the response, when it is an event-source id value, is also what
|
|
94
|
+
the events in that same tuple are appended to. Returning more than one
|
|
95
|
+
unhandleable element throws `MultipleUnhandledTupleValues`.
|
|
96
|
+
- **Events never carry their own event source.** Cross-stream writes use
|
|
97
|
+
`EventForEventSourceId`; they are not expressed by a property on the event.
|
|
98
|
+
|
|
99
|
+
```csharp
|
|
100
|
+
[Command]
|
|
101
|
+
public record Register<Thing>(<ConceptType> <Name>)
|
|
102
|
+
{
|
|
103
|
+
public (<ThingId>, <ThingRegistered>) Handle()
|
|
104
|
+
{
|
|
105
|
+
var id = <ThingId>.New();
|
|
106
|
+
return (id, new(<Name>));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
```csharp
|
|
112
|
+
[Command]
|
|
113
|
+
public record Transfer(<AccountId> From, <AccountId> To, <Money> Amount)
|
|
114
|
+
{
|
|
115
|
+
public IEnumerable<object> Handle() =>
|
|
116
|
+
[
|
|
117
|
+
new EventForEventSourceId(From, new <Withdrawn>(Amount)),
|
|
118
|
+
new EventForEventSourceId(To, new <Deposited>(Amount)),
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Where the event source id comes from
|
|
124
|
+
|
|
125
|
+
Chronicle resolves the command's event source id in this order:
|
|
126
|
+
|
|
127
|
+
1. `commandContext.Response`, when the response is an event-source-id value —
|
|
128
|
+
the tuple case above;
|
|
129
|
+
2. `ICanProvideEventSourceId.GetEventSourceId()` on the command;
|
|
130
|
+
3. a single `EventSourceId`/`EventSourceId<T>`-derived or `[Key]`-marked
|
|
131
|
+
property on the command;
|
|
132
|
+
4. otherwise a freshly generated id.
|
|
133
|
+
|
|
134
|
+
A command with more than one candidate property is ambiguous and `ARCCHR0002`
|
|
135
|
+
warns; implement `ICanProvideEventSourceId` to say which value wins. Do not rely
|
|
136
|
+
on property order.
|
|
137
|
+
|
|
138
|
+
Two attributes are spelled `[Key]`. Chronicle reads `Cratis.Chronicle.Keys.KeyAttribute`.
|
|
139
|
+
Marking `System.ComponentModel.DataAnnotations.KeyAttribute` in an application
|
|
140
|
+
that uses Chronicle compiles, resolves nothing, and silently invents a new event
|
|
141
|
+
source id for every command — `ARCCHR0008` reports it.
|
|
142
|
+
|
|
143
|
+
## Fetch what the handler needs with `Provide()`
|
|
144
|
+
|
|
145
|
+
`Provide` is an optional instance method that runs before `Handle`. Its purpose
|
|
146
|
+
is to keep IO out of `Handle`: it fetches or computes values, and Arc binds them
|
|
147
|
+
to `Handle`'s parameters by type.
|
|
148
|
+
|
|
149
|
+
```csharp
|
|
150
|
+
[Command]
|
|
151
|
+
public record Open<Thing>(<ThingId> Id, <OwnerId> OwnerId)
|
|
152
|
+
{
|
|
153
|
+
public async Task<Result<<Owner>, ValidationResult>> Provide(IReadModels readModels)
|
|
154
|
+
{
|
|
155
|
+
var owner = await readModels.GetInstanceById<<Owner>>((EventSourceId)OwnerId);
|
|
156
|
+
return owner is null
|
|
157
|
+
? ValidationResult.Error("Owner must exist.")
|
|
158
|
+
: owner;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
public <ThingOpened> Handle(<Owner> owner) => new(owner.Id);
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
- Short-circuit with `ValidationResult.Error(...)`; do not throw for a rejection
|
|
166
|
+
the user can act on. A thrown exception surfaces as `HasExceptions` and HTTP
|
|
167
|
+
500, not as a validation failure.
|
|
168
|
+
- Every value `Provide` returns must be consumed by a `Handle` parameter.
|
|
169
|
+
`ARC0005` warns otherwise. `ValidationResult`, `AuthorizationResult` and
|
|
170
|
+
`CommandResult` are exempt because they short-circuit rather than feed `Handle`.
|
|
171
|
+
- `Provide` runs after authorization and validation.
|
|
172
|
+
|
|
173
|
+
An Arc read model can also be injected directly into `Provide`, `Handle`, or a
|
|
174
|
+
`CommandValidator<T>` — but only by **the command's own resolved key**. Reading a
|
|
175
|
+
read model keyed by anything else needs an explicit by-id read, and an absent
|
|
176
|
+
instance does not always arrive as `null`. Read
|
|
177
|
+
[read-model injection](references/read-model-injection.md) before relying on a
|
|
178
|
+
directly injected read model.
|
|
179
|
+
|
|
180
|
+
## Decide what the causation chain may record
|
|
181
|
+
|
|
182
|
+
Every property value of the command is written to the causation of every event
|
|
183
|
+
the command appends, and the event log is immutable. Decide this when the
|
|
184
|
+
property is added.
|
|
185
|
+
|
|
186
|
+
| Marking | Use for | Effect |
|
|
187
|
+
| --- | --- | --- |
|
|
188
|
+
| `[PII]` (Chronicle) | personal data | encrypted in the event and enrolled in erasure; already withheld from causation |
|
|
189
|
+
| `[NotAudited]` (Arc Chronicle) | a secret that is not personal data — password, token, API key | withheld from causation, nothing else |
|
|
190
|
+
|
|
191
|
+
```csharp
|
|
192
|
+
[Command]
|
|
193
|
+
public record Change<Secret>(
|
|
194
|
+
<UserId> User,
|
|
195
|
+
[property: NotAudited] string Old<Secret>,
|
|
196
|
+
[property: NotAudited] string New<Secret>)
|
|
197
|
+
{
|
|
198
|
+
public <SecretChanged> Handle(I<Hasher> hasher) => new(hasher.Hash(New<Secret>));
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`[NotAudited]` applies to a class, struct, property, or parameter. On the type it
|
|
203
|
+
withholds every property at once. Marking the concept type instead makes it
|
|
204
|
+
travel to every command that takes that value. The command is still named on the
|
|
205
|
+
chain either way; only the values are withheld.
|
|
206
|
+
|
|
207
|
+
`ARCCHR0009` warns when a property's *name* reads like a secret and is unmarked.
|
|
208
|
+
It cannot see a secret whose name does not say so, so a clean build means
|
|
209
|
+
"nothing obvious was missed", not "no secrets are recorded". When the name only
|
|
210
|
+
reads like a secret and the value is safe to record, the framework's own guidance
|
|
211
|
+
is to mark it `[NotAudited]` anyway or rename the property — the value is written
|
|
212
|
+
either way, so the reading is all a reviewer has to go on.
|
|
213
|
+
|
|
214
|
+
## Generate the TypeScript proxy
|
|
215
|
+
|
|
216
|
+
```xml
|
|
217
|
+
<PackageReference Include="Cratis.Arc.ProxyGenerator.Build" Version="22.10.4" />
|
|
218
|
+
|
|
219
|
+
<PropertyGroup>
|
|
220
|
+
<CratisProxiesOutputPath>$(MSBuildThisFileDirectory)../<Web>/src/api</CratisProxiesOutputPath>
|
|
221
|
+
</PropertyGroup>
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`dotnet build` runs the generator after the build, and only when
|
|
225
|
+
`CratisProxiesOutputPath` is set. Output folders mirror the C# namespace, not the
|
|
226
|
+
file path. See [proxy generation](references/proxy-generation.md) for the full
|
|
227
|
+
set of MSBuild knobs and the common failures.
|
|
228
|
+
|
|
229
|
+
## The generated client contract
|
|
230
|
+
|
|
231
|
+
The generated proxy is a `Command` from `@cratis/arc`. Its members are the same
|
|
232
|
+
whatever renders it:
|
|
233
|
+
|
|
234
|
+
| Member | What it does |
|
|
235
|
+
| --- | --- |
|
|
236
|
+
| `route` | The generated route, including the configured API prefix |
|
|
237
|
+
| `roles` | The roles the command declares; empty when it declares none |
|
|
238
|
+
| `<property>` | Get or set one value; setting raises `propertyChanged` |
|
|
239
|
+
| `hasChanges` | True when any value differs from the initial values |
|
|
240
|
+
| `execute(allowedSeverity?, ignoreWarnings?)` | Sends the command; resolves to `CommandResult` |
|
|
241
|
+
| `validate()` | Runs authorization and validation on the server without the handler |
|
|
242
|
+
| `validateClientSide()` | Runs only the extracted rules locally; never touches the network |
|
|
243
|
+
| `setInitialValues(values)` | Sets the change-tracking baseline |
|
|
244
|
+
| `setInitialValuesFromCurrentValues()` | Rebaselines onto the current values |
|
|
245
|
+
| `revertChanges()` / `clear()` | Restore the baseline / reset everything |
|
|
246
|
+
|
|
247
|
+
Branch on the specific flag, not only on `isSuccess`. The exact `CommandResult`
|
|
248
|
+
and `ValidationResult` shapes are in
|
|
249
|
+
[command result](references/command-result.md) — in particular, a validation
|
|
250
|
+
failure carries `members: string[]` (camelCased) and a numeric `severity`, not a
|
|
251
|
+
`propertyName` string.
|
|
252
|
+
|
|
253
|
+
`validateClientSide()` runs only the rules the generator could extract, so it can
|
|
254
|
+
pass where `execute()` still fails validation — see
|
|
255
|
+
[proxy generation](references/proxy-generation.md) for the exact extractable set.
|
|
256
|
+
|
|
257
|
+
Binding this proxy into a React component — the generated `use()` hook and the
|
|
258
|
+
Cratis Components command dialog and form fields — belongs to the Arc React and
|
|
259
|
+
Components guidance, not to this skill.
|
|
260
|
+
|
|
261
|
+
## Route near misses
|
|
262
|
+
|
|
263
|
+
- Adding or changing a rule on an existing command: the Arc command validation
|
|
264
|
+
guidance.
|
|
265
|
+
- Executing an existing command from backend code: `cratis-arc-command-execution`.
|
|
266
|
+
- Append-time uniqueness or concurrency constraints: the Chronicle event
|
|
267
|
+
constraints guidance.
|
|
268
|
+
- Choosing the concept or identity type for a value: `cratis-fundamentals-concept`.
|
|
269
|
+
|
|
270
|
+
## Verify
|
|
271
|
+
|
|
272
|
+
- The command is a `record`, carries `[Command]`, and declares a public instance
|
|
273
|
+
`Handle`.
|
|
274
|
+
- The return shape matches what the command is supposed to do, and any
|
|
275
|
+
cross-stream event is wrapped in `EventForEventSourceId`.
|
|
276
|
+
- At most one tuple element is unhandleable.
|
|
277
|
+
- The event source id resolves from exactly one place; `ARCCHR0002` is silent.
|
|
278
|
+
- `[Key]`, where used, is `Cratis.Chronicle.Keys.KeyAttribute`.
|
|
279
|
+
- No `IEventLog` is injected into `Handle`.
|
|
280
|
+
- Every value `Provide` returns is consumed by `Handle`.
|
|
281
|
+
- Rejections are validation results, never thrown exceptions.
|
|
282
|
+
- Every secret or personal value is marked before it can reach the event log.
|
|
283
|
+
- `dotnet build` is clean in Debug and Release, with no suppressed `ARC*` or
|
|
284
|
+
`ARCCHR*` diagnostic left unjustified.
|
|
285
|
+
- The generated proxy exists at the configured output path and the frontend
|
|
286
|
+
compiles against it.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# CommandResult and ValidationResult
|
|
2
|
+
|
|
3
|
+
Verified against `Cratis.Arc.Core` `22.10.4` and `@cratis/arc` `22.10.4`.
|
|
4
|
+
|
|
5
|
+
## Server side
|
|
6
|
+
|
|
7
|
+
`Cratis.Arc.Commands.CommandResult`:
|
|
8
|
+
|
|
9
|
+
| Member | Type | Notes |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| `CorrelationId` | `CorrelationId` | Defaults to `CorrelationId.NotSet` |
|
|
12
|
+
| `IsSuccess` | `bool` | Computed: `IsAuthorized && IsValid && !HasExceptions` |
|
|
13
|
+
| `IsAuthorized` | `bool` | Settable; defaults to `true` |
|
|
14
|
+
| `IsValid` | `bool` | Computed: `!ValidationResults.Any()` |
|
|
15
|
+
| `HasExceptions` | `bool` | Computed: `ExceptionMessages.Any()` |
|
|
16
|
+
| `ValidationResults` | `IEnumerable<ValidationResult>` | |
|
|
17
|
+
| `ExceptionMessages` | `IEnumerable<string>` | |
|
|
18
|
+
| `ExceptionStackTrace` | `string` | |
|
|
19
|
+
| `AuthorizationFailureReason` | `string` | |
|
|
20
|
+
|
|
21
|
+
`CommandResult<TResult>` adds a `Response` of `TResult`.
|
|
22
|
+
|
|
23
|
+
`IsValid` and `HasExceptions` are **derived**, not stored. There is no way to
|
|
24
|
+
set them directly — a result is invalid because it carries validation results.
|
|
25
|
+
|
|
26
|
+
Factories: `Success`, `Unauthorized`, `MissingHandler`, `Error`, `InvalidBody`,
|
|
27
|
+
`FromException`.
|
|
28
|
+
|
|
29
|
+
⚠️ **A failed result carries no response.** The pipeline clears the response once
|
|
30
|
+
every execution scope has completed and the result is not successful, so a
|
|
31
|
+
transaction that rolls back after `Handle` returned a value does not leak that
|
|
32
|
+
value into the error body.
|
|
33
|
+
|
|
34
|
+
## Severity filtering
|
|
35
|
+
|
|
36
|
+
`ValidationResultSeverity`: `Unknown = 0`, `Information = 1`, `Warning = 2`,
|
|
37
|
+
`Error = 3`.
|
|
38
|
+
|
|
39
|
+
Only *blocking* results survive onto the result. With no `allowedSeverity`, only
|
|
40
|
+
`Error` blocks — information and warnings are filtered out entirely. With an
|
|
41
|
+
`allowedSeverity`, anything strictly greater than it blocks. So passing
|
|
42
|
+
`Warning` keeps errors blocking and lets warnings through.
|
|
43
|
+
|
|
44
|
+
## Client side
|
|
45
|
+
|
|
46
|
+
`@cratis/arc` `ICommandResult<TResponse = object>`:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
readonly correlationId: Guid;
|
|
50
|
+
readonly isSuccess: boolean;
|
|
51
|
+
readonly isAuthorized: boolean;
|
|
52
|
+
readonly isValid: boolean;
|
|
53
|
+
readonly hasExceptions: boolean;
|
|
54
|
+
readonly validationResults: ValidationResult[];
|
|
55
|
+
readonly exceptionMessages: string[];
|
|
56
|
+
readonly authorizationFailureReason: string;
|
|
57
|
+
readonly exceptionStackTrace: string;
|
|
58
|
+
readonly response?: TResponse;
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`ValidationResult` is a class with these readonly members:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
severity: ValidationResultSeverity // numeric enum, 0..3
|
|
65
|
+
message: string
|
|
66
|
+
members: string[]
|
|
67
|
+
state: any
|
|
68
|
+
reason: ValidationResultReason // defaults to Rule
|
|
69
|
+
reasonDetail?: string
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
⚠️ **There is no `propertyName` and no string severity.** A rule attributes its
|
|
73
|
+
failure to one or more `members`, and `severity` is the numeric
|
|
74
|
+
`ValidationResultSeverity` enum. Code that reads `v.propertyName` or compares
|
|
75
|
+
`v.severity === 'Error'` is reading fields that do not exist.
|
|
76
|
+
|
|
77
|
+
Match a failure to a field through `members`, and branch on `reasonDetail`
|
|
78
|
+
rather than parsing `message` — the message is prose written for a human and is
|
|
79
|
+
free to change, while `reasonDetail` is the identity of the specific thing that
|
|
80
|
+
rejected the command (a constraint name, for instance).
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
const result = await command.execute();
|
|
84
|
+
|
|
85
|
+
if (!result.isAuthorized) { /* the caller is not allowed to do this */ }
|
|
86
|
+
else if (!result.isValid) {
|
|
87
|
+
const errorFor = (property: string) =>
|
|
88
|
+
result.validationResults.find(v => v.members.includes(property))?.message;
|
|
89
|
+
}
|
|
90
|
+
else if (result.hasExceptions) { /* server fault */ }
|
|
91
|
+
else { /* result.response */ }
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`members` is **camelCased on purpose**, so a form can match a server failure to
|
|
95
|
+
the field that caused it without transforming anything. A nested member is a
|
|
96
|
+
dotted path from the graph root — `address.postalCode`. A failure raised by a
|
|
97
|
+
`ConceptValidator<T>` is attributed to the field holding the concept, not to the
|
|
98
|
+
concept's inner `Value`, because TypeScript erases the concept to its primitive
|
|
99
|
+
and names the field the same way.
|
|
100
|
+
|
|
101
|
+
`reason` also distinguishes a genuine rejection from a validator that threw
|
|
102
|
+
while validating hostile or partial input: the latter arrives as a single result
|
|
103
|
+
with a `ValidatorFailed` reason and a generic message, replacing every message
|
|
104
|
+
the validator's author wrote. Do not read the message to tell the two apart.
|
|
105
|
+
|
|
106
|
+
## Executing with a severity
|
|
107
|
+
|
|
108
|
+
`command.execute(allowedSeverity?, ignoreWarnings?)` mirrors the server-side
|
|
109
|
+
filtering. `command.validate()` runs authorization and validation on the server
|
|
110
|
+
without the handler; `command.validateClientSide()` runs only the extracted
|
|
111
|
+
rules locally and never touches the network.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Handler shapes and how Arc dispatches them
|
|
2
|
+
|
|
3
|
+
Verified against `Cratis.Arc.Core` and `Cratis.Arc.Chronicle` `22.10.4` with
|
|
4
|
+
`Cratis.Chronicle` `16.39.1`.
|
|
5
|
+
|
|
6
|
+
## The pipeline around `Handle`
|
|
7
|
+
|
|
8
|
+
One command execution runs in this order:
|
|
9
|
+
|
|
10
|
+
1. resolve the handler — no handler produces a `MissingHandler` result;
|
|
11
|
+
2. build the command context, including the resolved event source id;
|
|
12
|
+
3. begin every `ICommandExecutionScope`;
|
|
13
|
+
4. run the command filters — **authorization filters first**, then the rest,
|
|
14
|
+
stopping at the first blocking verdict;
|
|
15
|
+
5. resolve `Handle`'s arguments, which is where `Provide()` runs;
|
|
16
|
+
6. invoke `Handle`;
|
|
17
|
+
7. dispatch the returned value;
|
|
18
|
+
8. complete the execution scopes in reverse order.
|
|
19
|
+
|
|
20
|
+
If the final result is not successful, the response is **cleared** before it is
|
|
21
|
+
serialized. A caller never receives both a failure and a payload.
|
|
22
|
+
|
|
23
|
+
Authorization runs before validation deliberately: sorting authorization filters
|
|
24
|
+
first is what stops a validation failure from short-circuiting the chain and
|
|
25
|
+
leaving the authorization verdict at its permissive default.
|
|
26
|
+
|
|
27
|
+
## Dispatch
|
|
28
|
+
|
|
29
|
+
The returned value is dispatched on its **runtime** type, not on the method's
|
|
30
|
+
declared return type.
|
|
31
|
+
|
|
32
|
+
- `Task<T>` and `ValueTask<T>` are awaited first, then the inner value is
|
|
33
|
+
dispatched. (`ARC0010` warns when a synchronous result is wrapped in a `Task`
|
|
34
|
+
for no reason.)
|
|
35
|
+
- A `Result<TValue, TError>` — `Cratis.Monads.Result<,>` from
|
|
36
|
+
`Cratis.Fundamentals` — is a `OneOf`, so its inner value is unwrapped and
|
|
37
|
+
dispatched. `Result<TEvent, ValidationResult>` therefore appends on success and
|
|
38
|
+
becomes a validation failure on error. Both directions have implicit
|
|
39
|
+
conversions, so `return new SomethingHappened(...)` and
|
|
40
|
+
`return ValidationResult.Error("...")` both compile in the same method.
|
|
41
|
+
- A tuple has every non-null element dispatched independently.
|
|
42
|
+
- Anything no handler claims becomes the response payload.
|
|
43
|
+
|
|
44
|
+
## What claims an event
|
|
45
|
+
|
|
46
|
+
The Chronicle response-value handlers claim a value only when the event type is
|
|
47
|
+
**registered** — that is, the record carries `[EventType]` and Chronicle's event
|
|
48
|
+
type registry knows it.
|
|
49
|
+
|
|
50
|
+
| Runtime value | Claimed by | Appended to |
|
|
51
|
+
| --- | --- | --- |
|
|
52
|
+
| A registered event record | single-event handler | the command's event source id |
|
|
53
|
+
| `IEnumerable<object>` where every element is a registered event | events handler | the command's event source id |
|
|
54
|
+
| `EventForEventSourceId` | single wrapper handler | the wrapper's own id |
|
|
55
|
+
| A collection containing at least one `EventForEventSourceId`, every other element being a registered event | wrapper-collection handler | each wrapper to its own id, each plain event to the command's |
|
|
56
|
+
|
|
57
|
+
⚠️ **An unregistered event type is not an error.** No handler claims it, so it
|
|
58
|
+
silently becomes the HTTP response body instead of being appended. A command
|
|
59
|
+
that "runs fine" but appends nothing is almost always a missing `[EventType]`.
|
|
60
|
+
|
|
61
|
+
An empty collection statically typed as `IEnumerable<EventForEventSourceId>` is
|
|
62
|
+
still recognised and appends nothing, rather than being serialized as the
|
|
63
|
+
response.
|
|
64
|
+
|
|
65
|
+
`[EventType]` takes an optional id and generation. Pass neither for a new event:
|
|
66
|
+
the identifier defaults to the type name, and `ARCCHR0004` reports an id that
|
|
67
|
+
merely repeats the type name. An id that *differs* from the type name is the
|
|
68
|
+
supported way to rename the record while stored events keep resolving, and is
|
|
69
|
+
left alone.
|
|
70
|
+
|
|
71
|
+
## The tuple rule
|
|
72
|
+
|
|
73
|
+
Within a tuple:
|
|
74
|
+
|
|
75
|
+
1. every element is offered to the handlers;
|
|
76
|
+
2. the elements nothing can handle are candidates for the response;
|
|
77
|
+
3. **more than one unhandleable element throws `MultipleUnhandledTupleValues`** —
|
|
78
|
+
Arc cannot decide which one is the response;
|
|
79
|
+
4. the single unhandleable element is set as the response *before* the
|
|
80
|
+
handleable elements are handled, which is what lets the events see it.
|
|
81
|
+
|
|
82
|
+
That last point is the mechanism behind the create-command idiom: an
|
|
83
|
+
`EventSourceId`/`EventSourceId<T>`-derived value is not appendable, so it becomes
|
|
84
|
+
the response, and `commandContext.GetEventSourceId()` prefers a response that is
|
|
85
|
+
an event-source-id value over the id resolved from the command's properties. The
|
|
86
|
+
events in the same tuple are therefore appended to the id the handler just
|
|
87
|
+
generated, and the client receives it as the response.
|
|
88
|
+
|
|
89
|
+
```csharp
|
|
90
|
+
public (<ThingId>, <ThingRegistered>) Handle()
|
|
91
|
+
{
|
|
92
|
+
var id = <ThingId>.New();
|
|
93
|
+
return (id, new(<Name>));
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Returning two ids, or an id and another plain value, is the failure case.
|
|
98
|
+
|
|
99
|
+
## Cross-stream appends
|
|
100
|
+
|
|
101
|
+
An event never carries its own event source. To write to another stream, wrap:
|
|
102
|
+
|
|
103
|
+
```csharp
|
|
104
|
+
public IEnumerable<object> Handle() =>
|
|
105
|
+
[
|
|
106
|
+
new EventForEventSourceId(From, new <Withdrawn>(Amount)),
|
|
107
|
+
new EventForEventSourceId(To, new <Deposited>(Amount)),
|
|
108
|
+
];
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`EventForEventSourceId` also carries optional `Subject`, `EventStreamType` and
|
|
112
|
+
`EventStreamId` init members when the target stream is not the default.
|
|
113
|
+
|
|
114
|
+
Concurrency scope is resolved **per target stream** — one expected tail per
|
|
115
|
+
event source id, not one shared across the streams a cross-stream command writes
|
|
116
|
+
to.
|
|
117
|
+
|
|
118
|
+
Do not reach for `IEventLog` to do this instead. `ARCCHR0007` warns when a
|
|
119
|
+
command's `Handle` injects `IEventLog`, because that bypasses Arc's append
|
|
120
|
+
pipeline along with its correlation and ordering guarantees.
|
|
121
|
+
|
|
122
|
+
## Failure shapes
|
|
123
|
+
|
|
124
|
+
| Situation | What the caller sees |
|
|
125
|
+
| --- | --- |
|
|
126
|
+
| No handler for the command type | `ExceptionMessages` names the command type |
|
|
127
|
+
| Authorization filter denies | `IsAuthorized` false, `AuthorizationFailureReason` set |
|
|
128
|
+
| Validator or `Provide()` returns `ValidationResult.Error` | `IsValid` false, `ValidationResults` populated |
|
|
129
|
+
| Append rejected by a Chronicle constraint | a validation failure whose `Reason` says a constraint rejected it and whose `ReasonDetail` names it |
|
|
130
|
+
| `Provide()`/`Handle()` throws | `HasExceptions` true — HTTP 500, not a validation failure |
|
|
131
|
+
| Two unhandleable tuple elements | `MultipleUnhandledTupleValues` becomes an exception outcome |
|
|
132
|
+
|
|
133
|
+
Branch on `ValidationResult.ReasonDetail` rather than on `Message`. The message
|
|
134
|
+
is prose written for a human and is free to change; the detail is the identity of
|
|
135
|
+
the specific thing that rejected the command.
|