@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.
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 +36 -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 +72 -43
  23. package/{skills → plugin/skills}/dknet-entity/SKILL.md +43 -33
  24. package/{skills → plugin/skills}/dknet-feature/SKILL.md +36 -13
  25. package/{skills → plugin/skills}/dknet-feature-lifecycle/SKILL.md +48 -42
  26. package/{skills → plugin/skills}/dknet-feature-remove/SKILL.md +16 -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
@@ -11,21 +11,21 @@ of this same map — read it too if you're unsure which project a file belongs i
11
11
  ## Layer boundaries and dependency direction
12
12
 
13
13
  ```
14
- Minimal.Api → entry point, endpoints, auth, OpenAPI
14
+ <YourApp>.Api → entry point, endpoints, auth, OpenAPI
15
15
  ↓
16
- Minimal.AppServices → CQRS handlers, validators, DTOs, domain event handlers
16
+ <YourApp>.AppServices → CQRS handlers, validators, DTOs, domain event handlers
17
17
  ↓
18
- Minimal.Domains → entities, aggregate roots, domain service contracts
18
+ <YourApp>.Domains → entities, aggregate roots, domain service contracts
19
19
  ↑
20
- Minimal.Infra → EF Core (CoreDbContext), repos, event publisher, service bus
20
+ <YourApp>.Infra → EF Core (CoreDbContext), repos, event publisher, service bus
21
21
  (wires into Api via InfraSetup.AddInfraServices)
22
22
 
23
- Minimal.Share → shared constants/options/base types (read by all layers)
24
- Minimal.AppHost → Aspire orchestration only (Redis + PostgreSQL + Minimal.Api), no business logic
23
+ <YourApp>.Share → shared constants/options/base types (read by all layers)
24
+ <YourApp>.AppHost → Aspire orchestration only (Redis + PostgreSQL + <YourApp>.Api), no business logic
25
25
  ```
26
26
 
27
- Every project reference points inward: `Minimal.Domains` references only `Minimal.Share`;
28
- `Minimal.AppServices` depends only on `Domains` (+ `Share`); `Minimal.Infra` and `Minimal.Api`
27
+ Every project reference points inward: `<YourApp>.Domains` references only `<YourApp>.Share`;
28
+ `<YourApp>.AppServices` depends only on `Domains` (+ `Share`); `<YourApp>.Infra` and `<YourApp>.Api`
29
29
  depend on both, never the reverse. An outward reference (`Domains` → `AppServices`, say) is a
30
30
  circular project reference MSBuild refuses outright — the compiler holds this boundary, not a test.
31
31
 
@@ -33,15 +33,15 @@ circular project reference MSBuild refuses outright — the compiler holds this
33
33
 
34
34
  | Project | Owns |
35
35
  |---|---|
36
- | `Minimal.Domains` | Entities, aggregate roots, owned types, domain service **contracts** (`IDomainService`) |
37
- | `Minimal.Infra` | EF Core mappers, seed data, repositories, `EventPublisher`, service-bus topology, domain service **implementations** |
38
- | `Minimal.AppServices` | Command/query requests, handlers, FluentValidation validators, specs, DTOs, domain-event consumers |
39
- | `Minimal.Api` | `IEndpointConfig` route groups, auth policies, platform `Configs/` |
40
- | `Minimal.Share` | Cross-cutting constants/options (`FeatureOptions`, `SharedConsts`) read by every layer |
41
- | `Minimal.AppHost` | Aspire orchestration (Redis, PostgreSQL, sample-data generation) — no business logic |
42
-
43
- `Minimal.App.Tests` (xUnit + Shouldly), `Minimal.App.BDDTests` (Reqnroll + NUnit) and
44
- `Minimal.App.TestSupport` (shared `WebApplicationFactory` base) round out the solution but carry no
36
+ | `<YourApp>.Domains` | Entities, aggregate roots, owned types, domain service **contracts** (`IDomainService`) |
37
+ | `<YourApp>.Infra` | EF Core mappers, seed data, repositories, `EventPublisher`, service-bus topology, domain service **implementations** |
38
+ | `<YourApp>.AppServices` | Command/query requests, handlers, FluentValidation validators, specs, DTOs, domain-event consumers |
39
+ | `<YourApp>.Api` | `IEndpointConfig` route groups, auth policies, platform `Configs/` |
40
+ | `<YourApp>.Share` | Cross-cutting constants/options (`FeatureOptions`, `SharedConsts`) read by every layer |
41
+ | `<YourApp>.AppHost` | Aspire orchestration (Redis, PostgreSQL, sample-data generation) — no business logic |
42
+
43
+ `<YourApp>.App.Tests` (xUnit + Shouldly), `<YourApp>.App.BDDTests` (Reqnroll + NUnit) and
44
+ `<YourApp>.App.TestSupport` (shared `WebApplicationFactory` base) round out the solution but carry no
45
45
  feature code of their own.
46
46
 
47
47
  ## Vertical-slice folder footprint for one feature
@@ -51,12 +51,12 @@ the two shipped samples, `ManualSample` and `AutomatedSample`, under `ApiEndpoin
51
51
 
52
52
  | Layer | `mode=manual` | `mode=auto` |
53
53
  |---|---|---|
54
- | Domains | `Minimal.Domains/Features/<Feature>/Entities/` — hand-written mutation + `AddEvent(...)` | same path — class-level `[RaisesEvent]`, `[CrudCreate]` ctor, `[CrudUpdate]`/`[CrudAction]` methods |
55
- | Infra | `Minimal.Infra/Features/<Feature>/Mappers/` (`IEntityTypeConfiguration<T>`), optional `StaticData/` (seed data) | same `Mappers/` (still hand-written — no generator produces it), optional `ExternalEvents/` (broker consumers) |
56
- | AppServices | `Minimal.AppServices/<Feature>/V1/Actions/`, `Queries/`, `Specs/`, `Events/`, `<Feature>Dto.cs` | `Minimal.AppServices/<Feature>/V1/<Feature>Dto.cs` (one `[GenerateDto]` line), `Events/` (consumers only — generator raises, doesn't consume), plus optional `Validators/` (precondition rules against a generated request), `Actions/` (any operation dropped out of the generated map), `Queries/`/`Specs/` (custom read shapes), and a Mapster `IRegister` for any hand-added DTO property |
57
- | Api | `Minimal.Api/ApiEndpoints/<Feature>/<Entity>V1Endpoint.cs` — every route a literal `Map*` call | same file — one `group.Map<Entity>Crud(o => …)` call, plus any routes excluded from it mapped literally below |
58
- | Tests | `Minimal.App.Tests/Unit/<Feature>/`, `Minimal.App.Tests/Integration/<Feature>/V1/` | same |
59
- | BDD | `Minimal.App.BDDTests/Features/<Plural>/*.feature` + `Steps/*.cs` | same |
54
+ | Domains | `<YourApp>.Domains/Features/<Feature>/Entities/` — hand-written mutation + `AddEvent(...)` | same path — class-level `[RaisesEvent]`, `[CrudCreate]` ctor, `[CrudUpdate]`/`[CrudAction]` methods |
55
+ | Infra | `<YourApp>.Infra/Features/<Feature>/Mappers/` (`IEntityTypeConfiguration<T>`), optional `StaticData/` (seed data) | same `Mappers/` (still hand-written — no generator produces it), optional `ExternalEvents/` (broker consumers) |
56
+ | AppServices | `<YourApp>.AppServices/<Feature>/V1/Actions/`, `Queries/`, `Specs/`, `Events/`, `<Feature>Dto.cs` | `<YourApp>.AppServices/<Feature>/V1/<Feature>Dto.cs` (one `[GenerateDto]` line), `Events/` (consumers only — generator raises, doesn't consume), plus optional `Validators/` (precondition rules against a generated request), `Actions/` (any operation dropped out of the generated map), `Queries/`/`Specs/` (custom read shapes), and a Mapster `IRegister` for any hand-added DTO property |
57
+ | Api | `<YourApp>.Api/ApiEndpoints/<Feature>/<Entity>V1Endpoint.cs` — every route a literal `Map*` call | same file — one `group.Map<Entity>Crud(o => …)` call, plus any routes excluded from it mapped literally below |
58
+ | Tests | `<YourApp>.App.Tests/Unit/<Feature>/`, `<YourApp>.App.Tests/Integration/<Feature>/V1/` | same |
59
+ | BDD | `<YourApp>.App.BDDTests/Features/<Plural>/*.feature` + `Steps/*.cs` | same |
60
60
 
61
61
  The domain/AppServices feature folder name doesn't have to match the BDD folder's plural — the two
62
62
  samples happen to (`ManualSample`↔`PurchaseOrders`, `AutomatedSample`↔`Products`).
@@ -65,16 +65,16 @@ samples happen to (`ManualSample`↔`PurchaseOrders`, `AutomatedSample`↔`Produ
65
65
 
66
66
  | What | Found by | Scans |
67
67
  |---|---|---|
68
- | HTTP route group | `IEndpointConfig` | `UseEndpointConfigs`, assembly scan of `Minimal.Api` |
69
- | Command/query request+handler | `Fluents.Requests.*`/`Fluents.Queries.*` | `AutoDeclareFrom`/`AddServicesFromAssembly` on the in-memory bus, scanning `Minimal.AppServices` |
68
+ | HTTP route group | `IEndpointConfig` | `UseEndpointConfigs`, assembly scan of `<YourApp>.Api` |
69
+ | Command/query request+handler | `Fluents.Requests.*`/`Fluents.Queries.*` | `AutoDeclareFrom`/`AddServicesFromAssembly` on the in-memory bus, scanning `<YourApp>.AppServices` |
70
70
  | Request validator | `AbstractValidator<TRequest>` | `AddValidatorsFromAssembly(typeof(AppSetup).Assembly, includeInternalTypes: true)` |
71
71
  | EF Core table mapping | `IEntityTypeConfiguration<T>` | `UseAutoConfigModel([...])` — must be wired in **both** `InfraSetup.AddInfraServices` and `InfraMigration.MigrateDb` |
72
72
  | Seed data | `DataSeedingConfiguration<T>` (base class, not an interface) | `UseAutoDataSeeding([...])` — same both-places rule; wiring only one is a real bug this template hit once |
73
- | Repos/domain services | — (not auto-discovered) | Explicit `AddScoped<IService, Service>()` line in `InfraSetup.AddInfraServices`; keep implementations `internal sealed` under `Minimal.Infra/Services/` |
74
- | DTO mapping | `[MapsFrom(typeof(Entity))]` or `[GenerateDto(typeof(Entity))]` | Mapster `ScanMaps()` in `Minimal.AppServices/AppSetup.cs` |
73
+ | Repos/domain services | — (not auto-discovered) | Explicit `AddScoped<IService, Service>()` line in `InfraSetup.AddInfraServices`; keep implementations `internal sealed` under `<YourApp>.Infra/Services/` |
74
+ | DTO mapping | `[MapsFrom(typeof(Entity))]` or `[GenerateDto(typeof(Entity))]` | Mapster `ScanMaps()` in `<YourApp>.AppServices/AppSetup.cs` |
75
75
  | Custom Mapster config | `IRegister` | `config.Scan(assembly)`, same `AppSetup.cs` |
76
76
  | Internal event consumer | `Fluents.EventsConsumers.IHandler<TEvent>` in `AppServices` | `AddServicesFromAssembly` on the in-memory child bus |
77
- | External (broker) event consumer | same interface in `Minimal.Infra/Features/<Feature>/ExternalEvents/` | `AddServicesFromAssembly` on the Azure child bus — reached only via an explicit `azb.Produce`/`azb.Consume` pair in `ServiceBusSetup.cs` |
77
+ | External (broker) event consumer | same interface in `<YourApp>.Infra/Features/<Feature>/ExternalEvents/` | `AddServicesFromAssembly` on the Azure child bus — reached only via an explicit `azb.Produce`/`azb.Consume` pair in `ServiceBusSetup.cs` |
78
78
 
79
79
  ## The two shipped samples
80
80
 
@@ -92,12 +92,12 @@ Run from the solution root — `dotnet build`/`dotnet test` need no explicit `.s
92
92
  ```bash
93
93
  dotnet build -c Release
94
94
  dotnet test --settings coverage.runsettings --collect:"XPlat Code Coverage"
95
- dotnet run --project ApiEndpoints/Minimal.Api # API only
96
- dotnet run --project ApiEndpoints/Minimal.AppHost # Redis + PostgreSQL via Aspire
95
+ dotnet run --project ApiEndpoints/<YourApp>.Api # API only
96
+ dotnet run --project ApiEndpoints/<YourApp>.AppHost # Redis + PostgreSQL via Aspire
97
97
 
98
98
  cd ApiEndpoints
99
- dotnet ef migrations add <Name> -c CoreDbContext -p Minimal.Infra/Minimal.Infra.csproj
100
- dotnet ef migrations remove -c CoreDbContext -p Minimal.Infra/Minimal.Infra.csproj
99
+ dotnet ef migrations add <Name> -c CoreDbContext -p <YourApp>.Infra/<YourApp>.Infra.csproj
100
+ dotnet ef migrations remove -c CoreDbContext -p <YourApp>.Infra/<YourApp>.Infra.csproj
101
101
  ```
102
102
 
103
103
  See `dknet-scaffold` for the install/generate steps and how these project names map onto your own
@@ -1,12 +1,18 @@
1
1
  ---
2
2
  name: dknet-queries-specs
3
- description: Write read/query logic for a DKNet feature — Specification<T> filters, hand-written query requests/handlers dispatched over IMessageBus, and the generic filter/search/order/page list route every generator-driven CRUD slice gets for free. Use after the domain entity and DTO exist, whenever a feature needs anything more than the default GET-by-id.
3
+ description: Write read/query logic for a DKNet feature — the generic filter/search/order/page list route every generator-driven CRUD slice gets for free, Specification<T> filters, and hand-written query requests/handlers over IMessageBus for the shapes that route cannot express. Use after the domain entity and DTO exist, whenever a feature needs anything more than the default GET-by-id.
4
4
  ---
5
5
 
6
6
  # Skill: Queries and Specifications
7
7
 
8
- Covers the read side of a feature: specifications, hand-written queries (`mode=manual`), and the
9
- generic list route every `[CrudCreate]`-declared entity gets automatically (`mode=auto`).
8
+ Covers the read side of a feature: specifications, the generic list route every `[CrudCreate]`-declared
9
+ entity gets automatically (`mode=auto`), and hand-written queries (`mode=manual`).
10
+
11
+ **Check the generic list route (§3) before writing a query.** Its `filter`/`search`/`orderBy`/paging
12
+ plus `fromDate`/`toDate` contract covers most list needs with no handler, validator or query object
13
+ at all. Write a hand-written query (§2) only for what it cannot express: a predicate over something
14
+ that is not a DTO field, a projection that is not the DTO, an aggregate/summary shape, or a
15
+ get-by-id that must apply an extra rule.
10
16
 
11
17
  ## 1. Specifications — why, and the WHERE-FALSE trap
12
18
 
@@ -39,7 +45,7 @@ internal sealed class SpecGetPurchaseOrder : Specification<PurchaseOrder>
39
45
  }
40
46
  ```
41
47
 
42
- (`Minimal.AppServices/ManualSample/V1/Specs/SpecGetPurchaseOrder.cs`; the automated sample's
48
+ (`<YourApp>.AppServices/ManualSample/V1/Specs/SpecGetPurchaseOrder.cs`; the automated sample's
43
49
  `SpecGetProduct` and `SpecProductByName` follow the same shape.)
44
50
 
45
51
  - **`CreatePredicate()`** with no `.And(...)`/`.Or(...)` ever called compiles to `WHERE FALSE`. Any
@@ -51,14 +57,16 @@ internal sealed class SpecGetPurchaseOrder : Specification<PurchaseOrder>
51
57
  building `predicator` without ever calling it is a no-op spec.
52
58
  - **Specs live in `<Feature>/V1/Specs/`, `internal sealed`** — `SpecGetPurchaseOrder`,
53
59
  `SpecGetProduct`, `SpecProductByName` all follow this.
54
- - **Specs are reused outside query handlers, too.** `SpecProductByName` (`Minimal.AppServices/
60
+ - **Specs are reused outside query handlers, too.** `SpecProductByName` (`<YourApp>.AppServices/
55
61
  AutomatedSample/V1/Specs/SpecProductByName.cs`) exists purely so `CreateProductRequestValidator` can
56
62
  check for a duplicate name before create — a spec is not only a query-handler concern.
57
63
  - **Unit-test a spec without EF Core or a database**: compile `.FilterQuery!.Compile()` into a plain
58
64
  `Func<TEntity, bool>` and assert against in-memory instances (`SpecGetPurchaseOrderTests.cs` — see
59
65
  [Testing pointers](#5-testing-pointers)).
60
66
 
61
- ## 2. Hand-written queries (`mode=manual`)
67
+ ## 2. Hand-written queries (`mode=manual`) — the fallback
68
+
69
+ Reach for this only after §3's generic list route has been ruled out for the shape you need.
62
70
 
63
71
  A query is a record implementing one of two `SlimBus.Extensions.Fluents.Queries` interfaces,
64
72
  dispatched from the endpoint via `IMessageBus.Send(...)` exactly like a command.
@@ -125,7 +133,7 @@ internal sealed class ListPurchaseOrdersQueryHandler(IRepositorySpec repository,
125
133
  }
126
134
  ```
127
135
 
128
- (`Minimal.AppServices/ManualSample/V1/Queries/GetPurchaseOrderById.cs`,
136
+ (`<YourApp>.AppServices/ManualSample/V1/Queries/GetPurchaseOrderById.cs`,
129
137
  `ListPurchaseOrders.cs`.)
130
138
 
131
139
  - **Nullable paging parameters + `[AsParameters]` + a co-located validator with `.When(x =>
@@ -142,7 +150,7 @@ internal sealed class ListPurchaseOrdersQueryHandler(IRepositorySpec repository,
142
150
  ### Aggregate/summary queries over `repository.Query(spec)`
143
151
 
144
152
  Not every read is "one row" or "a page of rows". `ProductPriceSummaryQuery`
145
- (`Minimal.AppServices/AutomatedSample/V1/Queries/ProductPriceSummary.cs`) computes a count and an
153
+ (`<YourApp>.AppServices/AutomatedSample/V1/Queries/ProductPriceSummary.cs`) computes a count and an
146
154
  average directly against the queryable a spec produces:
147
155
 
148
156
  ```csharp
@@ -246,7 +254,7 @@ inherit a latent failure.
246
254
 
247
255
  ### Behavioral spec: `ProductList.feature`
248
256
 
249
- `Minimal.App.BDDTests/Features/Products/ProductList.feature` is the regression fence for this whole
257
+ `<YourApp>.App.BDDTests/Features/Products/ProductList.feature` is the regression fence for this whole
250
258
  contract — it exists specifically because nothing in the slice is hand-written, so a future package
251
259
  bump could silently change behavior. Representative scenarios:
252
260
 
@@ -265,9 +273,9 @@ bump could silently change behavior. Representative scenarios:
265
273
  ## 4. Status counts
266
274
 
267
275
  `group.MapGetStatusCounts<TEntity>("status", new StatusPropertyInfo(nameof(X.Status), typeof(XStatus)))`
268
- (`Minimal.Api/Configs/Endpoints/StatusCountsEndpointMapperExtensions.cs`) is template-local — not part
276
+ (`<YourApp>.Api/Configs/Endpoints/StatusCountsEndpointMapperExtensions.cs`) is template-local — not part
269
277
  of the published `DKNet.AspCore.Extensions` package. It groups rows by an enum-backed property using
270
- `ModelSpecStatusCounts<TEntity>` (`Minimal.AppServices/Share/Generics/ModelSpecGenericStatusCounts.cs`):
278
+ `ModelSpecStatusCounts<TEntity>` (`<YourApp>.AppServices/Share/Generics/ModelSpecGenericStatusCounts.cs`):
271
279
 
272
280
  ```csharp
273
281
  public class ModelSpecStatusCounts<TEntity> : Specification<TEntity> where TEntity : DomainEntity
@@ -306,11 +314,11 @@ this today; wire it into a `Map(RouteGroupBuilder)` like any other route (see th
306
314
 
307
315
  - **Spec unit tests** — compile the spec's `.FilterQuery!.Compile()` and assert against
308
316
  hand-constructed entities, no EF Core or database involved
309
- (`Minimal.App.Tests/Unit/ManualSample/SpecGetPurchaseOrderTests.cs`): a "no filter matches
317
+ (`<YourApp>.App.Tests/Unit/ManualSample/SpecGetPurchaseOrderTests.cs`): a "no filter matches
310
318
  everything" case is the WHERE-FALSE regression guard, plus one case per optional filter argument and
311
319
  one for combining them.
312
320
  - **List/paging integration tests** — exercise the real HTTP route against `ApiFixture`
313
- (`Minimal.App.Tests/Integration/ManualSample/V1/PurchaseOrderListPagingTests.cs`): the shape to copy
321
+ (`<YourApp>.App.Tests/Integration/ManualSample/V1/PurchaseOrderListPagingTests.cs`): the shape to copy
314
322
  is asserting the *declared default* is served when a nullable paging parameter is omitted (not just
315
323
  "200 OK"), and asserting an out-of-range value produces the same `ValidationProblemDetails` shape as
316
324
  every other validation failure, not a `500`.
@@ -81,8 +81,9 @@ re-running the template; change them later by editing `Directory.Packages.props`
81
81
  ```
82
82
 
83
83
  **Path convention:** every other skill in this plugin writes paths relative to the solution root
84
- using the template's own placeholder names (`Minimal.Api`, `ApiEndpoints/Minimal.Domains/...`) —
85
- substitute your own `<YourApp>.*` prefix mentally when working in a generated solution.
84
+ with the placeholder `<YourApp>` standing in for your solution prefix (`<YourApp>.Api`,
85
+ `ApiEndpoints/<YourApp>.Domains/...`). Substitute the name you passed to `-n` — a skill never
86
+ hard-codes `Minimal.*`, because that prefix exists only in the template's own source tree.
86
87
 
87
88
  **No `.claude/`, `.github/`, or `docs/` reaches a generated solution.** Only `AGENTS.md`, the four
88
89
  solution-level files above, and `ApiEndpoints/**` are packed (`DKNet.Minimal.Template.nuspec`) —
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: dknet-unit-tests
3
- description: Write xUnit + Shouldly tests for a DKNet.Templates feature in Minimal.App.Tests — architecture/convention rules, pure functional tests (entity methods, validators, specs, mapping), and result-level integration tests against ApiFixture + IMessageBus (handler Result failures, EF persistence, domain events). Use after AppServices actions and endpoint config are ready. Covers only what xUnit owns; HTTP request/response and event-log scenarios belong in the `dknet-bdd-tests` skill. Invoke as `/dknet-unit-tests <Feature> <Entity> [mode=manual|auto]` to scaffold it for a feature.
3
+ description: Write xUnit + Shouldly tests for a DKNet.Templates feature in <YourApp>.App.Tests — architecture/convention rules, pure functional tests (entity methods, validators, specs, mapping), and result-level integration tests against ApiFixture + IMessageBus (handler Result failures, EF persistence, domain events). Use after AppServices actions and endpoint config are ready. Covers only what xUnit owns; HTTP request/response and event-log scenarios belong in the `dknet-bdd-tests` skill. Invoke as `/dknet-unit-tests <Feature> <Entity> [mode=manual|auto]` to scaffold it for a feature.
4
4
  metadata:
5
5
  kind: workflow
6
6
  arguments: "<Feature> <Entity> [mode=manual|auto]"
@@ -9,11 +9,11 @@ allowed-tools: Read, Grep, Glob, Edit, Write, Bash, Agent
9
9
 
10
10
  Usage: `/dknet-unit-tests <Feature> <Entity> [mode=manual|auto]`
11
11
 
12
- # xUnit tests (Minimal.App.Tests)
12
+ # xUnit tests (<YourApp>.App.Tests)
13
13
 
14
14
  ## Both shipped suites are teaching material — business tests only
15
15
 
16
- `Minimal.App.Tests` and `Minimal.App.BDDTests` ship inside every generated solution. A team reads them
16
+ `<YourApp>.App.Tests` and `<YourApp>.App.BDDTests` ship inside every generated solution. A team reads them
17
17
  to learn how tests are written here, so every test must be about the business domain: the `PurchaseOrder`
18
18
  (manual) and `Product` (automated) samples — entity invariants, validators, specs, handler results, CRUD
19
19
  over HTTP, domain events.
@@ -26,6 +26,10 @@ sample's business rules, stop and delete the test.
26
26
 
27
27
  ## Test layering — where a test belongs
28
28
 
29
+ **Business behavior goes to BDD first.** If a rule can be stated as "a caller does X and gets Y", it
30
+ belongs in `<YourApp>.App.BDDTests` as a scenario — that is the readable, business-facing record of
31
+ the rule. xUnit is for what a scenario cannot express or cannot distinguish.
32
+
29
33
  xUnit owns three things; BDD must not re-cover them:
30
34
 
31
35
  1. **Architecture/convention** — `Architecture/*`: NetArchTest + reflection over the compiled assembly
@@ -33,20 +37,21 @@ xUnit owns three things; BDD must not re-cover them:
33
37
  expose a domain entity type). Cannot be expressed as an HTTP scenario; never port to BDD.
34
38
  2. **Pure functional** — `Unit/*`: entity methods, validators, spec predicate filters, static data. No
35
39
  host, no DB, no HTTP.
36
- 3. **Result-level integration** — `Integration/<Feature>/V1/*`: handler failures asserted on the
37
- `IResult`/`IResultBase` object (not-found, missing acting user, "already cancelled", 409 precondition
38
- codes) and EF model/schema shape. BDD's HTTP-status/response-body assertions are coarser and would lose
39
- this intent — keep the `Result`-level assertion here even though the same rule is also proven over HTTP
40
- in BDD.
40
+ 3. **Result-level integration** — `Integration/<Feature>/V1/*`: EF model/schema shape, and a handler
41
+ failure asserted on the `IResult`/`IResultBase` object **only when the HTTP response cannot tell it
42
+ apart from another failure**. Two rules that both answer `400`, or a `NotFoundError` versus an
43
+ ownership filter that also returns `404`, need the `Result`-level assertion to pin *which* rule
44
+ fired. A failure whose status code and message already identify it uniquely does not — write that
45
+ one as a BDD scenario and leave it out of here.
41
46
 
42
47
  BDD owns user-facing HTTP behavior (request → status → response body) and domain-event side effects
43
- observed via log capture. Do not duplicate the same behavior in both suites — an HTTP-status check for a
44
- rule already covered here belongs in BDD instead, not copied into both.
48
+ observed via log capture — which is most of a feature's business rules. Never assert the same rule in
49
+ both suites: pick the suite that states it best, and delete the other copy.
45
50
 
46
- ## Fixtures (`Minimal.App.TestSupport` + `Integration/Support`)
51
+ ## Fixtures (`<YourApp>.App.TestSupport` + `Integration/Support`)
47
52
 
48
- `TestApiFactoryBase(string? dbName) : WebApplicationFactory<Minimal.Api.Program>` (in
49
- `Minimal.App.TestSupport`) is the shared host substitution both xUnit and BDD build on. It:
53
+ `TestApiFactoryBase(string? dbName) : WebApplicationFactory<<YourApp>.Api.Program>` (in
54
+ `<YourApp>.App.TestSupport`) is the shared host substitution both xUnit and BDD build on. It:
50
55
 
51
56
  - Sets `FeatureManagement:RunDbMigrationWhenAppStart/EnableSwagger/EnableAzureAppConfig = false` and
52
57
  `ConnectionStrings:AppDb = UseInMemory`.
@@ -143,7 +148,7 @@ public sealed class PurchaseOrderActionsIntegrationTests(ApiFixture fixture) : I
143
148
 
144
149
  The same class also proves a guarded state transition by calling the handler twice — `Cancel` once
145
150
  succeeds, a second `Cancel` on the same order fails with the `precondition.purchase-order-already-cancelled`
146
- message (`PreconditionCodes`, `Minimal.AppServices/Share/PreconditionCodes.cs`), and every mutating action
151
+ message (`PreconditionCodes`, `<YourApp>.AppServices/Share/PreconditionCodes.cs`), and every mutating action
147
152
  fails when `ByUser` is left empty (the acting-user rule the manual mode enforces in the handler itself).
148
153
 
149
154
  **Precondition branch reachable only over HTTP from xUnit**
@@ -245,7 +250,7 @@ orders.ShouldAllBe(o => o.CreatedBy == SharedConsts.SystemAccount);
245
250
  ```
246
251
 
247
252
  **Architecture rule** (`Architecture/AppServiceTests.cs`), using NetArchTest against the compiled
248
- `Minimal.AppServices` assembly:
253
+ `<YourApp>.AppServices` assembly:
249
254
 
250
255
  ```csharp
251
256
  var result = Types.InAssembly(typeof(AppSetup).Assembly)
@@ -268,11 +273,11 @@ result.IsSuccessful.ShouldBeTrue();
268
273
  violation on a generated CRUD route**; it returns 201/200 (`DKNet.AspCore.Extensions`'s generic
269
274
  `Map*<TRequest,TDto>` wrapper isn't visible to the validation source generator). A FluentValidation
270
275
  validator still runs on every route. Verify a declared event's composed name against the compiled
271
- assembly before asserting on it — `strings bin/**/Minimal.Domains.dll | grep <Entity>`.
276
+ assembly before asserting on it — `strings bin/**/<YourApp>.Domains.dll | grep <Entity>`.
272
277
 
273
278
  ## Conventions
274
279
 
275
- - xUnit + Shouldly (`result.IsSuccess.ShouldBeTrue()`, not `Assert.True`). `Minimal.App.Tests.csproj`
280
+ - xUnit + Shouldly (`result.IsSuccess.ShouldBeTrue()`, not `Assert.True`). `<YourApp>.App.Tests.csproj`
276
281
  disables analyzers and warnings-as-errors — production code style rules do not apply here.
277
282
  - Implicit usings from `GlobalUsings.cs`: `AutoBogus`, `Shouldly`, `System.Text.Json`, `MapsterMapper`,
278
283
  plus csproj-level `System.Net`, `Microsoft.Extensions.DependencyInjection`, `Xunit`. Still add explicit
@@ -287,11 +292,11 @@ result.IsSuccessful.ShouldBeTrue();
287
292
  ## Commands
288
293
 
289
294
  ```bash
290
- dotnet test ApiEndpoints/Minimal.App.Tests/Minimal.App.Tests.csproj --filter "FullyQualifiedName~PurchaseOrder"
295
+ dotnet test ApiEndpoints/<YourApp>.App.Tests/<YourApp>.App.Tests.csproj --filter "FullyQualifiedName~PurchaseOrder"
291
296
  dotnet test --settings coverage.runsettings --collect:"XPlat Code Coverage"
292
297
  ```
293
298
 
294
- `coverage.runsettings` includes `[DKNet*]*` and `[Minimal*]*`, excludes `*.Tests`/`*Tests` assemblies and
299
+ `coverage.runsettings` includes `[DKNet*]*` and `[<YourApp>*]*`, excludes `*.Tests`/`*Tests` assemblies and
295
300
  `**/bin/**, **/obj/**, **/*Tests.cs, **/GlobalUsings.cs, **/*.g.cs` by file — don't put real logic in an
296
301
  excluded path expecting it to be measured.
297
302
 
@@ -333,7 +338,7 @@ excluded path expecting it to be measured.
333
338
 
334
339
  The procedure an agent follows when invoked with arguments. The reference sections above are the rules it applies.
335
340
 
336
- You are adding integration tests in `Minimal.App.Tests` that exercise the AppServices and Domains layers through the real DI container.
341
+ You are adding integration tests in `<YourApp>.App.Tests` that exercise the AppServices and Domains layers through the real DI container.
337
342
 
338
343
  ### Inputs
339
344
 
@@ -343,7 +348,7 @@ You are adding integration tests in `Minimal.App.Tests` that exercise the AppSer
343
348
  ### Required reading
344
349
 
345
350
  1. The reference sections above
346
- 2. `ApiEndpoints/Minimal.App.Tests/` — existing fixtures and test patterns (`Architecture/`, `Integration/`, `Unit/`).
351
+ 2. `ApiEndpoints/<YourApp>.App.Tests/` — existing fixtures and test patterns (`Architecture/`, `Integration/`, `Unit/`).
347
352
 
348
353
  ### Steps
349
354
 
@@ -370,7 +375,7 @@ You are adding integration tests in `Minimal.App.Tests` that exercise the AppSer
370
375
  the gap is correct. Note the gap in the report instead.
371
376
  2. Run only the affected tests:
372
377
  ```
373
- dotnet test ApiEndpoints/Minimal.App.Tests/Minimal.App.Tests.csproj --filter "FullyQualifiedName~<Entity>"
378
+ dotnet test ApiEndpoints/<YourApp>.App.Tests/<YourApp>.App.Tests.csproj --filter "FullyQualifiedName~<Entity>"
374
379
  ```
375
380
  3. If any test fails, fix the test or product code (per skill guidance) — do not relax assertions.
376
381
  4. Report: test file path, count, pass/fail, coverage areas hit.
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dknet-minimal",
3
3
  "description": "Agent skills and agents for building vertical-slice DDD/CQRS features on solutions generated from DKNet.Minimal.Template (.NET 10, EF Core, .NET Aspire, SlimMessageBus, FluentValidation, Mapster, Reqnroll BDD).",
4
- "version": "14.1.0",
4
+ "version": "14.2.0",
5
5
  "author": {
6
6
  "name": "baoduy"
7
7
  },
@@ -19,8 +19,8 @@
19
19
  "slimmessagebus",
20
20
  "fluentvalidation"
21
21
  ],
22
- "agents": "agents/",
22
+ "agents": "plugin/agents/",
23
23
  "skills": [
24
- "skills/"
24
+ "plugin/skills/"
25
25
  ]
26
26
  }