@drunkcoding/dknet-implementation-skills 14.1.0 → 14.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +37 -26
  3. package/package.json +2 -3
  4. package/{.claude-plugin → plugin/.claude-plugin}/plugin.json +1 -1
  5. package/{agents → plugin/agents}/dknet-architect.md +4 -4
  6. package/{agents → plugin/agents}/dknet-bdd-engineer.md +6 -6
  7. package/{agents → plugin/agents}/dknet-implementer.md +11 -11
  8. package/{skills → plugin/skills}/README.md +13 -12
  9. package/{skills → plugin/skills}/dknet-auth-and-ownership/SKILL.md +100 -36
  10. package/{skills → plugin/skills}/dknet-bdd-tests/SKILL.md +25 -16
  11. package/{skills → plugin/skills}/dknet-bdd-tests/checklist.md +1 -1
  12. package/{skills → plugin/skills}/dknet-crud/SKILL.md +34 -22
  13. package/{skills → plugin/skills}/dknet-ddd-principles/SKILL.md +62 -13
  14. package/{skills → plugin/skills}/dknet-docs/SKILL.md +73 -31
  15. package/{skills → plugin/skills}/dknet-docs/templates/README-template.md +8 -8
  16. package/{skills → plugin/skills}/dknet-docs/templates/api-reference-template.md +7 -0
  17. package/{skills → plugin/skills}/dknet-docs/templates/architecture-template.md +14 -8
  18. package/{skills → plugin/skills}/dknet-docs/templates/data-model-template.md +2 -2
  19. package/{skills → plugin/skills}/dknet-docs/templates/events-template.md +2 -2
  20. package/{skills → plugin/skills}/dknet-dto-mapping/SKILL.md +37 -29
  21. package/{skills → plugin/skills}/dknet-efcore-config/SKILL.md +38 -38
  22. package/{skills → plugin/skills}/dknet-endpoint/SKILL.md +70 -43
  23. package/{skills → plugin/skills}/dknet-entity/SKILL.md +43 -33
  24. package/{skills → plugin/skills}/dknet-feature/SKILL.md +34 -13
  25. package/{skills → plugin/skills}/dknet-feature-lifecycle/SKILL.md +48 -42
  26. package/{skills → plugin/skills}/dknet-feature-remove/SKILL.md +15 -15
  27. package/{skills → plugin/skills}/dknet-messaging-events/SKILL.md +36 -29
  28. package/{skills → plugin/skills}/dknet-package-adoption/SKILL.md +3 -3
  29. package/{skills → plugin/skills}/dknet-platform-config/SKILL.md +14 -14
  30. package/{skills → plugin/skills}/dknet-project-structure/SKILL.md +32 -32
  31. package/{skills → plugin/skills}/dknet-queries-specs/SKILL.md +21 -13
  32. package/{skills → plugin/skills}/dknet-scaffold/SKILL.md +3 -2
  33. package/{skills → plugin/skills}/dknet-unit-tests/SKILL.md +27 -22
  34. package/plugin.json +3 -3
  35. /package/{skills → plugin/skills}/dknet-docs/checklist.md +0 -0
@@ -12,9 +12,20 @@ Usage: `/dknet-endpoint <Feature> <Entity> [mode=manual|auto] [routePrefix] [ver
12
12
  # Skill: Endpoint Configuration
13
13
 
14
14
  Wires AppServices actions/queries — or an entity's `[CrudCreate]`/`[CrudUpdate]`/`[CrudAction]`
15
- declarations — to HTTP routes via `IEndpointConfig`. Three ways to map a route: pick the one matching
16
- how the feature's actions were built (`mode=manual` or `mode=auto`), and reach for the third only when
17
- neither fits one particular route.
15
+ declarations — to HTTP routes via `IEndpointConfig`.
16
+
17
+ **Pick the highest rung that expresses the route. Do not start at the bottom.**
18
+
19
+ | Rung | Shape | Use it when |
20
+ |---|---|---|
21
+ | 1 | **Option B** — `[CrudCreate]`/`[CrudUpdate]`/`[CrudAction]` on the entity + one `group.Map{Entity}Crud()` call (§3) | The route is create, update, delete, get-by-id, list, or a single-aggregate business action. This is the default for a new feature — declare the operation on the entity and let the generator emit the request, handler and route. |
22
+ | 2 | **Option C** — the package's generic `MapGetById`/`MapGetList`/`MapPost<TRequest,TDto>`/… helpers (§4) | The entity carries no CRUD attributes (a read-mostly reference table, an entity you don't own), but the route is still a plain shape the helpers already implement. |
23
+ | 3 | **Option A** — a literal `group.MapPost/MapGet/MapPut/MapDelete(...)` call dispatching through `IMessageBus` (§2) | Nothing above can express it: multiple aggregates in one transaction, a response shape with no DTO behind it, or a create route that must enforce `.RequiredIdempotentKey()` or a DataAnnotations attribute (§5). |
24
+
25
+ Rungs mix freely inside one `Map(RouteGroupBuilder)` — `ProductV1Endpoint` maps its whole generated
26
+ slice, excludes one route by name, and hand-maps two below it. `mode=manual`/`mode=auto` says which
27
+ rung the feature's *actions* were built for; it does not license dropping to rung 3 for a route rung 1
28
+ would have covered.
18
29
 
19
30
  ## 1. The `IEndpointConfig` contract
20
31
 
@@ -28,7 +39,7 @@ internal sealed class {Entity}V1Endpoint : IEndpointConfig
28
39
  ```
29
40
 
30
41
  - **Discovery**: every non-abstract `IEndpointConfig` in the API assembly is found by
31
- `UseEndpointConfigs(...)`, called once from `Minimal.Api/Program.cs`. Never register a route group
42
+ `UseEndpointConfigs(...)`, called once from `<YourApp>.Api/Program.cs`. Never register a route group
32
43
  by hand.
33
44
  - **Versioning**: `EnableVersioning` (`FeatureManagement`, default `true`) turns `GroupEndpoint` into
34
45
  `/v{version}/{route}` — `"/products"` at `Version => 1` becomes `/v1/products`. Off, the group
@@ -56,11 +67,11 @@ MapDelete(...)` call, dispatching through `IMessageBus` by hand:
56
67
  ```csharp
57
68
  using DKNet.AspCore.Extensions.Responses;
58
69
  using DKNet.AspCore.Idempotency;
59
- using Minimal.AppServices.ManualSample.V1.Actions;
60
- using Minimal.AppServices.ManualSample.V1.Queries;
61
- using PurchaseOrderDto = Minimal.AppServices.ManualSample.V1.PurchaseOrderDto;
70
+ using <YourApp>.AppServices.ManualSample.V1.Actions;
71
+ using <YourApp>.AppServices.ManualSample.V1.Queries;
72
+ using PurchaseOrderDto = <YourApp>.AppServices.ManualSample.V1.PurchaseOrderDto;
62
73
 
63
- namespace Minimal.Api.ApiEndpoints.ManualSample;
74
+ namespace <YourApp>.Api.ApiEndpoints.ManualSample;
64
75
 
65
76
  internal sealed class PurchaseOrderV1Endpoint : IEndpointConfig
66
77
  {
@@ -80,10 +91,6 @@ internal sealed class PurchaseOrderV1Endpoint : IEndpointConfig
80
91
  "Create purchase order. <br/><br/> Note: Idempotency key is required in the header. <br/>" +
81
92
  "X-Idempotency-Key: {IdempotencyKey} <br/>");
82
93
 
83
- group.MapGet("/", async ([AsParameters] ListPurchaseOrdersQuery query, IMessageBus bus, CancellationToken ct) =>
84
- Results.Ok(await bus.Send(query, cancellationToken: ct)))
85
- .WithDescription("Get purchase orders (paged, optionally filtered by customer name).");
86
-
87
94
  group.MapGet("{id:guid}", async (Guid id, IMessageBus bus, CancellationToken ct) =>
88
95
  {
89
96
  var dto = await bus.Send(new GetPurchaseOrderByIdQuery { Id = id }, cancellationToken: ct);
@@ -144,12 +151,12 @@ registers the entire generated CRUD surface; hand-mapped routes for what the gen
144
151
  go below it.
145
152
 
146
153
  ```csharp
147
- using Minimal.AppServices.AutomatedSample.V1;
148
- using Minimal.AppServices.AutomatedSample.V1.Actions;
149
- using Minimal.AppServices.AutomatedSample.V1.Queries;
150
- using Minimal.AppServices.Crud;
154
+ using <YourApp>.AppServices.AutomatedSample.V1;
155
+ using <YourApp>.AppServices.AutomatedSample.V1.Actions;
156
+ using <YourApp>.AppServices.AutomatedSample.V1.Queries;
157
+ using <YourApp>.AppServices.Crud;
151
158
 
152
- namespace Minimal.Api.ApiEndpoints.AutomatedSample;
159
+ namespace <YourApp>.Api.ApiEndpoints.AutomatedSample;
153
160
 
154
161
  internal sealed class ProductV1Endpoint : IEndpointConfig
155
162
  {
@@ -197,7 +204,7 @@ internal sealed class ProductV1Endpoint : IEndpointConfig
197
204
  }
198
205
  ```
199
206
 
200
- `ProductScopes` (`Minimal.Api/ApiEndpoints/AutomatedSample/ProductScopes.cs`) is a plain
207
+ `ProductScopes` (`<YourApp>.Api/ApiEndpoints/AutomatedSample/ProductScopes.cs`) is a plain
201
208
  `internal static class` of policy-name constants — one per authorization scope the routes above
202
209
  require, registered as authorization policies by `AddAuthConfig()` only when `RequireAuthorization`
203
210
  is on:
@@ -229,7 +236,7 @@ PUT {id} (per update request), DELETE {id} and each generated domain-action endp
229
236
  ### Where the generated code lives
230
237
 
231
238
  `DKNet.SlimBus.Generators` emits, per entity, under
232
- `ApiEndpoints/Minimal.AppServices/obj/Generated/DKNet.SlimBus.Generators/.../` — not committed, so
239
+ `ApiEndpoints/<YourApp>.AppServices/obj/Generated/DKNet.SlimBus.Generators/.../` — not committed, so
233
240
  build once (`dotnet build`) and read it there for the exact shape:
234
241
 
235
242
  - `{Entity}CrudRequests.g.cs` — one `sealed partial record` per route (`Create{Entity}Request`,
@@ -305,8 +312,9 @@ group.MapParameterlessActionById<TRequest, TKey, TDto>("{id}/x", "PUT"); // any
305
312
  already resolve `ErrorResponseOptions` via `[FromServices]` — calling these helpers directly gets
306
313
  the same behavior.
307
314
 
308
- Reach for Option C only when neither A nor B fits one particular entity — most features should be
309
- entirely A or entirely B.
315
+ Reach for Option C when the entity carries no CRUD attributes but the route is still one of these
316
+ stock shapes — it is a rung above hand-mapping, not a last resort. Drop to Option A only for a route
317
+ none of these helpers can express.
310
318
 
311
319
  ## 5. Cross-cutting behavior
312
320
 
@@ -333,12 +341,34 @@ Only Option A's literal `group.MapPost("/", async (CreateXRequest req, ...) => .
333
341
  attribute enforced. If a create/update rule must be enforced, either hand-map that one route (Option
334
342
  A) or write it as a FluentValidation validator instead of a DataAnnotations attribute.
335
343
 
336
- **Authorization scopes are conditional on the flag, always.** `RequireAuthorization(...)` needs
337
- authorization middleware to evaluate — that middleware is only added when `FeatureManagement:
338
- RequireAuthorization` is `true`. Calling `.RequireAuthorization(scope)` unconditionally would throw at
339
- request time with the flag off (local development, both test suites). Every scope call in
340
- `ProductV1Endpoint` is therefore gated behind reading the flag from `IOptions<FeatureOptions>`, as
341
- shown in §3 — copy that guard, don't call `RequireAuthorization` bare.
344
+ **Authorization scopes: declare them on the class with `[EndpointGroupScope]` first.**
345
+ `EndpointGroupScopeAttribute` (`DKNet.AspCore.Extensions.Endpoints`, **13.0.0 or newer**) sits above
346
+ the `IEndpointConfig` class and names the scope one or more HTTP methods require:
347
+
348
+ ```csharp
349
+ [EndpointGroupScope(ProductScopes.Read, EndpointHttpMethods.Get)]
350
+ [EndpointGroupScope(ProductScopes.Write, EndpointHttpMethods.Post, EndpointHttpMethods.Put,
351
+ EndpointHttpMethods.Delete)]
352
+ internal sealed class ProductV1Endpoint : IEndpointConfig { /* ... */ }
353
+ ```
354
+
355
+ It is stackable; a declaration naming no method becomes the group's default, and a per-method
356
+ declaration beats that default for its own method. It applies to generated and hand-mapped routes
357
+ alike, skips any route that already names its own policy or allows anonymous access, and **fails
358
+ closed** — once one declaration exists, a served method with neither a declaration nor a group
359
+ default is refused when the group's endpoints are built. Above all, it is applied only when
360
+ `EndpointRegistrationOptions.RequireAuthorization` is `true` (the value `Program.cs` assigns from
361
+ `FeatureOptions.RequireAuthorization`), so it needs no flag check of its own.
362
+
363
+ Fall back to `o.Configure(CrudOp.X, …)`, `o.Configure("RouteName", …)`, or
364
+ `.RequireAuthorization(scope)` on a `RouteHandlerBuilder` **only when two routes sharing one HTTP
365
+ method need different scopes** — `Product`'s `Update` and `AssignSupplierReference` are both `PUT`,
366
+ and only the latter may demand `products.supplier`. Those three forms do *not* self-gate: with the
367
+ flag off no policies were registered and the call throws at request time, so each must sit behind the
368
+ `IOptions<FeatureOptions>` guard shown in §3. The shipped `ProductV1Endpoint` is written exactly this
369
+ way: two class-level declarations, and a guarded per-route override for only the two `PUT` routes
370
+ (`AssignSupplierReference`, `discontinue`) that need their own scope. Full treatment: the
371
+ `dknet-auth-and-ownership` skill.
342
372
 
343
373
  **Status-counts helper.** `group.MapGetStatusCounts<TEntity>("status", new StatusPropertyInfo(nameof(X.Status), typeof(XStatus)))`
344
374
  is a template-local extension (not part of the published package) that groups an entity's rows by an
@@ -365,9 +395,10 @@ skill.
365
395
 
366
396
  ### `mode=manual`
367
397
 
368
- 1. Confirm the feature's `AppServices` layer exposes hand-written request/query records (see the
369
- `dknet-crud` skill).
370
- 2. Create `ApiEndpoints/Minimal.Api/ApiEndpoints/{Feature}/{Entity}V1Endpoint.cs`, `internal sealed`,
398
+ 1. Confirm rung 3 is actually needed — that the route is not one `[CrudCreate]`/`[CrudUpdate]`/
399
+ `[CrudAction]` (rung 1) or a generic helper (rung 2) would have covered — and that the feature's
400
+ `AppServices` layer exposes hand-written request/query records (see the `dknet-crud` skill).
401
+ 2. Create `ApiEndpoints/<YourApp>.Api/ApiEndpoints/{Feature}/{Entity}V1Endpoint.cs`, `internal sealed`,
371
402
  implementing `IEndpointConfig`.
372
403
  3. Map every route as a literal `group.MapPost/MapGet/MapPut/MapDelete(...)` call per §2, dispatching
373
404
  through `IMessageBus`.
@@ -379,25 +410,21 @@ skill.
379
410
  1. Confirm the entity declares `[CrudCreate]`/`[CrudUpdate]`/`[CrudAction]` (see `/dknet-entity`)
380
411
  and its DTO is `[GenerateDto(typeof(Entity))]`.
381
412
  2. Build once so the generated `Map{Entity}Crud()` extension exists.
382
- 3. Create `ApiEndpoints/Minimal.Api/ApiEndpoints/{Feature}/{Entity}V1Endpoint.cs`, calling
383
- `group.Map{Entity}Crud()` — bare, or with an `Action<CrudMapOptions>` per §3 for per-route scopes
384
- or exclusions.
413
+ 3. Create `ApiEndpoints/<YourApp>.Api/ApiEndpoints/{Feature}/{Entity}V1Endpoint.cs`, calling
414
+ `group.Map{Entity}Crud()` — bare, or with an `Action<CrudMapOptions>` per §3 for exclusions.
415
+ Declare authorization scopes with `[EndpointGroupScope]` on the class (§5); use
416
+ `o.Configure(...)` only for a route whose scope its HTTP method cannot decide.
385
417
  4. For anything the generator can't express, hand-map it below the composite call, dropping the
386
418
  generated route it replaces via `o.Exclude(...)` when one exists.
387
419
 
388
420
  ## Verification
389
421
 
390
- ```bash
391
- dotnet build -c Release
392
- dotnet run --project ApiEndpoints/Minimal.Api
393
- ```
394
-
395
- Then exercise the route via `/docs` (Scalar, when `EnableSwagger` is on) or curl:
422
+ `dotnet build -c Release`, then `dotnet run --project ApiEndpoints/<YourApp>.Api` and exercise the
423
+ route via `/docs` (Scalar, when `EnableSwagger` is on) or curl:
396
424
 
397
425
  ```bash
398
426
  curl -X POST https://localhost:5001/v1/{route} \
399
427
  -H "Content-Type: application/json" -H "X-Idempotency-Key: $(uuidgen)" -d '{...}'
400
- curl https://localhost:5001/v1/{route}/{id}
401
428
  ```
402
429
 
403
430
  ## Common mistakes
@@ -406,7 +433,7 @@ curl https://localhost:5001/v1/{route}/{id}
406
433
  |---|---|---|
407
434
  | A `[Range]`/`[Required]` on a `[CrudCreate]`/`[CrudUpdate]` parameter is enforced | A generated route accepts the out-of-range value and returns `201`/`200` | The .NET validation source generator can't see through the package's generic `Map*<TRequest,TDto>` wrapper — see §5 |
408
435
  | `.RequiredIdempotentKey()` works the same on a generated create route | There's no route to call it on unless you exclude `"Create"` and hand-map it | The generated extension's calls are compiler output, not source you can chain onto |
409
- | `RequireAuthorization(scope)` is safe to call unconditionally | It throws at request time when `RequireAuthorization` is off | No authorization middleware is added unless the flag is on — gate every call on `IOptions<FeatureOptions>` |
436
+ | `RequireAuthorization(scope)` is safe to call unconditionally | It throws at request time when `RequireAuthorization` is off | No authorization middleware is added unless the flag is on — gate every call on `IOptions<FeatureOptions>`, or declare the scope with `[EndpointGroupScope]`, which is applied only when the flag is on |
410
437
  | A `[CrudAction]` method parameter named `byUser` is the acting-user stamp | It becomes a caller-settable, body-bound `required string ByUser` on the generated request | Generated requests carry no `[FromClaim]`; the automated sample's acting-user attribution goes through `DataOwnerHook`/`AddCurrentUserProvider` instead — never name a generated action parameter after the acting user |
411
438
  | Excluding a route by a typo'd name is silently ignored | The host throws `ArgumentException` at start-up | `ValidateRouteNames` checks every `Exclude`/`Configure` name against the entity's real route names before the app can serve traffic |
412
439
  | A hand-mapped endpoint can call the generic `MapPost<TRequest,TDto>` helper directly for convenience | It works, but bypasses the DataAnnotations-enforcement your Option A route would otherwise get | Only a literal `group.MapPost("/", async (...) => ...)` — not a call to the generic wrapper — is visible to the validation source generator |
@@ -432,7 +459,7 @@ not apply to it. If `mode=` was not supplied, detect it: a `[CrudCreate]` on the
432
459
  ### Required reading
433
460
 
434
461
  1. The reference sections above
435
- 2. `ApiEndpoints/Minimal.Api/ApiEndpoints/ManualSample/PurchaseOrderV1Endpoint.cs` (exemplar — every route is a literal `group.MapPost/MapGet/MapPut/MapDelete(...)` call against the raw minimal-API surface, base route `/v1/purchase-orders`)
462
+ 2. `ApiEndpoints/<YourApp>.Api/ApiEndpoints/ManualSample/PurchaseOrderV1Endpoint.cs` (exemplar — every route is a literal `group.MapPost/MapGet/MapPut/MapDelete(...)` call against the raw minimal-API surface, base route `/v1/purchase-orders`)
436
463
  3. The `dknet-feature-lifecycle` skill §1 — read the endpoint-registration and request-idempotency rows before choosing a mapping style
437
464
 
438
465
  ### Steps (`mode=manual`)
@@ -455,4 +482,4 @@ not apply to it. If `mode=` was not supplied, detect it: a `[CrudCreate]` on the
455
482
 
456
483
  ### Alternative: generated CRUD route
457
484
 
458
- If the entity is plain CRUD with `[CrudCreate]`/`[CrudUpdate]`/`[GenerateDto]` already in place (see `Product`), skip hand-mapping entirely — the generator emits a `Map<Entity>Crud()` extension (namespace `Minimal.AppServices.Crud`) that wires GetById/GetList/Create/Update/Delete in one call. `ProductV1Endpoint` is the exemplar: `group.MapProductCrud(o => …)` carrying the per-route scopes and one `Exclude("Discontinue")`, plus a `.WithDescription`, with the two routes the generator cannot express hand-mapped below it. This path does **not** get `.RequiredIdempotentKey()` and its DataAnnotations validation is not enforced (the .NET 10 validation source generator can't see through the generic `Map*<TRequest,TDto>` wrapper the generated route uses) — confirmed live: `POST /v1/products` with a negative price returns `201`. Only use this path when idempotency and enforced validation are not required.
485
+ If the entity is plain CRUD with `[CrudCreate]`/`[CrudUpdate]`/`[GenerateDto]` already in place (see `Product`), skip hand-mapping entirely — the generator emits a `Map<Entity>Crud()` extension (namespace `<YourApp>.AppServices.Crud`) that wires GetById/GetList/Create/Update/Delete in one call. `ProductV1Endpoint` is the exemplar: `group.MapProductCrud(o => …)` carrying the per-route scopes and one `Exclude("Discontinue")`, plus a `.WithDescription`, with the two routes the generator cannot express hand-mapped below it. This path does **not** get `.RequiredIdempotentKey()` and its DataAnnotations validation is not enforced (the .NET 10 validation source generator can't see through the generic `Map*<TRequest,TDto>` wrapper the generated route uses) — confirmed live: `POST /v1/products` with a negative price returns `201`. Only use this path when idempotency and enforced validation are not required.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: dknet-entity
3
- description: Create DDD domain entities following this project's AggregateRoot/DomainEntity inheritance pattern, either hand-written (mode=manual) or generator-declared (mode=auto). Use when adding a new domain entity or owned type to Minimal.Domains. Invoke as `/dknet-entity <Feature> <Entity> [mode=manual|auto] [props…]` to scaffold it for a feature.
3
+ description: Create DDD domain entities following this project's AggregateRoot/DomainEntity inheritance pattern, either hand-written (mode=manual) or generator-declared (mode=auto). Use when adding a new domain entity or owned type to <YourApp>.Domains. Invoke as `/dknet-entity <Feature> <Entity> [mode=manual|auto] [props…]` to scaffold it for a feature.
4
4
  metadata:
5
5
  kind: workflow
6
6
  arguments: "<Feature> <Entity> [mode=manual|auto] [props…] e.g. Orders Order mode=manual Number:string Total:decimal"
@@ -13,8 +13,14 @@ Usage: `/dknet-entity <Feature> <Entity> [mode=manual|auto] [props…] e.g. Orde
13
13
 
14
14
  Create domain entities that integrate with this project's DDD infrastructure — `AggregateRoot`,
15
15
  `DomainEntity`, and (rarely) owned value objects — in either of the two shapes this template ships:
16
- hand-written (`mode=manual`, mirrors `ManualSample/PurchaseOrder`) or generator-declared
17
- (`mode=auto`, mirrors `AutomatedSample/Product`).
16
+ generator-declared (`mode=auto`, mirrors `AutomatedSample/Product`) or hand-written
17
+ (`mode=manual`, mirrors `ManualSample/PurchaseOrder`).
18
+
19
+ **`mode=auto` is the default.** Declare the CRUD surface with `[CrudCreate]`/`[CrudUpdate]`/
20
+ `[CrudAction]` and the events with `[RaisesEvent]`; hand-write an entity's operations only for the
21
+ reasons `dknet-feature-lifecycle` §1 lists. Within a hand-written entity, raise events in the same
22
+ order of preference: `[RaisesEvent]`, then `AddEvent<TEvent>()` (payload projected from the entity
23
+ by `IMapper`), then `AddEvent(new …)` with a hand-built payload.
18
24
 
19
25
  If the aggregate boundary, entity-vs-value-object choice, or invariant placement isn't obvious, read
20
26
  `dknet-ddd-principles` first — this skill covers class mechanics, not those judgment calls. Event
@@ -25,11 +31,11 @@ here — this skill only covers how an entity raises an event.
25
31
 
26
32
  ```
27
33
  AuditedEntity<Guid> (DKNet.EfCore.Abstractions.Entities — Id, CreatedBy/On, UpdatedBy/On)
28
- └── DomainEntity (Minimal.Domains/Share/DomainEntity.cs)
29
- └── AggregateRoot (Minimal.Domains/Share/AggregateRoot.cs)
34
+ └── DomainEntity (<YourApp>.Domains/Share/DomainEntity.cs)
35
+ └── AggregateRoot (<YourApp>.Domains/Share/AggregateRoot.cs)
30
36
  ```
31
37
 
32
- `Minimal.Domains/Share/DomainEntity.cs`:
38
+ `<YourApp>.Domains/Share/DomainEntity.cs`:
33
39
 
34
40
  ```csharp
35
41
  public abstract class DomainEntity : AuditedEntity<Guid>
@@ -45,7 +51,7 @@ public abstract class DomainEntity : AuditedEntity<Guid>
45
51
  }
46
52
  ```
47
53
 
48
- `Minimal.Domains/Share/AggregateRoot.cs`:
54
+ `<YourApp>.Domains/Share/AggregateRoot.cs`:
49
55
 
50
56
  ```csharp
51
57
  public abstract class AggregateRoot : DomainEntity
@@ -89,7 +95,7 @@ and every property setter is `private` — don't redeclare any of them.
89
95
 
90
96
  ## mode=manual — hand-written (`ManualSample/PurchaseOrder`)
91
97
 
92
- `Minimal.Domains/Features/ManualSample/Entities/PurchaseOrder.cs`:
98
+ `<YourApp>.Domains/Features/ManualSample/Entities/PurchaseOrder.cs`:
93
99
 
94
100
  ```csharp
95
101
  public enum PurchaseOrderStatus { Draft, Placed, Cancelled }
@@ -137,7 +143,7 @@ public sealed class PurchaseOrder : AggregateRoot
137
143
  }
138
144
  ```
139
145
 
140
- `Minimal.Domains/Features/ManualSample/Entities/PurchaseOrderCreatedEvent.cs` — the whole file:
146
+ `<YourApp>.Domains/Features/ManualSample/Entities/PurchaseOrderCreatedEvent.cs` — the whole file:
141
147
 
142
148
  ```csharp
143
149
  public sealed record PurchaseOrderCreatedEvent(Guid Id, string CustomerName, decimal Amount);
@@ -155,11 +161,14 @@ Rules this shape follows:
155
161
  - Every mutation method calls `SetUpdatedBy(userId)` at the end.
156
162
  - The domain event is raised by hand: `AddEvent(new PurchaseOrderCreatedEvent(...))` inside the
157
163
  constructor, right where the aggregate becomes valid. The event itself is a plain
158
- `public sealed record` next to the entity, in the same file or the same folder.
164
+ `public sealed record` next to the entity, in the same file or the same folder. This is the
165
+ lowest rung — a hand-written entity can still carry `[RaisesEvent]`, or call
166
+ `AddEvent<TEvent>()` and let `IMapper` project the payload; reach for a hand-built payload only
167
+ when it is not a projection of the entity's own state.
159
168
 
160
169
  ## mode=auto — generator-declared (`AutomatedSample/Product`)
161
170
 
162
- `Minimal.Domains/Features/AutomatedSample/Entities/Product.cs` (XML docs trimmed):
171
+ `<YourApp>.Domains/Features/AutomatedSample/Entities/Product.cs` (XML docs trimmed):
163
172
 
164
173
  ```csharp
165
174
  [RaisesEvent(EventOperations.Created, Include = [nameof(Id), nameof(Name), nameof(Price)])]
@@ -207,7 +216,7 @@ public class Product : AggregateRoot, IOwnedBy
207
216
  }
208
217
  ```
209
218
 
210
- **`Product` is not `sealed`.** No architecture test enforces sealing on `Minimal.Domains` entities
219
+ **`Product` is not `sealed`.** No architecture test enforces sealing on `<YourApp>.Domains` entities
211
220
  (only on mappers/handlers/validators), and nothing subclasses it — an inconsistency, not a rule to
212
221
  copy. Default to `sealed` unless you have a concrete reason not to, as `mode=manual` does.
213
222
 
@@ -234,13 +243,14 @@ uses them.
234
243
  - None of the three has a hand-written source file — they're compiled output. Verify a composed name
235
244
  against the built assembly before wiring a consumer to it:
236
245
  ```bash
237
- strings ApiEndpoints/Minimal.Domains/bin/Release/net10.0/Minimal.Domains.dll | grep Event
246
+ strings ApiEndpoints/<YourApp>.Domains/bin/Release/net10.0/<YourApp>.Domains.dll | grep Event
238
247
  ```
239
248
  - `[RaisesEvent]` alone raises nothing — the host must register `DKNet.EfCore.Events`' save hook
240
249
  (`AddSlimBusEfCoreInterceptor<CoreDbContext>()`, already wired in this template) before declared
241
250
  events publish.
242
- - Never both styles for the same logical change: pick `AddEvent` (mode=manual) or `[RaisesEvent]`
243
- (mode=auto) for a given entity's events, not a mix on one property. Writing a consumer for a
251
+ - Never two raise styles for the same logical change: one property's event comes from
252
+ `[RaisesEvent]`, `AddEvent<TEvent>()`, or `AddEvent(new …)` — never a mix on one property. Prefer
253
+ them in that order; see `dknet-ddd-principles` for when each rung is the right one. Writing a consumer for a
244
254
  raised event (either style) is covered by `dknet-messaging-events`, not this skill.
245
255
 
246
256
  ### Generator attributes: `[CrudCreate]`, `[CrudUpdate]`, `[CrudAction]`
@@ -305,11 +315,11 @@ the interface alone doesn't do it.
305
315
 
306
316
  ## Domain services, sequences, and `DomainSchemas`
307
317
 
308
- `Minimal.Domains/Share/DomainSchemas.cs` holds named schema constants (`Migration = "migrate"`,
318
+ `<YourApp>.Domains/Share/DomainSchemas.cs` holds named schema constants (`Migration = "migrate"`,
309
319
  `Profile = "pro"`) for reuse across mappers — neither sample uses one; both pass a literal schema
310
320
  string to `ToTable(...)` instead. Add a constant only when a schema name is reused by >1 entity.
311
321
 
312
- `Minimal.Domains/Share/Sequences.cs` declares named PostgreSQL sequences:
322
+ `<YourApp>.Domains/Share/Sequences.cs` declares named PostgreSQL sequences:
313
323
 
314
324
  ```csharp
315
325
  [SqlSequence]
@@ -322,7 +332,7 @@ public enum Sequences
322
332
  }
323
333
  ```
324
334
 
325
- The domain-service contract pattern that wraps a sequence (`Minimal.Domains/Services/`):
335
+ The domain-service contract pattern that wraps a sequence (`<YourApp>.Domains/Services/`):
326
336
 
327
337
  ```csharp
328
338
  public interface IDomainService; // marker, no members
@@ -333,7 +343,7 @@ public interface ISequenceServices : IDomainService
333
343
  public interface IMembershipService : ISequenceServices; // one sequence, one interface
334
344
  ```
335
345
 
336
- The `Minimal.Infra` implementation is a one-line primary-constructor subclass of an internal
346
+ The `<YourApp>.Infra` implementation is a one-line primary-constructor subclass of an internal
337
347
  `SequenceService` base (see `dknet-efcore-config`). Neither sample uses a sequence; add one the same
338
348
  way `IMembershipService`/`MembershipService` do for `Sequences.Membership`, for entities needing a
339
349
  human-readable sequential id instead of a `Guid`.
@@ -359,9 +369,9 @@ builder.OwnsOne(e => e.ShippingAddress, owned =>
359
369
  });
360
370
  ```
361
371
 
362
- ## Architecture tests that constrain `Minimal.Domains`
372
+ ## Architecture tests that constrain `<YourApp>.Domains`
363
373
 
364
- `Architecture/RecordArchitectureTests.cs` scans `Minimal.Domains`/`Minimal.AppServices` for any
374
+ `Architecture/RecordArchitectureTests.cs` scans `<YourApp>.Domains`/`<YourApp>.AppServices` for any
365
375
  record with a public property that has a **private** setter, and fails — Mapster/AutoMapper can't
366
376
  map those. A hand-written event record (`public sealed record XCreatedEvent(...)`) is positional and
367
377
  passes by construction. `Architecture/SampleInvariantTests.cs` enforces the two-sample split:
@@ -373,10 +383,10 @@ conventions above are followed by convention, not test (unlike mapper/handler/va
373
383
 
374
384
  ## Unit tests to mirror
375
385
 
376
- - `Minimal.App.Tests/Unit/ManualSample/PurchaseOrderTests.cs` — constructor sets properties and
386
+ - `<YourApp>.App.Tests/Unit/ManualSample/PurchaseOrderTests.cs` — constructor sets properties and
377
387
  raises `PurchaseOrderCreatedEvent` (asserted via `order.GetEvents()`); the rehydration constructor
378
388
  does not raise it; mutation methods stamp `UpdatedBy`.
379
- - `Minimal.App.Tests/Unit/AutomatedSample/ProductTests.cs` — constructor sets properties (no event
389
+ - `<YourApp>.App.Tests/Unit/AutomatedSample/ProductTests.cs` — constructor sets properties (no event
380
390
  assertion — declared events don't raise outside a real `SaveChanges`, see
381
391
  `dknet-messaging-events`); reflection asserts `[CrudCreate]`/`[CrudUpdate]` `DataAnnotations` are
382
392
  present, the only thing a unit test can prove about a forwarded-but-unenforced attribute;
@@ -387,7 +397,7 @@ conventions above are followed by convention, not test (unlike mapper/handler/va
387
397
 
388
398
  **mode=manual**
389
399
 
390
- 1. Create `ApiEndpoints/Minimal.Domains/Features/<Feature>/Entities/<Entity>.cs` following the
400
+ 1. Create `ApiEndpoints/<YourApp>.Domains/Features/<Feature>/Entities/<Entity>.cs` following the
391
401
  three-constructor shape above, `AddEvent(new <Entity>CreatedEvent(...))` in the public one.
392
402
  2. Add `public sealed record <Entity>CreatedEvent(...)` next to the entity.
393
403
  3. Mark the class `sealed` unless you have a specific reason not to.
@@ -395,11 +405,11 @@ conventions above are followed by convention, not test (unlike mapper/handler/va
395
405
 
396
406
  **mode=auto**
397
407
 
398
- 1. Create `ApiEndpoints/Minimal.Domains/Features/<Feature>/Entities/<Entity>.cs` following the
408
+ 1. Create `ApiEndpoints/<YourApp>.Domains/Features/<Feature>/Entities/<Entity>.cs` following the
399
409
  attribute shape above (`[RaisesEvent]`, `[CrudCreate]`, `[CrudUpdate]`, `[CrudAction]`, `IOwnedBy`
400
410
  as needed).
401
411
  2. Build once, inspect `obj/Generated/DKNet.SlimBus.Generators/…` for request/handler names and
402
- `strings …/Minimal.Domains.dll | grep Event` for composed event names.
412
+ `strings …/<YourApp>.Domains.dll | grep Event` for composed event names.
403
413
  3. Continue to `dknet-efcore-config` for the mapper (unchanged by this mode).
404
414
 
405
415
  ## mode=manual vs mode=auto — when to pick which
@@ -435,9 +445,9 @@ more than one aggregate, or a domain method needs the acting user's identity as
435
445
  | Adding a `createdBy`/`byUser` constructor parameter to a `[CrudCreate]` constructor | Never — it becomes a caller-settable field on the generated create request. Stamp the acting user from the authenticated caller at save time instead. |
436
446
  | Naming a `[CrudAction]` parameter `byUser` and expecting it to carry the authenticated caller | **What you might expect:** it's populated like `[FromClaim]`. **What actually happens:** it's an ordinary caller-settable body field on the generated request. **Why:** the generator forwards only the parameter list and its `DataAnnotations`; it has no concept of `[FromClaim]`. |
437
447
  | Assuming `ChangePrice`'s `[Range(0.01, double.MaxValue)]` is enforced because it's on the entity | It's forwarded onto the generated request but only *enforced* when the route is a literal `Map*(string, Delegate)` call — generated CRUD routes aren't. See `dknet-crud`. |
438
- | Guessing a composed `[RaisesEvent]` name from the pattern alone | Always verify with `strings ApiEndpoints/Minimal.Domains/bin/Release/net10.0/Minimal.Domains.dll \| grep Event` after building — there's no source file to read. |
448
+ | Guessing a composed `[RaisesEvent]` name from the pattern alone | Always verify with `strings ApiEndpoints/<YourApp>.Domains/bin/Release/net10.0/<YourApp>.Domains.dll \| grep Event` after building — there's no source file to read. |
439
449
  | Expecting an `Updated` rule to fire because a setter ran | It only fires when the named property's value actually changed relative to the change tracker's original value on that save. |
440
- | Sealing `Product`-style logic because "the manual sample is sealed" | `sealed` is a good default but is not enforced on `Minimal.Domains` entities by any architecture test — don't cite one that doesn't exist. |
450
+ | Sealing `Product`-style logic because "the manual sample is sealed" | `sealed` is a good default but is not enforced on `<YourApp>.Domains` entities by any architecture test — don't cite one that doesn't exist. |
441
451
 
442
452
  ---
443
453
 
@@ -462,14 +472,14 @@ the `dknet-feature-lifecycle` skill §1 to choose one and say which you chose.
462
472
 
463
473
  1. The reference sections above (mode=manual / mode=auto sections cover the exemplar shapes in full)
464
474
  2. the `dknet-efcore-config` skill
465
- 3. `manual` exemplar — `ApiEndpoints/Minimal.Domains/Features/ManualSample/Entities/PurchaseOrder.cs`; `auto` exemplar — `ApiEndpoints/Minimal.Domains/Features/AutomatedSample/Entities/Product.cs`
466
- 4. `ApiEndpoints/Minimal.Infra/Features/ManualSample/Mappers/PurchaseOrderConfigs.cs` (exemplar mapper — hand-written in **both** modes; no generator produces this)
475
+ 3. `manual` exemplar — `ApiEndpoints/<YourApp>.Domains/Features/ManualSample/Entities/PurchaseOrder.cs`; `auto` exemplar — `ApiEndpoints/<YourApp>.Domains/Features/AutomatedSample/Entities/Product.cs`
476
+ 4. `ApiEndpoints/<YourApp>.Infra/Features/ManualSample/Mappers/PurchaseOrderConfigs.cs` (exemplar mapper — hand-written in **both** modes; no generator produces this)
467
477
 
468
478
  ### Steps
469
479
 
470
- 1. Use the `dknet-implementer` subagent (via the Agent tool) to execute Steps 1–4 of the implementer protocol: domain entity, schema constant, owned types, EF Core mapper, optional sequence/seed data, then `dotnet ef migrations add <Name> -c CoreDbContext -p Minimal.Infra/Minimal.Infra.csproj` from `ApiEndpoints/`. Apply the mode=manual or mode=auto rules from the sections above verbatim — don't re-derive them.
480
+ 1. Use the `dknet-implementer` subagent (via the Agent tool) to execute Steps 1–4 of the implementer protocol: domain entity, schema constant, owned types, EF Core mapper, optional sequence/seed data, then `dotnet ef migrations add <Name> -c CoreDbContext -p <YourApp>.Infra/<YourApp>.Infra.csproj` from `ApiEndpoints/`. Apply the mode=manual or mode=auto rules from the sections above verbatim — don't re-derive them.
471
481
  2. Run `dotnet build -c Release` and stop on first error.
472
- 3. **`auto` only** — verify composed event-record names against the built DLL (see "Events: `[RaisesEvent]`" above) before wiring a consumer: `strings ApiEndpoints/Minimal.Domains/bin/Release/net10.0/Minimal.Domains.dll | grep <Entity>`.
482
+ 3. **`auto` only** — verify composed event-record names against the built DLL (see "Events: `[RaisesEvent]`" above) before wiring a consumer: `strings ApiEndpoints/<YourApp>.Domains/bin/Release/net10.0/<YourApp>.Domains.dll | grep <Entity>`.
473
483
  4. Report:
474
484
  - mode used,
475
485
  - files created (relative paths),
@@ -479,5 +489,5 @@ the `dknet-feature-lifecycle` skill §1 to choose one and say which you chose.
479
489
 
480
490
  ### Constraints
481
491
 
482
- - Do NOT touch `Minimal.AppServices` or `Minimal.Api` here — those belong to `/dknet-crud` and `/dknet-endpoint`.
492
+ - Do NOT touch `<YourApp>.AppServices` or `<YourApp>.Api` here — those belong to `/dknet-crud` and `/dknet-endpoint`.
483
493
  - Entity/mapper/event rules: see Validation checklist above — verify against it, don't restate it.
@@ -39,12 +39,15 @@ of the same output.
39
39
  | `auto` | `AutomatedSample` / `Product` | `[RaisesEvent]` / `[CrudCreate]` / `[CrudUpdate]` / `[CrudAction]` on the entity plus a one-line `[GenerateDto]`. Requests, handlers, and routes are generated. No idempotency, **forwarded DataAnnotations not enforced** (a FluentValidation validator on a generated request *is*), acting user via `DataOwnerHook`. |
40
40
 
41
41
  If `mode=` was not supplied, apply §1 of the lifecycle skill, **recommend one with a reason**, and ask
42
- the user to confirm. Default to `manual` whenever the request mentions an operation that writes more
43
- than one aggregate in one transaction, idempotent writes, a filtered query, a DTO that hides fields,
44
- or attribute-declared validation that must return `400` — `auto` is a deliberate trade, not a
45
- fallback. A business rule, state transition or duplicate check on its own does **not** force
46
- `manual`: write it as a FluentValidation validator on the generated request, the way the product
47
- sample refuses a duplicate name and a delete of a product still for sale.
42
+ the user to confirm. **Default to `auto`.** Recommend `manual` only when the request actually needs
43
+ one of the five things the generator cannot express: an operation writing more than one aggregate in
44
+ one transaction, idempotent writes, a query beyond the generic list route's
45
+ `filter`/`search`/`orderBy` contract, `[FromClaim]` acting-user attribution, or attribute-declared
46
+ validation that must return `400` and cannot be restated as a FluentValidation rule. A business
47
+ rule, state transition, duplicate check or a DTO that hides fields does **not** force `manual`:
48
+ write the rule as a FluentValidation validator on the generated request (as the product sample
49
+ refuses a duplicate name and a delete of a product still for sale) and narrow the DTO with
50
+ `[GenerateDto(..., Exclude = [...])]`.
48
51
 
49
52
  When `auto` is selected, state the validation gap in your confirmation message: a `[Range]` on a
50
53
  generated request property is forwarded but never enforced, so a `POST` with an invalid value returns
@@ -74,7 +77,9 @@ Run `/dknet-entity <Feature> <Entity> mode=<mode> <props…>`. Verify in both mo
74
77
  - `dotnet build` is green.
75
78
 
76
79
  Additionally verify, by mode:
77
- - `manual` — mutation methods raise events via `AddEvent(...)`; a hand-written event record exists.
80
+ - `manual` — every event is raised at the highest rung that fits: `[RaisesEvent]` where the event is
81
+ "created" or "these properties changed", else `AddEvent<TEvent>()` with the payload projected from
82
+ the entity, and a hand-built `AddEvent(new …)` only where the payload is not a projection.
78
83
  - `auto` — class-level `[RaisesEvent(...)]`, a `[CrudCreate]` constructor, and at least one
79
84
  `[CrudUpdate]` method are present. No `AddEvent` call anywhere in the slice.
80
85
 
@@ -89,24 +94,40 @@ Build green either way.
89
94
  ### Phase 4 — Endpoint
90
95
 
91
96
  Run `/dknet-endpoint <Feature> <Entity> mode=<mode>`. Verify the new `*V1Endpoint : IEndpointConfig` exists and:
92
- - `manual` — every route is a literal `group.MapPost/MapGet/MapPut/MapDelete(...)` call, and the create route chains `.RequiredIdempotentKey()`.
97
+ - `manual` — every route is a literal `group.MapPost/MapGet/MapPut/MapDelete(...)` call, and the create route chains `.RequiredIdempotentKey()`. Confirm each hand-mapped route is one no CRUD attribute and no generic `Map*` helper could have covered; report any that could have been generated.
93
98
  - `auto` — the body is a single `group.Map<Entity>Crud()` call. There is no `.RequiredIdempotentKey()` on this path; do not add one, it will not compile onto the generated route.
94
99
 
100
+ Authorization scopes, either mode: declared with `[EndpointGroupScope]` on the endpoint class where
101
+ the package version supports it, dropping to `o.Configure(...)`/`.RequireAuthorization(...)` — each
102
+ behind the `FeatureOptions.RequireAuthorization` guard — only for a route whose HTTP method cannot
103
+ decide its scope.
104
+
95
105
  Build green.
96
106
 
97
107
  ### Phase 5 — Unit/integration tests
98
108
 
99
- Run `/dknet-unit-tests <Feature> <Entity> mode=<mode>`. Verify all tests pass and cover happy path, not-found, and domain events in both modes, plus by mode:
100
- - `manual` — FluentValidation failures, duplicate detection, and any rejected state transition.
109
+ Run `/dknet-unit-tests <Feature> <Entity> mode=<mode>`. xUnit here covers only what a BDD scenario
110
+ cannot: architecture/convention rules, pure functional tests (entity methods, validators, specs),
111
+ EF model/schema shape, and a `Result`-level assertion where the HTTP response cannot tell two
112
+ failures apart. Every business rule reachable over HTTP is Phase 6's job — do not write it twice.
113
+ - `manual` — validator rules and rejected state transitions asserted at the unit level.
101
114
  - `auto` — entity-method behavior directly. Do **not** write a test asserting a `400` from a forwarded DataAnnotations attribute; it will return `201` and the test would encode the gap as expected behavior.
102
115
 
103
116
  ### Phase 6 — BDD acceptance tests
104
117
 
105
- Dispatch the `dknet-bdd-engineer` subagent with the feature scope. Verify `.feature` + step files exist with status + shape + key-field assertions and the BDD project passes.
118
+ Dispatch the `dknet-bdd-engineer` subagent with the feature scope. **This is where the feature's
119
+ business rules are specified** — happy path, every refusal, every state transition, and the
120
+ domain-event side effects visible in captured logs. Verify `.feature` + step files exist with
121
+ status + shape + key-field assertions, that every route from Phase 4 appears in at least one
122
+ scenario, and that the BDD project passes.
106
123
 
107
124
  ### Phase 7 — Feature documentation
108
125
 
109
- Run `/dknet-docs <Feature>`. Verify README, architecture diagrams, data-model, and api-reference exist under `docs/features/<feature-kebab>/` (or `docs/<feature>/` if internal).
126
+ Run `/dknet-docs <Feature>`. Verify README, architecture diagrams, data-model, and api-reference
127
+ exist under `docs/features/<feature-kebab>/` (or `docs/<feature>/` if internal), that
128
+ `api-reference.md` has a section for **every** route Phase 4 published, and that the architecture,
129
+ sequence and event diagrams are archify renders committed with their JSON sources under
130
+ `diagrams/` (Mermaid only if archify could not be installed — the report must say so).
110
131
 
111
132
  ### Phase 8 — Final gates
112
133
 
@@ -118,7 +139,7 @@ Run `/dknet-docs <Feature>`. Verify README, architecture diagrams, data-model, a
118
139
  - Migration name + tables.
119
140
  - Endpoints (route + verbs), flagging whether the create route is idempotent.
120
141
  - Test counts (unit + BDD).
121
- - Docs paths.
142
+ - Docs paths, and whether the diagrams are archify renders or Mermaid fallbacks.
122
143
  - For `auto`: the exact generated type names produced, and a plain statement that the forwarded
123
144
  DataAnnotations validation on those routes is not enforced.
124
145
  - `/dknet-feature-remove <Feature>` as the way to retire the slice.