@drunkcoding/dknet-implementation-skills 14.1.0 → 14.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +37 -26
- package/package.json +2 -3
- package/{.claude-plugin → plugin/.claude-plugin}/plugin.json +1 -1
- package/{agents → plugin/agents}/dknet-architect.md +4 -4
- package/{agents → plugin/agents}/dknet-bdd-engineer.md +6 -6
- package/{agents → plugin/agents}/dknet-implementer.md +11 -11
- package/{skills → plugin/skills}/README.md +13 -12
- package/{skills → plugin/skills}/dknet-auth-and-ownership/SKILL.md +100 -36
- package/{skills → plugin/skills}/dknet-bdd-tests/SKILL.md +25 -16
- package/{skills → plugin/skills}/dknet-bdd-tests/checklist.md +1 -1
- package/{skills → plugin/skills}/dknet-crud/SKILL.md +36 -22
- package/{skills → plugin/skills}/dknet-ddd-principles/SKILL.md +62 -13
- package/{skills → plugin/skills}/dknet-docs/SKILL.md +73 -31
- package/{skills → plugin/skills}/dknet-docs/templates/README-template.md +8 -8
- package/{skills → plugin/skills}/dknet-docs/templates/api-reference-template.md +7 -0
- package/{skills → plugin/skills}/dknet-docs/templates/architecture-template.md +14 -8
- package/{skills → plugin/skills}/dknet-docs/templates/data-model-template.md +2 -2
- package/{skills → plugin/skills}/dknet-docs/templates/events-template.md +2 -2
- package/{skills → plugin/skills}/dknet-dto-mapping/SKILL.md +37 -29
- package/{skills → plugin/skills}/dknet-efcore-config/SKILL.md +38 -38
- package/{skills → plugin/skills}/dknet-endpoint/SKILL.md +72 -43
- package/{skills → plugin/skills}/dknet-entity/SKILL.md +43 -33
- package/{skills → plugin/skills}/dknet-feature/SKILL.md +36 -13
- package/{skills → plugin/skills}/dknet-feature-lifecycle/SKILL.md +48 -42
- package/{skills → plugin/skills}/dknet-feature-remove/SKILL.md +16 -15
- package/{skills → plugin/skills}/dknet-messaging-events/SKILL.md +36 -29
- package/{skills → plugin/skills}/dknet-package-adoption/SKILL.md +3 -3
- package/{skills → plugin/skills}/dknet-platform-config/SKILL.md +14 -14
- package/{skills → plugin/skills}/dknet-project-structure/SKILL.md +32 -32
- package/{skills → plugin/skills}/dknet-queries-specs/SKILL.md +21 -13
- package/{skills → plugin/skills}/dknet-scaffold/SKILL.md +3 -2
- package/{skills → plugin/skills}/dknet-unit-tests/SKILL.md +27 -22
- package/plugin.json +3 -3
- /package/{skills → plugin/skills}/dknet-docs/checklist.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: dknet-bdd-tests
|
|
3
|
-
description: Create and maintain Reqnroll + NUnit BDD .feature scenarios and step bindings in
|
|
3
|
+
description: Create and maintain Reqnroll + NUnit BDD .feature scenarios and step bindings in <YourApp>.App.BDDTests — request/status/response-body scenarios and domain-event side effects observed via log capture, for the hand-written PurchaseOrder and generator-driven Product samples. Use when adding or updating HTTP-facing scenarios for a DKNet.Templates feature. Result-level Result-object assertions, architecture rules and pure functional tests belong in the `dknet-unit-tests` skill instead — do not duplicate a behavior here that xUnit already covers. Invoke as `/dknet-bdd-tests <Feature>` to scaffold it for a feature.
|
|
4
4
|
metadata:
|
|
5
5
|
kind: workflow
|
|
6
6
|
arguments: "<Feature> e.g. Orders"
|
|
@@ -9,17 +9,23 @@ allowed-tools: Read, Grep, Glob, Edit, Write, Bash
|
|
|
9
9
|
|
|
10
10
|
Usage: `/dknet-bdd-tests <Feature> e.g. Orders`
|
|
11
11
|
|
|
12
|
-
# BDD tests (
|
|
12
|
+
# BDD tests (<YourApp>.App.BDDTests)
|
|
13
13
|
|
|
14
14
|
## What BDD owns vs xUnit
|
|
15
15
|
|
|
16
|
-
BDD
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
**BDD is the default home for a feature's business rules.** Anything statable as "a caller does X and
|
|
17
|
+
gets Y" — happy paths, refusals, state transitions, precondition conflicts, domain-event side effects
|
|
18
|
+
observed through captured log lines — is a scenario here, because that is the form a team can read as
|
|
19
|
+
the specification of the feature.
|
|
20
|
+
|
|
21
|
+
xUnit (`dknet-unit-tests`) keeps what a scenario cannot express or cannot distinguish:
|
|
22
|
+
architecture/convention rules, pure functional tests (entity methods, validators, specs in isolation),
|
|
23
|
+
EF model/schema/migration shape, and a `Result`-level assertion **only** where the HTTP response does
|
|
24
|
+
not identify which rule fired (two failures sharing one status code, a `NotFoundError` versus an
|
|
25
|
+
ownership filter that also answers `404`). Schema/model/migration assertions never belong in BDD.
|
|
26
|
+
|
|
27
|
+
Never assert the same rule in both suites. If a rule is reachable over HTTP and its response
|
|
28
|
+
identifies it, the scenario is the test — delete the xUnit duplicate rather than keeping both.
|
|
23
29
|
|
|
24
30
|
Both shipped suites are teaching material: every scenario must be about the `PurchaseOrder` (manual) or
|
|
25
31
|
`Product` (automated) sample's business behavior, never about logging, health checks, CORS, security
|
|
@@ -185,7 +191,7 @@ Scenario: A product name that is already taken is refused
|
|
|
185
191
|
The log-line step just asserts on `LogCapture.Messages` — no polling needed here because the response body
|
|
186
192
|
already round-trips through the handler that logs synchronously before returning; if a future scenario
|
|
187
193
|
asserts on an *async consumer's* log line instead (a domain event's own subscriber, not the handler that
|
|
188
|
-
raised it), poll with `Eventually.IsTrueAsync(...)` from
|
|
194
|
+
raised it), poll with `Eventually.IsTrueAsync(...)` from `<YourApp>.App.TestSupport` rather than asserting
|
|
189
195
|
immediately — the in-memory bus publishes non-blocking:
|
|
190
196
|
|
|
191
197
|
```csharp
|
|
@@ -242,7 +248,7 @@ like `"status":"placed"` where the surrounding shape isn't in question.
|
|
|
242
248
|
## Commands
|
|
243
249
|
|
|
244
250
|
```bash
|
|
245
|
-
dotnet test ApiEndpoints
|
|
251
|
+
dotnet test ApiEndpoints/<YourApp>.App.BDDTests/<YourApp>.App.BDDTests.csproj --filter "TestCategory=PurchaseOrder"
|
|
246
252
|
```
|
|
247
253
|
|
|
248
254
|
## Step-by-step
|
|
@@ -302,10 +308,10 @@ Before any BDD design or edits:
|
|
|
302
308
|
### Scope
|
|
303
309
|
|
|
304
310
|
Work only on BDD test artifacts and closely related support wiring:
|
|
305
|
-
- `ApiEndpoints
|
|
306
|
-
- `ApiEndpoints
|
|
307
|
-
- `ApiEndpoints
|
|
308
|
-
- `ApiEndpoints
|
|
311
|
+
- `ApiEndpoints/<YourApp>.App.BDDTests/Features/**/*.feature`
|
|
312
|
+
- `ApiEndpoints/<YourApp>.App.BDDTests/Features/**/Steps/*.cs`
|
|
313
|
+
- `ApiEndpoints/<YourApp>.App.BDDTests/Support/*.cs`
|
|
314
|
+
- `ApiEndpoints/<YourApp>.App.BDDTests/*.csproj`
|
|
309
315
|
|
|
310
316
|
### Constraints
|
|
311
317
|
|
|
@@ -324,6 +330,9 @@ Work only on BDD test artifacts and closely related support wiring:
|
|
|
324
330
|
forwarded DataAnnotations — a scenario expecting `400` from an out-of-range value will fail against
|
|
325
331
|
a `201`. Cover that gap by asserting what happens, or leave it to the manual flow.
|
|
326
332
|
- Do not implement unrelated domain/business logic outside BDD test scope.
|
|
333
|
+
- Cover every business rule the feature exposes over HTTP — happy path, each refusal, each state
|
|
334
|
+
transition. If a rule already has an xUnit `Result`-level test and its HTTP response identifies it
|
|
335
|
+
uniquely, write the scenario here and say in your report that the xUnit duplicate should go.
|
|
327
336
|
|
|
328
337
|
### Workflow
|
|
329
338
|
|
|
@@ -338,7 +347,7 @@ Work only on BDD test artifacts and closely related support wiring:
|
|
|
338
347
|
3. Implement/adjust step bindings in `Steps/*.cs`.
|
|
339
348
|
4. Run validation:
|
|
340
349
|
- `dotnet build -c Release`
|
|
341
|
-
- `dotnet test ApiEndpoints
|
|
350
|
+
- `dotnet test ApiEndpoints/<YourApp>.App.BDDTests`
|
|
342
351
|
5. Report:
|
|
343
352
|
- changed files
|
|
344
353
|
- scenario count
|
|
@@ -34,6 +34,6 @@ Use this checklist before considering BDD scenario work complete.
|
|
|
34
34
|
## Validation
|
|
35
35
|
|
|
36
36
|
- [ ] `dotnet build -c Release` succeeds
|
|
37
|
-
- [ ] `dotnet test ApiEndpoints
|
|
37
|
+
- [ ] `dotnet test ApiEndpoints/<YourApp>.App.BDDTests/<YourApp>.App.BDDTests.csproj` passes
|
|
38
38
|
- [ ] No undefined or pending Reqnroll steps
|
|
39
39
|
- [ ] Scenario names are readable in test output
|
|
@@ -16,14 +16,14 @@ Commands only: Create, Update, a business-rule transition, Delete, and the gener
|
|
|
16
16
|
`dknet-queries-specs`. For DTO shape and Mapster wiring, load `dknet-dto-mapping`. For whether a
|
|
17
17
|
rule belongs on the entity or in a handler, load `dknet-ddd-principles` first.
|
|
18
18
|
|
|
19
|
-
## GlobalUsings already in
|
|
19
|
+
## GlobalUsings already in <YourApp>.AppServices
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
`<YourApp>.AppServices/GlobalUsings.cs` gives every file in the project these without an explicit
|
|
22
22
|
`using`: `DKNet.AspCore.Extensions.ModelBinding` (`[FromClaim]`), `DKNet.SlimBus.Extensions`
|
|
23
23
|
(`Fluents.*`, `NotFoundError`), `System.ComponentModel.DataAnnotations`, `System.Security.Claims`
|
|
24
|
-
(`ClaimTypes`), `System.Text.Json.Serialization`, `FluentResults`,
|
|
24
|
+
(`ClaimTypes`), `System.Text.Json.Serialization`, `FluentResults`, `<YourApp>.Domains.Services`,
|
|
25
25
|
`Microsoft.Extensions.DependencyInjection`, `FluentValidation`, `Mapster`, `MapsterMapper`,
|
|
26
|
-
|
|
26
|
+
`<YourApp>.AppServices.Extensions`, `DKNet.SlimBus.Extensions.LazyMapper`, `<YourApp>.AppServices.Share`
|
|
27
27
|
(`PreconditionCodes`, `IPrincipalProvider`). An action file typically adds only the entity's and its
|
|
28
28
|
own spec's namespace.
|
|
29
29
|
|
|
@@ -38,7 +38,7 @@ The handler method is `OnHandle(TRequest, CancellationToken)`, never `Handle`. B
|
|
|
38
38
|
by assembly scan onto the in-memory bus — no per-message registration. Handlers never call
|
|
39
39
|
`SaveChanges`: the SlimBus EF Core interceptor auto-saves once, after the handler returns. A request
|
|
40
40
|
record is `public sealed record`; its validator and handler are `internal sealed class` — enforced
|
|
41
|
-
by
|
|
41
|
+
by `<YourApp>.App.Tests/Architecture/AppServiceTests.cs`.
|
|
42
42
|
|
|
43
43
|
File layout: one file per verb, request + validator + handler co-located —
|
|
44
44
|
`<Feature>/V1/Actions/<Verb>.cs` (`ManualSample/V1/Actions/Create.cs`, `Update.cs`, `Cancel.cs`,
|
|
@@ -58,6 +58,10 @@ if (string.IsNullOrEmpty(request.ByUser))
|
|
|
58
58
|
}
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
`[FromRequestHeader("X-Header-Name")]` (same populator, same overwrite-always guarantee) reads a
|
|
62
|
+
named request header instead of a claim — for a correlation id or tenant hint, never for identity: a
|
|
63
|
+
header is caller-supplied and is not an authorization signal.
|
|
64
|
+
|
|
61
65
|
The demo authentication provider (`FeatureManagement:EnableDemoAuthentication`) supplies this claim
|
|
62
66
|
locally and in tests, so the guard is reachable but rarely hit — an authenticated caller with a
|
|
63
67
|
missing name claim still has to fail it explicitly, there is no fallback to a system account.
|
|
@@ -88,7 +92,7 @@ from `DataOwnerHook`/the audit hook instead, or hand-write the action.
|
|
|
88
92
|
## FluentValidation
|
|
89
93
|
|
|
90
94
|
A validator is `internal sealed class XValidator : AbstractValidator<TRequest>`, co-located with
|
|
91
|
-
its request. There is no per-route opt-in call:
|
|
95
|
+
its request. There is no per-route opt-in call: `<YourApp>.Api/Configs/FluentValidationConfig.cs`
|
|
92
96
|
registers every validator in the assembly —
|
|
93
97
|
|
|
94
98
|
```csharp
|
|
@@ -99,7 +103,7 @@ builder.Services.AddValidatorsFromAssembly(typeof(AppSetup).Assembly, includeInt
|
|
|
99
103
|
group (`Program.cs`), so it runs on generated routes exactly as it runs on hand-mapped ones. A
|
|
100
104
|
request failing validation never reaches the handler; it short-circuits to `400` (or `409`, see
|
|
101
105
|
below) with no handler code involved. Two hard constraints: the validator class must live in
|
|
102
|
-
|
|
106
|
+
`<YourApp>.AppServices` (one in `<YourApp>.Api` is never registered), and it must be `internal sealed`.
|
|
103
107
|
|
|
104
108
|
**Ordinary input rules** stay inline (`RuleFor(a => a.Amount).GreaterThan(0)`). Keep `Id` out of an
|
|
105
109
|
update validator — an unknown/empty id is a `404` from the handler's spec lookup, not a `400` from
|
|
@@ -142,9 +146,9 @@ If it refused a missing id too, the route's own `404` would be hidden behind a `
|
|
|
142
146
|
|
|
143
147
|
**Validators on generated requests are the supported way to add a business rule to a generated
|
|
144
148
|
route.** `CreateProductRequestValidator` and `DeleteProductRequestValidator` both target generated
|
|
145
|
-
request types (
|
|
149
|
+
request types (`<YourApp>.AppServices.Crud.CreateProductRequest` / `DeleteProductRequest`) — no
|
|
146
150
|
literal `Map*` call is needed for FluentValidation to reach them, only the `using
|
|
147
|
-
|
|
151
|
+
<YourApp>.AppServices.Crud;` and a constructor parameter naming the request type. This is the fix for
|
|
148
152
|
the one thing `[DataAnnotations]` cannot do on a generated route: `[Range]`/`[Required]`/etc.
|
|
149
153
|
forwarded onto a generated request property is **not evaluated**, because .NET's validation source
|
|
150
154
|
generator only recognizes literal `Map*(string, Delegate)` calls in the compiling project's own
|
|
@@ -229,7 +233,7 @@ Naming, mechanical from the entity's own signatures: `Create<Entity>Request` (fr
|
|
|
229
233
|
`<Method><Entity>Request` (from `[CrudAction]`), `Delete<Entity>Request` (always). Handlers mirror
|
|
230
234
|
the name with `Handler` instead of `Request` — `CreateProductHandler`, `ChangePriceProductHandler`,
|
|
231
235
|
`ApproveProductHandler`, `DiscontinueProductHandler`, `AssignSupplierReferenceProductHandler` — all
|
|
232
|
-
in namespace
|
|
236
|
+
in namespace `<YourApp>.AppServices.Crud`, emitted under
|
|
233
237
|
`obj/Generated/DKNet.SlimBus.Generators/.../<Entity>CrudRequests.g.cs` and `...Handlers.g.cs`. They
|
|
234
238
|
are compiler output, not files in the repo — inspect them after a build, not by searching source.
|
|
235
239
|
|
|
@@ -255,7 +259,7 @@ Fetch by a private, generated `ProductByIdCrudSpec`, `404` if missing, call the
|
|
|
255
259
|
|
|
256
260
|
**Replacing a generated handler.** The generated file's own doc comment says how: "Write a class
|
|
257
261
|
implementing the same IHandler to replace it." Add your own `internal sealed class` implementing
|
|
258
|
-
`Fluents.Requests.IHandler<ChangePriceProductRequest, ProductDto>` in
|
|
262
|
+
`Fluents.Requests.IHandler<ChangePriceProductRequest, ProductDto>` in `<YourApp>.AppServices` — DI
|
|
259
263
|
registration is by interface via assembly scan, so yours and the generated one cannot coexist; keep
|
|
260
264
|
only yours (there is no attribute to suppress generation of the handler alone — drop the whole
|
|
261
265
|
route with `CrudMapOptions.Exclude` if you also need to change the request shape).
|
|
@@ -281,12 +285,12 @@ constructor.
|
|
|
281
285
|
|
|
282
286
|
Every body shares `title: "Error"`, `status`, `type` (the status's name, e.g. `"Conflict"`), and
|
|
283
287
|
`traceId` (the current `Activity` id, falling back to `TraceIdentifier`). One registration answers
|
|
284
|
-
all three failure kinds —
|
|
288
|
+
all three failure kinds — `<YourApp>.Api/Configs/FluentValidationConfig.cs`'s single
|
|
285
289
|
`AddErrorResponses(...)` call — there is no second place to configure this.
|
|
286
290
|
|
|
287
291
|
## Architecture rules enforced by tests
|
|
288
292
|
|
|
289
|
-
|
|
293
|
+
`<YourApp>.App.Tests/Architecture/AppServiceTests.cs` and `RecordArchitectureTests.cs` pin, exactly:
|
|
290
294
|
|
|
291
295
|
- Every class implementing `IRequestHandler<>`/`IRequestHandler<,>`/`IConsumer<>` must be
|
|
292
296
|
non-public and `sealed` (`AllHandlerClassesShouldBeInternalAndSealed`).
|
|
@@ -298,13 +302,14 @@ all three failure kinds — `Minimal.Api/Configs/FluentValidationConfig.cs`'s si
|
|
|
298
302
|
- A `[GenerateDto]` type must expose no property whose type (or generic argument) inherits
|
|
299
303
|
`DomainEntity` (`DtosWithGenerateDtoAttribute_ShouldNotHaveProperties_ThatAreDomainEntities`) —
|
|
300
304
|
the DTO boundary must not leak an entity.
|
|
301
|
-
- No record type in
|
|
305
|
+
- No record type in `<YourApp>.Domains` or `<YourApp>.AppServices` may declare a public property with a
|
|
302
306
|
**private** setter (`RecordTypes_ShouldNotContain_PrivateSetters_OnPublicProperties`) — AutoMapper
|
|
303
307
|
(Mapster's `MapToConstructor`/property assignment) cannot fill it.
|
|
304
308
|
|
|
305
309
|
## Step-by-step
|
|
306
310
|
|
|
307
|
-
**mode=manual** (mirror `ManualSample/PurchaseOrder`)
|
|
311
|
+
**mode=manual** (mirror `ManualSample/PurchaseOrder`) — only after confirming the operation is one
|
|
312
|
+
of the five the generator genuinely cannot express (`dknet-feature-lifecycle` §1):
|
|
308
313
|
|
|
309
314
|
1. Add `<Feature>/V1/Actions/Create.cs`: request implementing `IWitResponse<TDto>` with
|
|
310
315
|
`[FromClaim(ClaimTypes.Name)] ByUser`, a co-located validator, a handler constructing the
|
|
@@ -316,6 +321,7 @@ all three failure kinds — `Minimal.Api/Configs/FluentValidationConfig.cs`'s si
|
|
|
316
321
|
`Result.Ok()`.
|
|
317
322
|
4. Wire each into `<Feature>V1Endpoint.cs` with a literal `Map*` call (`dknet-endpoint`) so
|
|
318
323
|
DataAnnotations validation, if any, is actually enforced.
|
|
324
|
+
5. Keep `ApiEndpoints/<YourApp>.Client` in step by adding the matching client methods (`I{Plural}Client`, contracts under `Contracts/`) in the same change.
|
|
319
325
|
|
|
320
326
|
**mode=auto** (mirror `AutomatedSample/Product`):
|
|
321
327
|
|
|
@@ -327,6 +333,7 @@ all three failure kinds — `Minimal.Api/Configs/FluentValidationConfig.cs`'s si
|
|
|
327
333
|
4. For an operation that must write more than one aggregate, or must reject a repeat call as a
|
|
328
334
|
domain failure rather than a `200`, write a `...Command` (not `...Request`) and drop the
|
|
329
335
|
generated route for that member with `CrudMapOptions.Exclude("MethodName")` in the endpoint.
|
|
336
|
+
5. Keep `ApiEndpoints/<YourApp>.Client` in step by adding the matching client methods for every route the endpoint maps, in the same change.
|
|
330
337
|
|
|
331
338
|
## Validation checklist
|
|
332
339
|
|
|
@@ -386,10 +393,17 @@ the entity to already carry `[CrudCreate]`/`[CrudUpdate]`/`[RaisesEvent]`, and t
|
|
|
386
393
|
generate without them. If `mode=` was not supplied, detect it: grep the entity for `[CrudCreate]`;
|
|
387
394
|
present it means `auto`, absent means `manual`. Say which you detected.
|
|
388
395
|
|
|
389
|
-
Pick one per aggregate, don't mix them for the same entity
|
|
396
|
+
Pick one per aggregate, don't mix them for the same entity, and **start from the declarative
|
|
397
|
+
shape**:
|
|
390
398
|
|
|
391
|
-
- **
|
|
392
|
-
|
|
399
|
+
- **Declarative CRUD generation (`mode=auto`, the default)** — `[CrudCreate]`/`[CrudUpdate]`/
|
|
400
|
+
`[CrudAction]` + `[GenerateDto]` on the entity itself; `DKNet.SlimBus.Generators` produces the
|
|
401
|
+
request/handler/route types for you. A business rule that must *refuse* the operation is written
|
|
402
|
+
as a FluentValidation validator against the generated request and still runs, so it is not a
|
|
403
|
+
reason to leave this path.
|
|
404
|
+
- **Hand-written (`mode=manual`)** — every request/validator/handler/spec/DTO is a file you write.
|
|
405
|
+
Step down to it only for idempotent create, an enforced DataAnnotations rule, a multi-aggregate
|
|
406
|
+
transaction, a bespoke query, or `[FromClaim]` acting-user attribution.
|
|
393
407
|
|
|
394
408
|
The `dknet-feature-lifecycle` skill §1 is the authoritative comparison between the two; use it to decide.
|
|
395
409
|
|
|
@@ -405,7 +419,7 @@ The `dknet-feature-lifecycle` skill §1 is the decision procedure if the mode is
|
|
|
405
419
|
#### Required reading
|
|
406
420
|
|
|
407
421
|
1. The reference sections above
|
|
408
|
-
2. `ApiEndpoints
|
|
422
|
+
2. `ApiEndpoints/<YourApp>.AppServices/ManualSample/V1/` (exemplar: `Actions/{Create,Update,Cancel,Delete}.cs`, `Specs/SpecGetPurchaseOrder.cs`, `Queries/{GetPurchaseOrderById,ListPurchaseOrders}.cs`, `Events/`, `PurchaseOrderDto.cs`)
|
|
409
423
|
|
|
410
424
|
#### Steps
|
|
411
425
|
|
|
@@ -430,7 +444,7 @@ The `dknet-feature-lifecycle` skill §1 is the decision procedure if the mode is
|
|
|
430
444
|
|
|
431
445
|
### Path 2: Declarative CRUD generation (`mode=auto`)
|
|
432
446
|
|
|
433
|
-
|
|
447
|
+
The default path: declare the CRUD surface on the entity instead of writing it. Exemplar: `Product` (`ApiEndpoints/<YourApp>.Domains/Features/AutomatedSample/Entities/Product.cs`, `ApiEndpoints/<YourApp>.AppServices/AutomatedSample/V1/ProductDto.cs`).
|
|
434
448
|
|
|
435
449
|
Most of the entity-side attributes are `/dknet-entity mode=auto`'s job. This command's own output at
|
|
436
450
|
the AppServices layer is small on purpose: **one `[GenerateDto]` line, plus any hand-written event
|
|
@@ -460,7 +474,7 @@ consumer.** If you find yourself writing a request, validator, or handler here,
|
|
|
460
474
|
|
|
461
475
|
#### What gets generated
|
|
462
476
|
|
|
463
|
-
`DKNet.SlimBus.Generators` produces (namespace
|
|
477
|
+
`DKNet.SlimBus.Generators` produces (namespace `<YourApp>.AppServices.Crud`, not committed — inspect via `dotnet build` then `obj/Generated/DKNet.SlimBus.Generators/`):
|
|
464
478
|
|
|
465
479
|
- `Create<Entity>Request` / `Change<Member><Entity>Request` (named after the `[CrudUpdate]` method, e.g. `ChangePriceProductRequest`) + matching `internal sealed` handlers (`Create<Entity>Handler` / `Change<Member><Entity>Handler`) — no hand-written request/validator/handler exists for these.
|
|
466
480
|
- `<Entity>CrudEndpointExtensions.Map<Entity>Crud()` — GetById/GetList/Delete map straight to `DKNet.AspCore.Extensions`'s generic `MapGetById<TEntity,TKey,TDto>`/`MapGetList`/`MapDeleteById`; Create/Update use the generated handlers above.
|
|
@@ -469,7 +483,7 @@ consumer.** If you find yourself writing a request, validator, or handler here,
|
|
|
469
483
|
|
|
470
484
|
- Do NOT hand-write a request or handler for a `[CrudCreate]`/`[CrudUpdate]` member while its route is still generated — that defeats the point of the generator. A **validator** is the exception and the supported shape: a FluentValidation validator for the generated request runs on the generated route through the group filter. Exclude a route by name (`CrudMapOptions.Exclude(string)`) and hand-write it below the `Map<Entity>Crud(...)` call only when the operation writes more than one aggregate in one transaction. Generated and hand-written routes coexisting in one endpoint is the shipped shape, not a smell — see `ProductV1Endpoint`.
|
|
471
485
|
- **Validation gap, confirmed live**: a `[Range]`/`[Required]` on a `[CrudCreate]`/`[CrudUpdate]` parameter *is* forwarded onto the generated request property, but it is **never enforced** under this template's endpoint-registration convention — the .NET 10 validation source generator only recognizes literal `Map*(string, Delegate)` calls, and the generated route goes through `DKNet.AspCore.Extensions`'s generic `MapPost<TRequest,TDto>` wrapper instead. `POST /v1/products` with a negative price returns `201`, not `400`. Do not present a DataAnnotations attribute on a generated request as enforced without checking the endpoint's mapping style.
|
|
472
|
-
- Acting-user attribution cannot use `[FromClaim]` on a generated request (the generator forwards only `System.ComponentModel.DataAnnotations` attributes) — it goes through `DKNet.EfCore.DataAuthorization`'s `DataOwnerHook` instead, wired once in
|
|
486
|
+
- Acting-user attribution cannot use `[FromClaim]` on a generated request (the generator forwards only `System.ComponentModel.DataAnnotations` attributes) — it goes through `DKNet.EfCore.DataAuthorization`'s `DataOwnerHook` instead, wired once in `<YourApp>.Api/Configs/ServiceConfigs.cs`, not per-entity.
|
|
473
487
|
- No idempotency key support on the generated create route — see `/dknet-endpoint`'s "Alternative: generated CRUD route" section if the feature needs it.
|
|
474
488
|
|
|
475
489
|
#### Steps
|
|
@@ -24,7 +24,7 @@ An aggregate is a transactional consistency boundary: everything inside it is sa
|
|
|
24
24
|
|
|
25
25
|
- If two pieces of data must always be consistent with each other *at the moment they're saved* (e.g. a `PurchaseOrder`'s `Amount` and its `Status` — cancelling and re-pricing an order in the same save must not leave those two fields disagreeing), they belong in the same aggregate. That's why `PurchaseOrder` owns both `Amount` and `Status` directly rather than splitting them into two persisted types.
|
|
26
26
|
- If two pieces of data can be consistent *eventually*, a moment apart, they belong in separate aggregates, coordinated through a domain event — not a direct object reference.
|
|
27
|
-
- Aggregates reference each other by ID (`Guid`), never by object reference. `PurchaseOrder` (`ApiEndpoints
|
|
27
|
+
- Aggregates reference each other by ID (`Guid`), never by object reference. `PurchaseOrder` (`ApiEndpoints/<YourApp>.Domains/Features/ManualSample/Entities/PurchaseOrder.cs`) does not hold a `Customer` object — it holds a plain `CustomerName` string, no navigation property back to another aggregate.
|
|
28
28
|
|
|
29
29
|
Keep aggregates small. A large aggregate means more contention (every mutation locks the whole thing) and usually signals a boundary was drawn around "things that seem related" rather than "things that must be consistent together."
|
|
30
30
|
|
|
@@ -41,26 +41,73 @@ An invariant is a rule that must always hold true for an entity (e.g. "amount is
|
|
|
41
41
|
|
|
42
42
|
- Properties are `{ get; private set; }`. Nothing outside the entity can put it into an invalid state directly.
|
|
43
43
|
- The constructor establishes the invariant for a new entity. Named mutation methods (`ChangeAmount`, `Cancel` — see `PurchaseOrder` in `dknet-entity`) re-establish it for every mutation, and are the *only* path to changing mutable state.
|
|
44
|
-
- A rule that depends on the entity's **own current state** belongs on the entity, or right next to the fetch in the handler when it must read the stored row first. `PurchaseOrder.Cancel(string userId)` is the clearest example: `CancelPurchaseOrderCommandHandler` (`ApiEndpoints
|
|
44
|
+
- A rule that depends on the entity's **own current state** belongs on the entity, or right next to the fetch in the handler when it must read the stored row first. `PurchaseOrder.Cancel(string userId)` is the clearest example: `CancelPurchaseOrderCommandHandler` (`ApiEndpoints/<YourApp>.AppServices/ManualSample/V1/Actions/Cancel.cs`) checks `order.Status == PurchaseOrderStatus.Cancelled` and fails the request *before* calling `Cancel`; the transition itself still lives on the entity. Note the weakness this leaves — the guard is one call away from being bypassed by a second caller who skips it, so treat handler-side guards as a boundary check, not as the invariant's home.
|
|
45
45
|
- If a rule needs data external to the entity (e.g. "customer name must be unique across all orders"), that's not an entity invariant — it's a cross-entity business rule, and it belongs in the command handler as a duplicate-check `Specification` query (see `dknet-crud`), because the entity has no way to see other entities.
|
|
46
46
|
|
|
47
47
|
## When to Use a Domain Event
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
The trade-offs: you cannot single-step from "constructor ran" to "event raised" the way you can with `AddEvent`, and the payload's name and shape follow a fixed composition rule (`<Entity><NarrowingProps><Operation>Event`) rather than one you choose — verify the composed name against the compiled assembly before wiring a consumer to it.
|
|
54
|
-
|
|
55
|
-
Both styles are delivered identically afterward: `ApiEndpoints/Minimal.Infra/Services/EventPublisher.cs` forwards to `IMessageBus` regardless of which raised the event.
|
|
56
|
-
|
|
57
|
-
Reach for either one when **something outside this aggregate might care that this happened** — another aggregate needs to react, or an external system needs to be notified. `Product`'s declared `Created` event is consumed both in-process and over Azure Service Bus (`ProductCreatedNotificationHandler`), exactly as a hand-raised one would be.
|
|
49
|
+
First decide **whether** there should be an event at all. Reach for one when **something outside
|
|
50
|
+
this aggregate might care that this happened** — another aggregate needs to react, or an external
|
|
51
|
+
system needs to be notified. `Product`'s declared `Created` event is consumed both in-process and
|
|
52
|
+
over Azure Service Bus (`ProductCreatedNotificationHandler`).
|
|
58
53
|
|
|
59
54
|
Do NOT reach for an event when the effect is entirely local to this one request:
|
|
60
|
-
|
|
55
|
+
|
|
56
|
+
- Setting a computed field during the same handler → just do it in the handler or the entity method.
|
|
61
57
|
- A validation failure → return `Result.Fail(...)`, don't publish an event.
|
|
62
58
|
|
|
63
|
-
If you can't name a concrete future subscriber (even a logging handler counts, but "just in case"
|
|
59
|
+
If you can't name a concrete future subscriber (even a logging handler counts, but "just in case"
|
|
60
|
+
doesn't), it's not an event yet — add it when a real consumer appears.
|
|
61
|
+
|
|
62
|
+
### How to raise it — take the highest rung that works
|
|
63
|
+
|
|
64
|
+
| Rung | Form | Use it when |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| 1 | **`[RaisesEvent]` on the class** | The event means "this entity was created" or "these properties changed". Default. |
|
|
67
|
+
| 2 | **`AddEvent<TEvent>()` in a mutation method** | Rung 1 can't decide *whether* to raise — the condition is real logic — but the payload is still a projection of the entity. |
|
|
68
|
+
| 3 | **`AddEvent(new TEvent(...))`** | The payload is not a projection of the entity: it carries a computed value, a before/after pair, or data the entity doesn't hold. |
|
|
69
|
+
|
|
70
|
+
**Rung 1 — declared (`Product`).** The class carries
|
|
71
|
+
`[RaisesEvent(EventOperations.Created, Include = [nameof(Id), nameof(Name), nameof(Price)])]` and
|
|
72
|
+
`[RaisesEvent(EventOperations.Updated, nameof(Price))]`; no line of application code calls
|
|
73
|
+
`AddEvent` anywhere in `AutomatedSample/`. DKNet's EF Core save hook reads the declarations and
|
|
74
|
+
raises the composed records (`ProductCreatedEvent`, `ProductPriceUpdatedEvent`) after a successful
|
|
75
|
+
`SaveChanges`, driven by the change tracker. `<YourApp>.App.Tests/Architecture/SampleInvariantTests`
|
|
76
|
+
fails the build if a hand-written `AddEvent(` appears under `AutomatedSample`.
|
|
77
|
+
|
|
78
|
+
What you give up: you cannot single-step from "constructor ran" to "event raised", and the payload's
|
|
79
|
+
name and shape follow a fixed composition rule (`<Entity><NarrowingProps><Operation>Event`) rather
|
|
80
|
+
than one you choose — verify the composed name against the compiled assembly before wiring a
|
|
81
|
+
consumer to it, it has no source file.
|
|
82
|
+
|
|
83
|
+
**Rung 2 — type-only (`AddEvent<TEvent>()`).** Registers the event *type*; the publisher maps the
|
|
84
|
+
entity onto it (`IMapper.Map(entity, entityType, eventType)`) when the save succeeds:
|
|
85
|
+
|
|
86
|
+
```csharp
|
|
87
|
+
public void Cancel(string byUser)
|
|
88
|
+
{
|
|
89
|
+
if (Status == PurchaseOrderStatus.Cancelled) return; // the raise IS conditional
|
|
90
|
+
Status = PurchaseOrderStatus.Cancelled;
|
|
91
|
+
SetUpdatedBy(byUser);
|
|
92
|
+
AddEvent<PurchaseOrderCancelledEvent>(); // payload projected from the entity
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
You keep control of *when*, and still hand-write no payload. It **requires an `IMapper`
|
|
97
|
+
registration** — with none, the publisher throws `EventException` rather than silently dropping the
|
|
98
|
+
event — and the event record's members must be mappable from the entity's own properties.
|
|
99
|
+
|
|
100
|
+
**Rung 3 — instance (`AddEvent(new …)`, `PurchaseOrder`).** The constructor calls
|
|
101
|
+
`AddEvent(new PurchaseOrderCreatedEvent(Id, CustomerName, Amount))` directly. Full control of
|
|
102
|
+
payload, name and timing, at the cost of a hand-written record and a hand-written construction site
|
|
103
|
+
that can drift from the entity. Take it only when the payload is genuinely not a projection of the
|
|
104
|
+
entity's current state.
|
|
105
|
+
|
|
106
|
+
All three are delivered identically afterward:
|
|
107
|
+
`ApiEndpoints/<YourApp>.Infra/Services/EventPublisher.cs` forwards to `IMessageBus` regardless of
|
|
108
|
+
which raised the event. Mixing rungs on one entity is allowed — a declared `Created` alongside one
|
|
109
|
+
hand-raised transition event is fine — except under `AutomatedSample`, where the architecture test
|
|
110
|
+
pins it to rung 1 only.
|
|
64
111
|
|
|
65
112
|
## Avoiding Anemic Domain Models
|
|
66
113
|
|
|
@@ -77,6 +124,8 @@ The handler's job is orchestration: fetch the entity (via `IRepositorySpec` + a
|
|
|
77
124
|
- [ ] Does this type ever get looked up independently of its parent? If no, it's a value object, not an entity.
|
|
78
125
|
- [ ] Does this rule only need data already on the entity? If yes, enforce it in the entity's constructor/`Update` method, not the handler.
|
|
79
126
|
- [ ] Can I name a real, current subscriber for this event? If no, skip the event for now.
|
|
127
|
+
- [ ] If it is an event: can `[RaisesEvent]` express it? If not, can `AddEvent<TEvent>()`? Only then
|
|
128
|
+
hand-build the payload with `AddEvent(new …)`.
|
|
80
129
|
- [ ] Is the handler computing business logic, or just orchestrating fetch → mutate → persist → map? If it's computing, move the logic onto the entity.
|
|
81
130
|
|
|
82
131
|
---
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: dknet-docs
|
|
3
|
-
description: Generate structured technical documentation and
|
|
3
|
+
description: Generate structured technical documentation for a completed feature and every endpoint it exposes — README, architecture and flow diagrams drawn with archify (Mermaid as the fallback), per-route API reference, data model, and domain events. Use this when documenting an implemented feature. Invoke as `/dknet-docs <Feature>` to scaffold it for a feature.
|
|
4
4
|
metadata:
|
|
5
5
|
kind: workflow
|
|
6
6
|
arguments: "<Feature>"
|
|
@@ -29,7 +29,9 @@ Usage: `/dknet-docs <Feature>`
|
|
|
29
29
|
| `data-model.md` | Entity diagram, properties, constraints, relationships |
|
|
30
30
|
| `events.md` | Domain events catalog with publishers and subscribers |
|
|
31
31
|
|
|
32
|
-
**Diagram tool**:
|
|
32
|
+
**Diagram tool**: **archify first, Mermaid as the fallback** — see *Diagrams* below. Every endpoint the
|
|
33
|
+
feature exposes gets its own section in `api-reference.md`; a feature is not documented until every
|
|
34
|
+
route it publishes is.
|
|
33
35
|
|
|
34
36
|
**Real examples already in this repo**: this skill is about documenting a *new* feature you just
|
|
35
37
|
built, not about the two worked samples that ship with the template — but those samples
|
|
@@ -107,7 +109,7 @@ X-Idempotency-Key: 6e6f4d3c-1b7e-4c7a-9f1d-8a2b5c6d7e01
|
|
|
107
109
|
|
|
108
110
|
### Step 3: Write architecture.md (Diagrams + Data Flow)
|
|
109
111
|
|
|
110
|
-
**What you're doing**: Show how the feature is structured across layers with a vertical slice diagram.
|
|
112
|
+
**What you're doing**: Show how the feature is structured across layers with a vertical slice diagram. Render it with archify per *Diagrams* below; the Mermaid examples here are the fallback shape.
|
|
111
113
|
|
|
112
114
|
**Five diagrams to include**:
|
|
113
115
|
|
|
@@ -124,13 +126,13 @@ Example Vertical Slice Overview diagram (from the `PurchaseOrder` sample):
|
|
|
124
126
|
```mermaid
|
|
125
127
|
graph TD
|
|
126
128
|
Client["Client / Browser"]
|
|
127
|
-
subgraph API["
|
|
129
|
+
subgraph API["<YourApp>.Api"]
|
|
128
130
|
EP["PurchaseOrderV1Endpoint.cs"]
|
|
129
131
|
end
|
|
130
|
-
subgraph AppServices["
|
|
132
|
+
subgraph AppServices["<YourApp>.AppServices"]
|
|
131
133
|
HDL["Command Handlers"]
|
|
132
134
|
end
|
|
133
|
-
subgraph Domains["
|
|
135
|
+
subgraph Domains["<YourApp>.Domains"]
|
|
134
136
|
ENT["PurchaseOrder (AggregateRoot)"]
|
|
135
137
|
end
|
|
136
138
|
DB[("PostgreSQL")]
|
|
@@ -143,6 +145,24 @@ graph TD
|
|
|
143
145
|
|
|
144
146
|
**What you're doing**: Full endpoint documentation with curl examples, request/response schemas, and error codes.
|
|
145
147
|
|
|
148
|
+
**One section per route, no exceptions.** Enumerate the feature's routes from the endpoint config —
|
|
149
|
+
for a generated slice that means every route `Map<Entity>Crud()` publishes (`GET {id}`, `GET /`,
|
|
150
|
+
`POST /`, one `PUT {id}` per `[CrudUpdate]`, `DELETE {id}`, and one route per `[CrudAction]`), minus
|
|
151
|
+
anything `CrudMapOptions.Exclude` drops, plus every hand-mapped route below it. Build once and read
|
|
152
|
+
the generated `<Entity>CrudEndpoints.g.cs` rather than guessing the list. Each section states:
|
|
153
|
+
|
|
154
|
+
| Line | What it must say |
|
|
155
|
+
|---|---|
|
|
156
|
+
| Method + path | Including the `/v{n}` version segment when `EnableVersioning` is on |
|
|
157
|
+
| Authorization | The scope the route requires, and whether it comes from `[EndpointGroupScope]`, `o.Configure(...)` or a hand-mapped `.RequireAuthorization(...)` — or "none, `RequireAuthorization` off" |
|
|
158
|
+
| Idempotency | `X-Idempotency-Key` required, or explicitly "not idempotent — a retry creates a second row" for a generated create route |
|
|
159
|
+
| Request | Every bound field, its type, whether it is required, and where it binds from (body, route, query, claim) |
|
|
160
|
+
| Response | Status code + DTO, and every error status the route can actually return |
|
|
161
|
+
| Enforcement | Whether the route's DataAnnotations are enforced (literal `Map*` call) or forwarded only (generated/generic route) |
|
|
162
|
+
| curl | One runnable example including required headers |
|
|
163
|
+
|
|
164
|
+
Add a `sequence` diagram for any route whose flow is not "bind → validate → handler → save → map".
|
|
165
|
+
|
|
146
166
|
Copy `templates/api-reference-template.md` from this skill's folder and fill it in — Endpoints Summary table, one section per endpoint (query params/request body, response, error table, curl example), Common Error Response Format. Note the pagination-defaults gotcha: a hand-written list query's `pageIndex`/`pageSize` defaults differ from the generated `MapGetList` route's contract (`pageNumber`/`pageSize` default 1/1000, configurable ceiling via `DKNet:ListQuery`, plus `fromDate`/`toDate` windowing) — document whichever contract this feature's route actually uses. Also document the standard error format: `result.Response()` (`DKNet.AspCore.Extensions.Responses`) converts a failed `FluentResults` result into `ProblemDetails` with messages under an `errors` array; a `NotFoundError` produces the same shape with `status: 404`.
|
|
147
167
|
|
|
148
168
|
Example endpoint entry (from the `PurchaseOrder` sample):
|
|
@@ -214,31 +234,43 @@ public sealed record PurchaseOrderCreatedEvent(Guid Id, string CustomerName, dec
|
|
|
214
234
|
|
|
215
235
|
---
|
|
216
236
|
|
|
217
|
-
## Mermaid
|
|
237
|
+
## Diagrams — archify first, Mermaid as the fallback
|
|
218
238
|
|
|
219
|
-
|
|
239
|
+
Draw architecture, flow, sequence, data-flow and lifecycle diagrams with **archify**
|
|
240
|
+
(<https://github.com/tt-a1i/archify>), a skill that turns a small typed JSON spec into a validated,
|
|
241
|
+
self-contained HTML diagram plus SVG/PNG exports. Install it once with
|
|
242
|
+
`npx skills add tt-a1i/archify`, then invoke the `archify` skill and let it author, validate and
|
|
243
|
+
deliver each diagram — do not hand-write its JSON from memory.
|
|
220
244
|
|
|
221
|
-
|
|
222
|
-
|-------------|-----------------|-------------|
|
|
223
|
-
| Component flow | `graph TD` / `graph LR` | Overview of layers, event flows |
|
|
224
|
-
| Request sequence | `sequenceDiagram` | How a specific API call flows step-by-step |
|
|
225
|
-
| Entity classes | `classDiagram` | Class relationships and properties |
|
|
226
|
-
| Entity-Relation | `erDiagram` | Database table structure |
|
|
227
|
-
| State machine | `stateDiagram-v2` | Status transitions |
|
|
228
|
-
| Timeline | `timeline` | Feature evolution, release history |
|
|
245
|
+
Pick the archify type per diagram:
|
|
229
246
|
|
|
230
|
-
|
|
247
|
+
| Diagram | archify type | Mermaid fallback |
|
|
248
|
+
|---|---|---|
|
|
249
|
+
| Vertical slice across the six projects | `architecture` | `graph TD` |
|
|
250
|
+
| Request lifecycle for one route (endpoint → handler → entity → DB → response) | `sequence` | `sequenceDiagram` |
|
|
251
|
+
| Domain-event path (raise → publisher → in-memory + external consumers) | `dataflow` | `graph LR` |
|
|
252
|
+
| Status transitions the handlers actually perform | `lifecycle` | `stateDiagram-v2` |
|
|
253
|
+
| A multi-step business process or approval gate | `workflow` | `graph TD` |
|
|
254
|
+
| Table structure | *(none — keep Mermaid)* | `erDiagram` |
|
|
255
|
+
|
|
256
|
+
Commit both the spec and the export next to the docs that reference them:
|
|
231
257
|
|
|
232
|
-
````md
|
|
233
|
-
```mermaid
|
|
234
|
-
graph TD
|
|
235
|
-
A --> B
|
|
236
258
|
```
|
|
237
|
-
|
|
259
|
+
docs/features/<feature>/diagrams/
|
|
260
|
+
├── <feature>-slice.architecture.json ← archify source, re-render from this
|
|
261
|
+
├── <feature>-slice.svg ← exported, referenced from architecture.md
|
|
262
|
+
├── <feature>-create.sequence.json
|
|
263
|
+
└── <feature>-create.svg
|
|
264
|
+
```
|
|
238
265
|
|
|
239
|
-
|
|
266
|
+
Reference an export from markdown with a plain image link
|
|
267
|
+
(``), so it renders on GitHub, in VS Code preview and
|
|
268
|
+
in a wiki without archify installed.
|
|
240
269
|
|
|
241
|
-
|
|
270
|
+
**Fallback rule:** if archify is not installed and you cannot install it, write the diagram as a
|
|
271
|
+
fenced ```mermaid block in the markdown itself and say in your report that the diagram is Mermaid
|
|
272
|
+
pending an archify render. Never ship a feature doc with no diagram at all. An `erDiagram` stays
|
|
273
|
+
Mermaid either way — archify has no table-schema type.
|
|
242
274
|
|
|
243
275
|
## Feature Docs Folder Structure
|
|
244
276
|
|
|
@@ -248,9 +280,10 @@ docs/
|
|
|
248
280
|
└── purchase-orders/ ← kebab-case folder name
|
|
249
281
|
├── README.md ← Overview (START HERE)
|
|
250
282
|
├── architecture.md ← Diagrams + vertical slice
|
|
251
|
-
├── api-reference.md ←
|
|
283
|
+
├── api-reference.md ← One section per route + examples + curl
|
|
252
284
|
├── data-model.md ← Entity diagram + constraints
|
|
253
285
|
├── events.md ← Domain events + subscribers
|
|
286
|
+
├── diagrams/ ← archify sources + exported SVGs
|
|
254
287
|
└── decisions/ ← Optional ADRs
|
|
255
288
|
└── adr-001-idempotency-key-strategy.md
|
|
256
289
|
```
|
|
@@ -281,16 +314,25 @@ You are producing authoritative feature documentation for a vertical slice that
|
|
|
281
314
|
- how the acting user is attributed (`[FromClaim]` vs `DataOwnerHook`).
|
|
282
315
|
For automated slices, read event names off the compiled assembly, not off a guess at the
|
|
283
316
|
composition rule.
|
|
284
|
-
2.
|
|
317
|
+
2. Enumerate every route the feature publishes (generated + hand-mapped) before writing anything —
|
|
318
|
+
this is the spine of `api-reference.md` and the completeness check at the end.
|
|
319
|
+
3. Render the required artifacts under `docs/features/<feature>/` (or `docs/<feature>/` if the slice is template-internal):
|
|
285
320
|
- `README.md` (feature overview + quick links)
|
|
286
|
-
- `architecture.md` (
|
|
287
|
-
|
|
288
|
-
- `
|
|
289
|
-
|
|
290
|
-
|
|
321
|
+
- `architecture.md` (vertical slice, create sequence, event path — drawn with archify per
|
|
322
|
+
*Diagrams*, Mermaid only if archify is unavailable)
|
|
323
|
+
- `data-model.md` (ER diagram stays Mermaid)
|
|
324
|
+
- `api-reference.md` (one section per route, per the table in Step 4)
|
|
325
|
+
- `events.md` when the feature raises or consumes any event
|
|
326
|
+
- `diagrams/` holding each archify JSON source next to its exported SVG
|
|
327
|
+
4. Cross-link from `docs/features/README.md` (or whichever index file lists features).
|
|
328
|
+
5. Verify every route has a section, every referenced diagram file exists, and any Mermaid block
|
|
329
|
+
renders (no stray fences).
|
|
291
330
|
|
|
292
331
|
### Constraints
|
|
293
332
|
|
|
294
333
|
- Do not invent fields, validators, events, or endpoints — only document what's in the code.
|
|
295
334
|
- Use the templates in the skill folder verbatim where they fit; deviations need a one-line note.
|
|
296
335
|
- No hand-wavey language ("flexible", "robust", "scalable") — describe what the code actually does.
|
|
336
|
+
- Every route the feature publishes has its own `api-reference.md` section. A missing route is a
|
|
337
|
+
failed run, not a trim.
|
|
338
|
+
- Diagrams are archify renders unless archify could not be installed; say which in the report.
|
|
@@ -51,14 +51,14 @@ Authorization: Bearer {token}
|
|
|
51
51
|
|
|
52
52
|
| Layer | Path |
|
|
53
53
|
|-------|------|
|
|
54
|
-
| Domain Entity | `ApiEndpoints
|
|
55
|
-
| EF Core Mapper | `ApiEndpoints
|
|
56
|
-
| Create Handler | `ApiEndpoints
|
|
57
|
-
| Update Handler | `ApiEndpoints
|
|
58
|
-
| Delete Handler | `ApiEndpoints
|
|
59
|
-
| Domain Events | `ApiEndpoints
|
|
60
|
-
| Query Specs | `ApiEndpoints
|
|
61
|
-
| API Endpoints | `ApiEndpoints
|
|
54
|
+
| Domain Entity | `ApiEndpoints/<YourApp>.Domains/Features/{EntityFolder}/Entities/{EntityName}.cs` |
|
|
55
|
+
| EF Core Mapper | `ApiEndpoints/<YourApp>.Infra/Features/{EntityFolder}/Mappers/{EntityName}Mapper.cs` |
|
|
56
|
+
| Create Handler | `ApiEndpoints/<YourApp>.AppServices/{FeatureFolder}/V1/Actions/Create.cs` |
|
|
57
|
+
| Update Handler | `ApiEndpoints/<YourApp>.AppServices/{FeatureFolder}/V1/Actions/Update.cs` |
|
|
58
|
+
| Delete Handler | `ApiEndpoints/<YourApp>.AppServices/{FeatureFolder}/V1/Actions/Delete.cs` |
|
|
59
|
+
| Domain Events | `ApiEndpoints/<YourApp>.AppServices/{FeatureFolder}/V1/Events/` |
|
|
60
|
+
| Query Specs | `ApiEndpoints/<YourApp>.AppServices/{FeatureFolder}/V1/Specs/` |
|
|
61
|
+
| API Endpoints | `ApiEndpoints/<YourApp>.Api/ApiEndpoints/{EntityName}V1Endpoints.cs` |
|
|
62
62
|
|
|
63
63
|
## Related Documentation
|
|
64
64
|
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# {FeatureName} — API Reference
|
|
2
2
|
|
|
3
|
+
> Every route the feature publishes gets its own section below — generated CRUD routes included.
|
|
4
|
+
> Read the list off the built `<Entity>CrudEndpoints.g.cs` plus the endpoint config's hand-mapped
|
|
5
|
+
> calls; do not trim one because it looks obvious. Each section states method + path, the scope it
|
|
6
|
+
> requires and where that scope is declared, whether it is idempotent, every bound field and where it
|
|
7
|
+
> binds from, every status it can return, whether its DataAnnotations are enforced, and a runnable
|
|
8
|
+
> curl. See the `dknet-docs` skill, Step 4.
|
|
9
|
+
|
|
3
10
|
**Base Path**: `/api/v1/{feature-route}`
|
|
4
11
|
**Auth**: Bearer token required on all endpoints
|
|
5
12
|
**Content-Type**: `application/json`
|