@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
|
@@ -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
|
-
|
|
14
|
+
<YourApp>.Api → entry point, endpoints, auth, OpenAPI
|
|
15
15
|
↓
|
|
16
|
-
|
|
16
|
+
<YourApp>.AppServices → CQRS handlers, validators, DTOs, domain event handlers
|
|
17
17
|
↓
|
|
18
|
-
|
|
18
|
+
<YourApp>.Domains → entities, aggregate roots, domain service contracts
|
|
19
19
|
↑
|
|
20
|
-
|
|
20
|
+
<YourApp>.Infra → EF Core (CoreDbContext), repos, event publisher, service bus
|
|
21
21
|
(wires into Api via InfraSetup.AddInfraServices)
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
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:
|
|
28
|
-
|
|
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
|
-
|
|
|
37
|
-
|
|
|
38
|
-
|
|
|
39
|
-
|
|
|
40
|
-
|
|
|
41
|
-
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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 |
|
|
55
|
-
| Infra |
|
|
56
|
-
| AppServices |
|
|
57
|
-
| Api |
|
|
58
|
-
| Tests |
|
|
59
|
-
| BDD |
|
|
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
|
|
69
|
-
| Command/query request+handler | `Fluents.Requests.*`/`Fluents.Queries.*` | `AutoDeclareFrom`/`AddServicesFromAssembly` on the in-memory bus, scanning
|
|
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
|
|
74
|
-
| DTO mapping | `[MapsFrom(typeof(Entity))]` or `[GenerateDto(typeof(Entity))]` | Mapster `ScanMaps()` in
|
|
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
|
|
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
|
|
96
|
-
dotnet run --project ApiEndpoints
|
|
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
|
|
100
|
-
dotnet ef migrations remove -c CoreDbContext -p
|
|
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 —
|
|
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,
|
|
9
|
-
|
|
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
|
-
(
|
|
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` (
|
|
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
|
-
(
|
|
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
|
-
(
|
|
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
|
-
|
|
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
|
-
(
|
|
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>` (
|
|
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
|
-
(
|
|
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
|
-
(
|
|
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
|
-
|
|
85
|
-
|
|
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
|
|
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 (
|
|
12
|
+
# xUnit tests (<YourApp>.App.Tests)
|
|
13
13
|
|
|
14
14
|
## Both shipped suites are teaching material — business tests only
|
|
15
15
|
|
|
16
|
-
|
|
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/*`:
|
|
37
|
-
`IResult`/`IResultBase` object
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
44
|
-
|
|
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 (
|
|
51
|
+
## Fixtures (`<YourApp>.App.TestSupport` + `Integration/Support`)
|
|
47
52
|
|
|
48
|
-
`TestApiFactoryBase(string? dbName) : WebApplicationFactory
|
|
49
|
-
|
|
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`,
|
|
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
|
-
|
|
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
|
|
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`).
|
|
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
|
|
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 `[
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
}
|
|
File without changes
|