@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.
- 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 +34 -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 +70 -43
- package/{skills → plugin/skills}/dknet-entity/SKILL.md +43 -33
- package/{skills → plugin/skills}/dknet-feature/SKILL.md +34 -13
- package/{skills → plugin/skills}/dknet-feature-lifecycle/SKILL.md +48 -42
- package/{skills → plugin/skills}/dknet-feature-remove/SKILL.md +15 -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
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"plugins": [
|
|
13
13
|
{
|
|
14
14
|
"name": "dknet-minimal",
|
|
15
|
-
"source": "
|
|
15
|
+
"source": "./plugin",
|
|
16
16
|
"description": "Skills (Claude Code plugin + Agent Skills standard) and subagents for building vertical-slice features on the DKNet.Minimal.Template: endpoints, actions, queries, validation, DTO mapping, entities, seeding, SlimMessageBus/Azure Service Bus events, auth/ownership, platform flags, tests, and end-to-end /dknet-feature workflows.",
|
|
17
17
|
"category": "development",
|
|
18
18
|
"homepage": "https://github.com/baoduy/DKNet.Templates",
|
package/README.md
CHANGED
|
@@ -59,8 +59,8 @@ Every scaffold parameter, plus the run/test/migrate/pack commands:
|
|
|
59
59
|
|
|
60
60
|
## The shape your endpoints should take
|
|
61
61
|
|
|
62
|
-
**Composite-first.** One endpoint class per aggregate:
|
|
63
|
-
|
|
62
|
+
**Composite-first.** One endpoint class per aggregate: declare the group's authorization scopes on the
|
|
63
|
+
class, map the generated CRUD composite at the top, then hand-write below it only the routes that
|
|
64
64
|
carry real orchestration. Generated and hand-written routes live in the same group — this is not a
|
|
65
65
|
choice between two styles of endpoint.
|
|
66
66
|
|
|
@@ -68,29 +68,38 @@ choice between two styles of endpoint.
|
|
|
68
68
|
trimmed here to its structure:
|
|
69
69
|
|
|
70
70
|
```csharp
|
|
71
|
-
|
|
71
|
+
[EndpointGroupScope(ProductScopes.Read, EndpointHttpMethods.Get)]
|
|
72
|
+
[EndpointGroupScope(ProductScopes.Write, EndpointHttpMethods.Post, EndpointHttpMethods.Put,
|
|
73
|
+
EndpointHttpMethods.Delete)]
|
|
74
|
+
internal sealed class ProductV1Endpoint : IEndpointConfig
|
|
72
75
|
{
|
|
73
|
-
|
|
76
|
+
public void Map(RouteGroupBuilder group)
|
|
74
77
|
{
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
78
|
+
group.MapProductCrud(o =>
|
|
79
|
+
{
|
|
80
|
+
o.Exclude("Discontinue"); // hand-written below
|
|
81
|
+
|
|
82
|
+
// a PUT like Update, so the per-method declaration cannot separate them — holding
|
|
83
|
+
// products.write must not be enough to assign a supplier reference
|
|
84
|
+
if (requireAuthorization)
|
|
85
|
+
o.Configure("AssignSupplierReference", rb => rb.RequireAuthorization(ProductScopes.Supplier));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// writes two aggregates in one transaction — outside what the generator can express
|
|
89
|
+
group.MapPut("{id:guid}/discontinue", /* ... */).Produces<ProductDto>();
|
|
90
|
+
|
|
91
|
+
// a response shape the generator has none for; the group's GET declaration already covers it
|
|
92
|
+
group.MapGet("summary", /* ... */).Produces<ProductPriceSummaryDto>();
|
|
93
|
+
}
|
|
89
94
|
}
|
|
90
95
|
```
|
|
91
96
|
|
|
92
|
-
Seven of the nine routes come from `MapProductCrud`.
|
|
93
|
-
|
|
97
|
+
Seven of the nine routes come from `MapProductCrud`. `[EndpointGroupScope]` names the scope per HTTP
|
|
98
|
+
method once and covers generated and hand-mapped routes alike; a per-route `Configure`/
|
|
99
|
+
`RequireAuthorization` is needed only where two routes share an HTTP method but not a scope. Scopes
|
|
100
|
+
are applied only when `FeatureManagement:RequireAuthorization` is on, so a stock Development run
|
|
101
|
+
enforces none of them — the attribute checks that flag itself, which is why the per-route overrides
|
|
102
|
+
are the only calls that need an explicit guard.
|
|
94
103
|
|
|
95
104
|
### When an operation needs a hand-written route
|
|
96
105
|
|
|
@@ -160,15 +169,17 @@ capability-to-attribute tables ("I want X; which attribute or call gives it to m
|
|
|
160
169
|
|
|
161
170
|
## AI Plugin — Claude Code, GitHub Copilot, and any agent that reads SKILL.md
|
|
162
171
|
|
|
163
|
-
|
|
164
|
-
`agents/` (Claude Code)
|
|
172
|
+
The `dknet-minimal` plugin lives in [`plugin/`](plugin): `plugin/.claude-plugin/plugin.json` +
|
|
173
|
+
`plugin/skills/` + `plugin/agents/` (Claude Code). The repository root keeps only the entry points
|
|
174
|
+
that have to be found there — `.claude-plugin/marketplace.json` (pointing at `./plugin`),
|
|
175
|
+
`plugin.json` (GitHub Copilot) and `package.json` (npm). One set of
|
|
165
176
|
[Agent Skills](https://agentskills.io) teaches an AI coding agent how to build on this template: the
|
|
166
177
|
layer boundaries, every endpoint shape (hand-mapped, generated `Map<Entity>Crud()`, or the generic
|
|
167
178
|
route helpers), actions and queries, FluentValidation + Mapster DTOs, domain entities and static
|
|
168
179
|
seeding, SlimMessageBus internal events and Azure Service Bus forwarding, auth/ownership and every
|
|
169
180
|
`FeatureManagement` flag. Eight of the skills are slash workflows that scaffold a vertical slice end to
|
|
170
181
|
end; three Claude Code subagents (`dknet-architect`, `dknet-implementer`, `dknet-bdd-engineer`) back
|
|
171
|
-
`/dknet-feature`. Index of every skill: [`skills/README.md`](skills/README.md).
|
|
182
|
+
`/dknet-feature`. Index of every skill: [`plugin/skills/README.md`](plugin/skills/README.md).
|
|
172
183
|
|
|
173
184
|
`dotnet new dknet-minimal` does **not** copy the plugin into a generated solution (only `AGENTS.md`
|
|
174
185
|
ships) — install it into the generated repo with one of the channels below.
|
|
@@ -200,14 +211,14 @@ npx skills add baoduy/DKNet.Templates -s '*' -y # everything, no prompts
|
|
|
200
211
|
|
|
201
212
|
```bash
|
|
202
213
|
npm i -D @drunkcoding/dknet-implementation-skills
|
|
203
|
-
claude --plugin-dir node_modules/@drunkcoding/dknet-implementation-skills # Claude Code
|
|
214
|
+
claude --plugin-dir node_modules/@drunkcoding/dknet-implementation-skills/plugin # Claude Code
|
|
204
215
|
npx skills experimental_sync -a '*' # any agent: node_modules -> .agents/skills/ etc.
|
|
205
216
|
```
|
|
206
217
|
|
|
207
218
|
**Working on this repository**
|
|
208
219
|
|
|
209
220
|
```bash
|
|
210
|
-
claude --plugin-dir
|
|
221
|
+
claude --plugin-dir plugin # loads plugin/skills/ and plugin/agents/ from the checkout
|
|
211
222
|
./validate-plugin.sh # manifests, README install channels, skill portability
|
|
212
223
|
```
|
|
213
224
|
|
|
@@ -243,7 +254,7 @@ claude --plugin-dir . # loads skills/ and agents/ from the checkout
|
|
|
243
254
|
|
|
244
255
|
**Release.** Versions stay `0.0.0` in git. `publish-nuget-github.yml` computes the release version from
|
|
245
256
|
tags on `main`, packs and publishes the NuGet template, creates the GitHub release, then stamps the same
|
|
246
|
-
version into `package.json` / `plugin.json` /
|
|
257
|
+
version into `package.json` / `plugin.json` / `plugin/.claude-plugin/plugin.json` (`npm version` →
|
|
247
258
|
`scripts/sync-version.mjs`), validates the skills (`agentskills validate`, `claude plugin validate`,
|
|
248
259
|
`npx skills add --list`, `./validate-plugin.sh`) and publishes `@drunkcoding/dknet-implementation-skills` to npm
|
|
249
260
|
with OIDC trusted publishing. A version already on npm is skipped.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drunkcoding/dknet-implementation-skills",
|
|
3
|
-
"version": "14.1.
|
|
3
|
+
"version": "14.1.1",
|
|
4
4
|
"description": "Agent skills (Claude Code plugin + Agent Skills standard) that teach coding agents how to build features on solutions generated from DKNet.Minimal.Template. Install with `npx skills add baoduy/DKNet.Templates`.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dknet",
|
|
@@ -36,8 +36,7 @@
|
|
|
36
36
|
},
|
|
37
37
|
"files": [
|
|
38
38
|
".claude-plugin/",
|
|
39
|
-
"
|
|
40
|
-
"agents/",
|
|
39
|
+
"plugin/",
|
|
41
40
|
"plugin.json",
|
|
42
41
|
"README.md",
|
|
43
42
|
"LICENSE"
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "dknet-minimal",
|
|
4
4
|
"displayName": "DKNet Minimal Template",
|
|
5
5
|
"description": "Skills and subagents that teach an agent how to build features on the DKNet.Minimal.Template (.NET 10, vertical-slice DDD/CQRS): endpoints (hand-mapped, generated CRUD, generic helpers), actions and queries, FluentValidation + Mapster DTOs, domain entities and static seeding, SlimMessageBus internal and Azure Service Bus events, auth/ownership and every platform flag — plus slash workflows that scaffold a slice end-to-end.",
|
|
6
|
-
"version": "14.1.
|
|
6
|
+
"version": "14.1.1",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "baoduy",
|
|
9
9
|
"email": "baoduy2412@gmail.com",
|
|
@@ -19,19 +19,19 @@ Always start by reading:
|
|
|
19
19
|
- the `dknet-ddd-principles` skill for aggregate boundary, entity-vs-value-object, invariant, and domain-event judgment calls — apply these when deciding what the new aggregate owns and what triggers an event.
|
|
20
20
|
- `CLAUDE.md` and `AGENTS.md` for current layer rules and conventions.
|
|
21
21
|
- The two existing exemplar slices, and the `dknet-feature-lifecycle` skill §1 for the layer-by-layer trade-off between them:
|
|
22
|
-
- Hand-written —
|
|
23
|
-
- Generator-driven —
|
|
22
|
+
- Hand-written — `<YourApp>.Domains/Features/ManualSample/Entities/PurchaseOrder.cs`, `<YourApp>.Infra/Features/ManualSample/`, `<YourApp>.AppServices/ManualSample/V1/`, `<YourApp>.Api/ApiEndpoints/ManualSample/PurchaseOrderV1Endpoint.cs`.
|
|
23
|
+
- Generator-driven — `<YourApp>.Domains/Features/AutomatedSample/Entities/Product.cs` (`[RaisesEvent]`/`[CrudCreate]`/`[CrudUpdate]`), `<YourApp>.AppServices/AutomatedSample/V1/ProductDto.cs` (`[GenerateDto]`), `<YourApp>.Api/ApiEndpoints/AutomatedSample/ProductV1Endpoint.cs`.
|
|
24
24
|
- The skill that matches the layer you're planning (the `dknet-entity` skill, `dknet-efcore-config`, `dknet-crud`, `dknet-endpoint`, `dknet-bdd-tests`, `dknet-unit-tests`).
|
|
25
25
|
|
|
26
26
|
## Output contract
|
|
27
27
|
|
|
28
28
|
Produce a single markdown plan with these sections, no more no less:
|
|
29
29
|
|
|
30
|
-
1. **Aggregates & owned types** — name, schema prefix for `DomainSchemas`, immutable vs. mutable fields, mutation methods, sequence usage. State explicitly which fields are entities vs. value objects and why (per `dknet-ddd-principles`), and what the aggregate's consistency boundary is. Also state up front which shape this feature should take: hand-written (`PurchaseOrder`-style
|
|
30
|
+
1. **Aggregates & owned types** — name, schema prefix for `DomainSchemas`, immutable vs. mutable fields, mutation methods, sequence usage. State explicitly which fields are entities vs. value objects and why (per `dknet-ddd-principles`), and what the aggregate's consistency boundary is. Also state up front which shape this feature should take. **Default to generator-driven** (`Product`-style: `[CrudCreate]`/`[CrudUpdate]`/`[CrudAction]`/`[RaisesEvent]` + `[GenerateDto]`). Propose hand-written (`PurchaseOrder`-style) only for idempotent writes, an operation that writes more than one aggregate in one transaction, a query beyond the generic list route's `filter`/`search`/`orderBy` contract, `[FromClaim]` acting-user attribution, or attribute-declared validation that must return `400`. A rule that merely refuses an operation is a FluentValidation validator on the generated request, and a DTO that hides fields is `[GenerateDto(..., Exclude = [...])]` — neither forces the hand-written shape. The `dknet-feature-lifecycle` skill §1 is the deciding reference.
|
|
31
31
|
2. **EF Core mapping** — table name, indexes, max lengths, column types, owned-type registrations, seed data. Both sample shapes hand-write this layer — no generator touches `IEntityTypeConfiguration<T>`.
|
|
32
32
|
3. **AppServices actions (V1)** — for the hand-written shape: for each of Create/Update/Delete, request shape, validator rules, duplicate spec, domain events emitted, lazy-mapping decision (mirror `PurchaseOrder`'s `Actions/Create.cs`/`Update.cs`/`Cancel.cs`/`Delete.cs`). For the generator-driven shape: which entity members carry `[CrudCreate]`/`[CrudUpdate]`/`[RaisesEvent]`, and the one-line `[GenerateDto(typeof(Entity))]` DTO — flag explicitly that generated-route validation is enforced only when the entity's endpoint uses literal `Map*(string, Delegate)` calls, not the generic `Map*<TRequest,TDto>` wrapper (see `Product`'s confirmed-live gap: `POST /v1/products` with a negative price returns `201`).
|
|
33
33
|
4. **Query specs** — `SpecGet<Entity>` constructor parameters; expected callers. N/A for the generator-driven shape (GetById/GetList map straight to `DKNet.AspCore.Extensions`'s generic `MapGetById`/`MapGetList` — no per-entity query object exists).
|
|
34
|
-
5. **Endpoint contract** — `IEndpointConfig` group path, version, mapping style (literal `group.MapPost/MapGet/MapPut/MapDelete` for hand-written, or the generated `Map<Entity>Crud()` extension for generator-driven), idempotency requirements (`.RequiredIdempotentKey()` for POST — required whenever the plan calls for idempotent writes; the generated CRUD route does not add this), auth
|
|
34
|
+
5. **Endpoint contract** — `IEndpointConfig` group path, version, mapping style (literal `group.MapPost/MapGet/MapPut/MapDelete` for hand-written, or the generated `Map<Entity>Crud()` extension for generator-driven), idempotency requirements (`.RequiredIdempotentKey()` for POST — required whenever the plan calls for idempotent writes; the generated CRUD route does not add this), auth decisions — name the scope per HTTP method so it can be declared with `[EndpointGroupScope]` on the endpoint class, and call out any route whose scope its HTTP method cannot decide, since that one needs a per-route `Configure`/`RequireAuthorization` behind the `FeatureOptions.RequireAuthorization` guard.
|
|
35
35
|
6. **Tests** — unit test coverage targets (happy path, validation, duplicates, not-found, events) and BDD scenarios (happy, business-rule failure, validation failure) with key contract assertions (status, response shape, key fields).
|
|
36
36
|
7. **Risks & open questions** — anything ambiguous; surface it here for the user to resolve before implementation begins.
|
|
37
37
|
8. **Hand-off checklist** — explicit list of slash commands the implementer should run in order.
|
|
@@ -11,17 +11,17 @@ You are the DKNet BDD Engineer. You write Reqnroll + NUnit acceptance tests that
|
|
|
11
11
|
|
|
12
12
|
1. the `dknet-bdd-tests` skill — the canonical pattern.
|
|
13
13
|
2. the `dknet-bdd-tests` skill's `checklist.md` — the completion gate.
|
|
14
|
-
3. `ApiEndpoints
|
|
14
|
+
3. `ApiEndpoints/<YourApp>.App.BDDTests/Support/BddApiFactory.cs` and `ApiHooks.cs` — fixture wiring you must not duplicate.
|
|
15
15
|
4. `specs/<feature>/contracts/*` (when present) — the source of truth for assertions.
|
|
16
16
|
5. `docs/features/<feature>/` (when present) — reference context for scenario wording.
|
|
17
17
|
|
|
18
18
|
## Scope (do not stray)
|
|
19
19
|
|
|
20
20
|
You may touch only:
|
|
21
|
-
- `ApiEndpoints
|
|
22
|
-
- `ApiEndpoints
|
|
23
|
-
- `ApiEndpoints
|
|
24
|
-
- `ApiEndpoints
|
|
21
|
+
- `ApiEndpoints/<YourApp>.App.BDDTests/Features/**/*.feature`
|
|
22
|
+
- `ApiEndpoints/<YourApp>.App.BDDTests/Features/**/Steps/*.cs`
|
|
23
|
+
- `ApiEndpoints/<YourApp>.App.BDDTests/Support/*.cs` (only when adding shared step infrastructure)
|
|
24
|
+
- `ApiEndpoints/<YourApp>.App.BDDTests/<YourApp>.App.BDDTests.csproj` (only when adding a NuGet/project ref through central package management)
|
|
25
25
|
|
|
26
26
|
If a test reveals a product bug, REPORT it — do not modify domain/AppServices/Api code.
|
|
27
27
|
|
|
@@ -49,7 +49,7 @@ For every API behavior, produce at minimum:
|
|
|
49
49
|
|
|
50
50
|
After edits:
|
|
51
51
|
1. `dotnet build -c Release`
|
|
52
|
-
2. `dotnet test ApiEndpoints
|
|
52
|
+
2. `dotnet test ApiEndpoints/<YourApp>.App.BDDTests/<YourApp>.App.BDDTests.csproj`
|
|
53
53
|
3. Report scenario count, pass/fail, any undefined or pending steps, and any contract gap that the spec did not cover.
|
|
54
54
|
|
|
55
55
|
## Output
|
|
@@ -24,30 +24,30 @@ Read these in order, every time:
|
|
|
24
24
|
- the `dknet-crud` skill
|
|
25
25
|
- the `dknet-endpoint` skill
|
|
26
26
|
5. The exemplar slice for any layer where you're unsure — this template ships two, and the `dknet-feature-lifecycle` skill §1 is the authoritative layer-by-layer comparison between them:
|
|
27
|
-
- **
|
|
28
|
-
- **
|
|
27
|
+
- **Generator-driven (the default — see "Declarative path" below)** — `<YourApp>.Domains/Features/AutomatedSample/Entities/Product.cs`, `<YourApp>.AppServices/AutomatedSample/V1/ProductDto.cs`, `<YourApp>.Api/ApiEndpoints/AutomatedSample/ProductV1Endpoint.cs`.
|
|
28
|
+
- **Hand-written (fallback, walkthrough below)** — `<YourApp>.Domains/Features/ManualSample/Entities/PurchaseOrder.cs`, `<YourApp>.Infra/Features/ManualSample/Mappers/`, `<YourApp>.AppServices/ManualSample/V1/Actions/`, `Specs/`, `Queries/`, `Events/`, `<YourApp>.Api/ApiEndpoints/ManualSample/PurchaseOrderV1Endpoint.cs`.
|
|
29
29
|
|
|
30
30
|
## Execution order (do not skip, do not reorder)
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
**Start from the declarative path below; this hand-written walkthrough is the fallback.** Follow it only when the plan calls for idempotent writes, an operation that writes more than one aggregate in one transaction, a query beyond the generic list route's `filter`/`search`/`orderBy` contract, `[FromClaim]` acting-user attribution, or attribute-declared validation that must return `400`. Neither a rule that conditionally refuses an operation nor a DTO that hides fields is on that list: the first is a FluentValidation validator written against the *generated* request, which runs on the generated route through the group-level `AddFluentValidationAutoValidation()` filter (how `Product` refuses a duplicate name and a delete of a product still for sale); the second is `[GenerateDto(..., Exclude = [...])]`. Steps 1–4 are the same either way — for the declarative path, do them and then skip to "Declarative path" instead of steps 5–6.
|
|
33
33
|
|
|
34
34
|
1. **Domain** — entity (`AggregateRoot`/`DomainEntity`), owned types, `DomainSchemas` constant, sequence name (if used), domain service interface (if needed). `PurchaseOrder` raises its own creation event by calling `AddEvent(new PurchaseOrderCreatedEvent(...))` directly inside the constructor — no attribute involved.
|
|
35
35
|
2. **Infra mapper** — `internal sealed : DefaultEntityTypeConfiguration<T>`, `base.Configure(builder)` first, indexes, lengths, `ToTable("...", DomainSchemas.X)` (see `PurchaseOrderConfigs`).
|
|
36
|
-
3. **Infra services / static seed data** — services `internal sealed` under
|
|
37
|
-
4. **EF migration** — `cd ApiEndpoints && dotnet ef migrations add <Name> -c CoreDbContext -p
|
|
36
|
+
3. **Infra services / static seed data** — services `internal sealed` under `<YourApp>.Infra/Services/` and registered explicitly in `InfraSetup.AddInfraServices` (no convention scan exists); seeders `internal sealed : DataSeedingConfiguration<T>` under `Features/<X>/StaticData/` so auto-seeding picks them up (see `PurchaseOrderStaticData`). Wire `.UseAutoDataSeeding(...)` into **both** `InfraSetup.AddInfraServices` and `InfraMigration.MigrateDb` — seeding only from one of the two paths means seed rows silently never appear over HTTP (a real bug this template hit once).
|
|
37
|
+
4. **EF migration** — `cd ApiEndpoints && dotnet ef migrations add <Name> -c CoreDbContext -p <YourApp>.Infra/<YourApp>.Infra.csproj`. Inspect the generated migration before continuing.
|
|
38
38
|
5. **AppServices** — hand-written DTO record (no `[GenerateDto]`; see `PurchaseOrderDto` — exposes exactly the fields you write into it), `Create*Request` / `Update*Request` / `Delete*Request` (`Fluents.Requests.IWitResponse<TDto>` or `INoResponse`, `[FromClaim(ClaimTypes.Name)] ByUser` for the acting user — never trust a payload value for it), `AbstractValidator`, `internal sealed` handlers using `IRepositorySpec` + `IMapper`, `SpecGet<Entity>`, domain event record + handler.
|
|
39
39
|
6. **Api endpoint** — new `*V1Endpoint : IEndpointConfig`; map every route with literal `group.MapPost/MapGet/MapPut/MapDelete(...)` calls against the raw minimal-API surface (see `PurchaseOrderV1Endpoint`). Add `.RequiredIdempotentKey()` to the POST chain — clients then send `X-Idempotency-Key: {Guid}`; a replayed key returns the original response instead of creating a duplicate.
|
|
40
40
|
7. **Tests** — invoke `/dknet-unit-tests` and `/dknet-bdd-tests` (or follow the corresponding skills directly). Don't claim done until both pass.
|
|
41
41
|
|
|
42
|
-
## Declarative
|
|
42
|
+
## Declarative path — the default (`Product`)
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
Skip steps 5–6's AppServices/Api work almost entirely (a business rule is still allowed here — write it as a FluentValidation validator against the generated request):
|
|
45
45
|
|
|
46
|
-
- `[RaisesEvent(EventOperations.Created, Include=[...])]` / `[RaisesEvent(EventOperations.Updated, nameof(Prop))]` at the class level instead of a hand-written event + `AddEvent(...)` call. Naming composes as `<Entity><NarrowingProps><Operation>Event` — e.g. `[RaisesEvent(EventOperations.Updated, nameof(Price))]` on `Product` generates `ProductPriceUpdatedEvent`, not `ProductUpdatedEvent`. Verify the composed name against the compiled assembly before wiring a consumer to it.
|
|
47
|
-
- `[CrudCreate]` on the constructor and `[CrudUpdate]` on a mutation method — `DKNet.SlimBus.Generators` then generates the request record, handler, and route registration for you (namespace
|
|
46
|
+
- `[RaisesEvent(EventOperations.Created, Include=[...])]` / `[RaisesEvent(EventOperations.Updated, nameof(Prop))]` at the class level instead of a hand-written event + `AddEvent(...)` call. Where a raise needs a real condition that `[RaisesEvent]` cannot express, use `AddEvent<TEvent>()` and let `IMapper` project the payload; hand-build one with `AddEvent(new …)` only when the payload is not a projection of the entity. Naming composes as `<Entity><NarrowingProps><Operation>Event` — e.g. `[RaisesEvent(EventOperations.Updated, nameof(Price))]` on `Product` generates `ProductPriceUpdatedEvent`, not `ProductUpdatedEvent`. Verify the composed name against the compiled assembly before wiring a consumer to it.
|
|
47
|
+
- `[CrudCreate]` on the constructor and `[CrudUpdate]` on a mutation method — `DKNet.SlimBus.Generators` then generates the request record, handler, and route registration for you (namespace `<YourApp>.AppServices.Crud`, not committed — inspect `obj/Generated/DKNet.SlimBus.Generators/` after a build).
|
|
48
48
|
- `[GenerateDto(typeof(Entity))] public sealed partial record <Entity>Dto;` — one line — instead of a hand-written DTO. Generates every audited property by default; use `Exclude`/`Include` to narrow.
|
|
49
|
-
- The endpoint becomes one `group.Map<Entity>Crud(o => …)` call instead of five hand-written `Map*` calls, with any route the generator cannot express excluded by name and hand-mapped below it (see `ProductV1Endpoint`).
|
|
50
|
-
- **Validation-gap caveat — do not skip this:** a `[Range]`/`[Required]` on a `[CrudCreate]`/`[CrudUpdate]` parameter *is* forwarded onto the generated request property, but it is **never enforced** under this template's endpoint-registration convention — the .NET 10 validation source generator only sees literal `Map*(string, Delegate)` calls, and the generated CRUD route goes through `DKNet.AspCore.Extensions`'s generic `MapPost<TRequest,TDto>` wrapper instead. Confirmed live: `POST /v1/products` with a negative price returns `201`, not `400`. Pick this path only when that gap is acceptable, or when you plan to enforce the rule some other way. Also: `[FromClaim]` can never reach a generated request (the generator forwards only DataAnnotations attributes), so acting-user attribution goes through `DKNet.EfCore.DataAuthorization`'s `DataOwnerHook` instead — wired once in
|
|
49
|
+
- The endpoint becomes one `group.Map<Entity>Crud(o => …)` call instead of five hand-written `Map*` calls, with any route the generator cannot express excluded by name and hand-mapped below it (see `ProductV1Endpoint`). Declare authorization scopes with `[EndpointGroupScope]` on the endpoint class, one declaration per HTTP method (needs `DKNet.AspCore.Extensions` 13.0.0+, which this template pins); use `o.Configure(...)`/`.RequireAuthorization(...)` behind the `FeatureOptions.RequireAuthorization` guard only for a route whose HTTP method cannot decide its scope.
|
|
50
|
+
- **Validation-gap caveat — do not skip this:** a `[Range]`/`[Required]` on a `[CrudCreate]`/`[CrudUpdate]` parameter *is* forwarded onto the generated request property, but it is **never enforced** under this template's endpoint-registration convention — the .NET 10 validation source generator only sees literal `Map*(string, Delegate)` calls, and the generated CRUD route goes through `DKNet.AspCore.Extensions`'s generic `MapPost<TRequest,TDto>` wrapper instead. Confirmed live: `POST /v1/products` with a negative price returns `201`, not `400`. Pick this path only when that gap is acceptable, or when you plan to enforce the rule some other way. Also: `[FromClaim]` can never reach a generated request (the generator forwards only DataAnnotations attributes), so acting-user attribution goes through `DKNet.EfCore.DataAuthorization`'s `DataOwnerHook` instead — wired once in `<YourApp>.Api/Configs/ServiceConfigs.cs`, not per-entity.
|
|
51
51
|
|
|
52
52
|
## Build/verify gates
|
|
53
53
|
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
# dknet-minimal skills
|
|
2
2
|
|
|
3
|
-
Every folder here is one [Agent Skill](https://agentskills.io) (`<name>/SKILL.md`).
|
|
4
|
-
`dknet-minimal` Claude Code plugin; the same files are what
|
|
5
|
-
`@drunkcoding/dknet-minimal-skills` ships on npm.
|
|
3
|
+
Every folder here is one [Agent Skill](https://agentskills.io) (`<name>/SKILL.md`). `plugin/` is the
|
|
4
|
+
`dknet-minimal` Claude Code plugin (`claude --plugin-dir plugin`); the same files are what
|
|
5
|
+
`npx skills add baoduy/DKNet.Templates` installs and what `@drunkcoding/dknet-minimal-skills` ships on npm.
|
|
6
|
+
Start with `dknet-project-structure`.
|
|
6
7
|
|
|
7
8
|
## Workflows (invoke as a slash command with arguments)
|
|
8
9
|
|
|
9
10
|
| Skill | Arguments | Purpose |
|
|
10
11
|
|---|---|---|
|
|
11
|
-
| `/dknet-bdd-tests` | `<Feature> e.g. Orders` | Create and maintain Reqnroll + NUnit BDD .feature scenarios and step bindings in
|
|
12
|
+
| `/dknet-bdd-tests` | `<Feature> e.g. Orders` | Create and maintain Reqnroll + NUnit BDD .feature scenarios and step bindings in <YourApp>.App.BDDTests — request/status/response-body scenarios and domain-event side effects observed via log capture, for the hand-written PurchaseOrder and generator-driven Product samples. Use when adding or updating HTTP-facing scenarios for a DKNet.Templates feature. Result-level Result-object assertions, architecture rules and pure functional tests belong in the `dknet-unit-tests` skill instead — do not duplicate a behavior here that xUnit already covers. Invoke as `/dknet-bdd-tests <Feature>` to scaffold it for a feature. |
|
|
12
13
|
| `/dknet-crud` | `<Feature> <Entity> [mode=manual\|auto] [version=V1]` | Create commands (Create/Update/business transitions/Delete) at the AppServices layer for a DKNet.Minimal feature — request contracts, FluentValidation validators, and SlimMessageBus handlers, in both the hand-written and generator-driven (CrudCreate/CrudUpdate/CrudAction) shapes. Use after the domain entity and EF Core mapper exist. Queries and paged lists are the `dknet-queries-specs` skill; response DTOs and Mapster are `dknet-dto-mapping`. Invoke as `/dknet-crud <Feature> <Entity> [mode=manual\|auto] [version=V1]` to scaffold it for a feature. |
|
|
13
|
-
| `/dknet-docs` | `<Feature>` | Generate structured technical documentation and
|
|
14
|
+
| `/dknet-docs` | `<Feature>` | Generate structured technical documentation for a completed feature and every endpoint it exposes — README, architecture and flow diagrams drawn with archify (Mermaid as the fallback), per-route API reference, data model, and domain events. Use this when documenting an implemented feature. Invoke as `/dknet-docs <Feature>` to scaffold it for a feature. |
|
|
14
15
|
| `/dknet-endpoint` | `<Feature> <Entity> [mode=manual\|auto] [routePrefix] [version=V1]` | Create Minimal API endpoint configurations using this project's IEndpointConfig pattern — raw minimal-API routes mapped by hand, a single generated Map<Entity>Crud() call, or the underlying DKNet.AspCore.Extensions generic route helpers called directly. Use after AppServices actions (or CrudCreate/CrudUpdate/CrudAction entity attributes) are ready, to expose a feature over HTTP. Invoke as `/dknet-endpoint <Feature> <Entity> [mode=manual\|auto] [routePrefix] [version=V1]` to scaffold it for a feature. |
|
|
15
|
-
| `/dknet-entity` | `<Feature> <Entity> [mode=manual\|auto] [props…] e.g. Orders Order mode=manual Number:string Total:decimal` | 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
|
|
16
|
+
| `/dknet-entity` | `<Feature> <Entity> [mode=manual\|auto] [props…] e.g. Orders Order mode=manual Number:string Total:decimal` | 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. |
|
|
16
17
|
| `/dknet-feature-remove` | `<Feature> [--dry-run] e.g. Orders` | Retire a DKNet vertical-slice business feature end-to-end — deletes its folders across all six projects, cleans the out-of-folder touchpoints, and drops its tables via a new migration. |
|
|
17
18
|
| `/dknet-feature` | `<Feature> <Entity> [mode=manual\|auto] [props…] e.g. Orders Order mode=manual Number:string Total:decimal` | Drive an end-to-end DKNet vertical-slice feature from plan to merged tests in either the manual or automated flow — orchestrates entity, CRUD, endpoint, tests, BDD, and docs. |
|
|
18
|
-
| `/dknet-unit-tests` | `<Feature> <Entity> [mode=manual\|auto]` | Write xUnit + Shouldly tests for a DKNet.Templates feature in
|
|
19
|
+
| `/dknet-unit-tests` | `<Feature> <Entity> [mode=manual\|auto]` | 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. |
|
|
19
20
|
|
|
20
21
|
## Reference skills (loaded by the agent when the topic comes up)
|
|
21
22
|
|
|
@@ -23,14 +24,14 @@ Every folder here is one [Agent Skill](https://agentskills.io) (`<name>/SKILL.md
|
|
|
23
24
|
|---|---|
|
|
24
25
|
| `dknet-auth-and-ownership` | Explains authentication, per-route authorization scopes, acting-user attribution, row-level data ownership, and role-aware sensitive-data filtering in this template — including the demo authentication provider and how to write a test that runs with RequireAuthorization on. Use whenever adding a scope-guarded route, wiring acting-user attribution for a new feature, reasoning about data isolation between callers, or writing an auth-on integration test. |
|
|
25
26
|
| `dknet-ddd-principles` | DDD tactical judgment for this codebase — aggregate boundaries, entity vs. value object, invariant enforcement, when to use a domain event, avoiding anemic domain models. Use before dknet-entity and dknet-crud whenever the aggregate shape or business-rule placement isn't obvious. |
|
|
26
|
-
| `dknet-dto-mapping` | Design response DTOs and Mapster mapping for a DKNet.Minimal feature — hand-written
|
|
27
|
+
| `dknet-dto-mapping` | Design response DTOs and Mapster mapping for a DKNet.Minimal feature — [GenerateDto] first and hand-written records as the fallback, custom Mapster IRegister mappings for values the generator's convention can't produce, LazyMapper, and the JSON/sensitive-data contract. Use after (or alongside) dknet-crud when a handler needs a DTO to return. |
|
|
27
28
|
| `dknet-efcore-config` | Create EF Core entity type configurations (mappers), static data seeders, CoreDbContext wiring, and infra domain-service implementations following this project's assembly-scan auto-discovery conventions. Use after creating a domain entity, for both mode=manual and mode=auto entities. |
|
|
28
29
|
| `dknet-feature-lifecycle` | The add/remove lifecycle of a DKNet vertical-slice business feature — how to choose the manual vs automated flow, the exact file footprint a feature occupies across all six projects, and the out-of-folder touchpoints a delete must clean up. Use before /dknet-feature or /dknet-feature-remove, and whenever you need to enumerate or retire an existing feature. |
|
|
29
|
-
| `dknet-messaging-events` | Explains how this template wires SlimMessageBus as its command/query/event backbone, how the
|
|
30
|
+
| `dknet-messaging-events` | Explains how this template wires SlimMessageBus as its command/query/event backbone, how the three domain-event raise styles ([RaisesEvent], AddEvent<TEvent>(), AddEvent(instance)) reach a subscriber, and how to forward an event to an external Azure Service Bus topic. Use whenever adding a domain event, wiring an internal or external event consumer, or reasoning about how a command/query travels from an endpoint to its handler. |
|
|
30
31
|
| `dknet-package-adoption` | Add DKNet's Core, EF Core, messaging/CQRS, or blob-storage NuGet packages to an EXISTING .NET project that was NOT created from the DKNet.Minimal.Template. Use when a consumer wants to reuse pieces of the DKNet framework in their own project layout without scaffolding a new solution. |
|
|
31
32
|
| `dknet-platform-config` | Reference for everything the template wires outside a feature's own vertical slice — Program.cs start-up order, the FeatureManagement flag table, every configuration section and its defaults, launch-time jobs, the Aspire host, and the test-host overrides. Use when adding or changing a platform-level behavior (auth, rate limiting, health checks, telemetry, caching, CORS, a new flag, a new launch job) rather than a business feature. |
|
|
32
33
|
| `dknet-project-structure` | Orientation to the DKNet.Minimal.Template layer boundaries, the six projects, the vertical-slice folder layout for a feature, and auto-discovery wiring. Use first, before any other dknet-* skill, when working in a solution generated from this template. |
|
|
33
|
-
| `dknet-queries-specs` | Write read/query logic for a DKNet feature —
|
|
34
|
+
| `dknet-queries-specs` | 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. |
|
|
34
35
|
| `dknet-scaffold` | Scaffold a new solution from the DKNet.Minimal template and get it running — dotnet new install/new, the six template parameters, what the generated tree looks like, first build/run with or without Aspire, deleting the two shipped sample features, and installing this skill plugin into the generated repo. Use when starting a new DKNet.Minimal solution, or when orienting inside a freshly generated one. |
|
|
35
36
|
|
|
36
37
|
## Claude Code subagents (`../agents/`)
|
|
@@ -41,7 +42,7 @@ Every folder here is one [Agent Skill](https://agentskills.io) (`<name>/SKILL.md
|
|
|
41
42
|
|
|
42
43
|
## Authoring
|
|
43
44
|
|
|
44
|
-
Rules every skill follows (enforced by
|
|
45
|
-
`description` (max 1024 chars), paths relative to the consumer solution root (`ApiEndpoints
|
|
45
|
+
Rules every skill follows (enforced by `../../validate-plugin.sh`): frontmatter `name` equals the folder, a single-line
|
|
46
|
+
`description` (max 1024 chars), paths relative to the consumer solution root (`ApiEndpoints/<YourApp>.*` where `<YourApp>` is the consumer's solution prefix — never a hard-coded `Minimal.*`, never `src/`),
|
|
46
47
|
other skills referenced by name, no links into this repository's `docs/`, exemplar code inlined. This index is
|
|
47
48
|
generated from the frontmatter — regenerate it after changing a skill.
|
|
@@ -7,7 +7,7 @@ description: Explains authentication, per-route authorization scopes, acting-use
|
|
|
7
7
|
|
|
8
8
|
## `FeatureManagement:RequireAuthorization`
|
|
9
9
|
|
|
10
|
-
When `true`, `AddAppConfig` calls `AddAuthConfig` (
|
|
10
|
+
When `true`, `AddAppConfig` calls `AddAuthConfig` (`<YourApp>.Api/Configs/Auth/AuthConfig.cs`):
|
|
11
11
|
|
|
12
12
|
```csharp
|
|
13
13
|
services.AddAuthentication().AddJwtBearer();
|
|
@@ -47,7 +47,7 @@ local development and the test hosts do not exercise real JWT validation by defa
|
|
|
47
47
|
|
|
48
48
|
## Scopes → policies
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
`<YourApp>.Api/ApiEndpoints/AutomatedSample/ProductScopes.cs` defines the scope constants for the
|
|
51
51
|
`Product` feature:
|
|
52
52
|
|
|
53
53
|
```csharp
|
|
@@ -62,46 +62,96 @@ internal static class ProductScopes
|
|
|
62
62
|
```
|
|
63
63
|
|
|
64
64
|
`AuthConfig`'s `foreach (var scope in ProductScopes.All)` loop registers **one authorization policy
|
|
65
|
-
per scope, the scope value doubling as its own policy name** — so a route
|
|
66
|
-
|
|
65
|
+
per scope, the scope value doubling as its own policy name** — so a route names
|
|
66
|
+
`ProductScopes.Read` directly, with no separate policy name to remember.
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
and branches on it:
|
|
68
|
+
### Attach the scope with `[EndpointGroupScope]` first
|
|
69
|
+
|
|
70
|
+
Prefer declaring scopes **above the `IEndpointConfig` class**, not route by route inside `Map`:
|
|
72
71
|
|
|
73
72
|
```csharp
|
|
73
|
+
[EndpointGroupScope(ProductScopes.Read, EndpointHttpMethods.Get)]
|
|
74
|
+
[EndpointGroupScope(ProductScopes.Write, EndpointHttpMethods.Post, EndpointHttpMethods.Put,
|
|
75
|
+
EndpointHttpMethods.Delete)]
|
|
76
|
+
internal sealed class ProductV1Endpoint : IEndpointConfig
|
|
77
|
+
{
|
|
78
|
+
public int Version => 1;
|
|
79
|
+
public string GroupEndpoint => "/products";
|
|
80
|
+
|
|
81
|
+
public void Map(RouteGroupBuilder group) => group.MapProductCrud();
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
(`EndpointGroupScopeAttribute`/`EndpointHttpMethods` live in
|
|
86
|
+
`DKNet.AspCore.Extensions.Endpoints`.)
|
|
87
|
+
|
|
88
|
+
Rules, in the order they bite:
|
|
89
|
+
|
|
90
|
+
- The attribute is `AllowMultiple` — stack one declaration per scope, each naming the HTTP methods
|
|
91
|
+
that need it.
|
|
92
|
+
- A declaration naming **no** HTTP method is the group's default for every method it serves. A
|
|
93
|
+
declaration that names a method wins over that default for its own method.
|
|
94
|
+
- It is applied **only when `EndpointRegistrationOptions.RequireAuthorization` is `true`** — the
|
|
95
|
+
same value `Program.cs` assigns from `FeatureOptions.RequireAuthorization`. That is the reason to
|
|
96
|
+
prefer it: the flag check is built in, so there is no `if (!requireAuthorization) return;` branch
|
|
97
|
+
to write and no way to forget one.
|
|
98
|
+
- A route that already names its own policy, or allows anonymous access, is left alone — which is
|
|
99
|
+
what makes the per-route fallbacks below safe to mix in.
|
|
100
|
+
- Once **any** declaration exists on a group, an endpoint serving a method with neither its own
|
|
101
|
+
declaration nor a group default is **refused when the group's endpoints are built**. Cover every
|
|
102
|
+
method the group serves, or declare a default.
|
|
103
|
+
- Requires `DKNet.AspCore.Extensions` **13.0.0 or newer**. The shipped `ProductV1Endpoint` is
|
|
104
|
+
declared exactly this way.
|
|
105
|
+
|
|
106
|
+
### Fallback, in order
|
|
107
|
+
|
|
108
|
+
Drop one rung only when the rung above cannot express the rule:
|
|
109
|
+
|
|
110
|
+
1. **`[EndpointGroupScope]`** — the scope is a function of the HTTP method. Covers a whole
|
|
111
|
+
generated CRUD slice in two lines.
|
|
112
|
+
2. **`o.Configure(CrudOp.X, …)` / `o.Configure("RouteName", …)`** — two routes sharing one HTTP
|
|
113
|
+
method need *different* scopes, so no per-method declaration can separate them.
|
|
114
|
+
`o.Configure(CrudOp, …)` targets a generated composite route by operation kind (see
|
|
115
|
+
`dknet-endpoint`); `o.Configure("RouteName", …)` targets one `[CrudAction]` route by its C#
|
|
116
|
+
member name. `Product` needs this: `Update` and `AssignSupplierReference` are both `PUT`, and
|
|
117
|
+
the second must demand `products.supplier` rather than `products.write`.
|
|
118
|
+
3. **`.RequireAuthorization(scope)` on the `RouteHandlerBuilder`** — a hand-mapped route, or a
|
|
119
|
+
generated route replaced by a hand-mapped one (`discontinue` below).
|
|
120
|
+
|
|
121
|
+
Rungs 2 and 3 do **not** self-gate. Calling `RequireAuthorization(scope)` with the flag off throws
|
|
122
|
+
at request time, because `AddAuthConfig` never ran and no policy of that name was registered — so
|
|
123
|
+
each must sit behind a flag check. `ProductV1Endpoint` reads it once via DI, and needs it for
|
|
124
|
+
exactly the two `PUT` routes the group declarations cannot separate:
|
|
125
|
+
|
|
126
|
+
```csharp
|
|
127
|
+
// Only for the per-route overrides below — the class-level declarations gate themselves.
|
|
74
128
|
var requireAuthorization = ((IEndpointRouteBuilder)group).ServiceProvider
|
|
75
129
|
.GetRequiredService<IOptions<FeatureOptions>>().Value.RequireAuthorization;
|
|
76
130
|
|
|
77
131
|
group.MapProductCrud(o =>
|
|
78
132
|
{
|
|
79
133
|
o.Exclude("Discontinue");
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
o.Configure(CrudOp.Update, rb => rb.RequireAuthorization(ProductScopes.Write));
|
|
86
|
-
o.Configure(CrudOp.Delete, rb => rb.RequireAuthorization(ProductScopes.Write));
|
|
87
|
-
o.Configure("Approve", rb => rb.RequireAuthorization(ProductScopes.Write));
|
|
88
|
-
// Its own scope, not Write — holding only products.write must not be enough to assign it.
|
|
89
|
-
o.Configure("AssignSupplierReference", rb => rb.RequireAuthorization(ProductScopes.Supplier));
|
|
134
|
+
|
|
135
|
+
// A PUT like Update, so no per-method declaration separates them, and products.write must not
|
|
136
|
+
// be enough to assign a supplier reference.
|
|
137
|
+
if (requireAuthorization)
|
|
138
|
+
o.Configure("AssignSupplierReference", rb => rb.RequireAuthorization(ProductScopes.Supplier));
|
|
90
139
|
});
|
|
91
140
|
|
|
92
141
|
var discontinue = group.MapPut("{id:guid}/discontinue", /* ... */);
|
|
93
142
|
if (requireAuthorization) discontinue.RequireAuthorization(ProductScopes.Discontinue);
|
|
143
|
+
|
|
144
|
+
// GET summary needs no call at all — the group's products.read declaration covers it.
|
|
145
|
+
group.MapGet("summary", /* ... */);
|
|
94
146
|
```
|
|
95
147
|
|
|
96
|
-
`
|
|
97
|
-
|
|
98
|
-
its C# member name. A hand-mapped route (`discontinue` above) calls `.RequireAuthorization(scope)`
|
|
99
|
-
directly on the `RouteHandlerBuilder` the same way.
|
|
148
|
+
`GetById`, `GetList`, `Create`, `Update`, `Delete` and `Approve` carry no scope call: their HTTP
|
|
149
|
+
method decides their scope, and the two class-level declarations already say what it is.
|
|
100
150
|
|
|
101
151
|
`IEndpointConfig` also exposes an optional `string? AuthPolicy` member for gating an entire group
|
|
102
|
-
under one policy (`null` means plain authentication) rather than per-
|
|
103
|
-
sample overrides it
|
|
104
|
-
|
|
152
|
+
under one policy (`null` means plain authentication) rather than per-method scopes — neither shipped
|
|
153
|
+
sample overrides it, because different methods in the same group need different scopes and
|
|
154
|
+
`[EndpointGroupScope]` expresses that without giving up the group-level declaration.
|
|
105
155
|
|
|
106
156
|
**Recipe: add a new scope for a new feature.**
|
|
107
157
|
1. Add a constant (and to an `All` array, if you loop like `ProductScopes` does) in a
|
|
@@ -109,14 +159,15 @@ different routes in the same group need different scopes.
|
|
|
109
159
|
2. Register it as a policy — either loop over your `All` array in `AuthConfig` the way
|
|
110
160
|
`ProductScopes.All` is registered, or call `options.AddPolicy(YourScopes.X, ...)` explicitly for a
|
|
111
161
|
one-off scope.
|
|
112
|
-
3.
|
|
113
|
-
|
|
114
|
-
|
|
162
|
+
3. Attach it with `[EndpointGroupScope(YourScopes.X, EndpointHttpMethods.…)]` on the endpoint class.
|
|
163
|
+
Only where a single HTTP method needs two different scopes, fall back to `o.Configure(...)` or
|
|
164
|
+
`.RequireAuthorization(...)` — and then gate that call behind
|
|
165
|
+
`FeatureOptions.RequireAuthorization`, never call it unconditionally.
|
|
115
166
|
|
|
116
167
|
## Demo authentication
|
|
117
168
|
|
|
118
169
|
`FeatureManagement:EnableDemoAuthentication` registers `DemoAuthenticationHandler`
|
|
119
|
-
(
|
|
170
|
+
(`<YourApp>.Api/Configs/Auth/DemoAuthConfig.cs`) as the default authenticate/challenge scheme. Every
|
|
120
171
|
request is authenticated, unconditionally, as a fixed fake identity:
|
|
121
172
|
|
|
122
173
|
```csharp
|
|
@@ -156,10 +207,19 @@ if (string.IsNullOrEmpty(request.ByUser))
|
|
|
156
207
|
— from `CreatePurchaseOrderCommandHandler`. A payload value for `ByUser` is always overwritten, never
|
|
157
208
|
trusted; this is a security seam, not a binding convenience.
|
|
158
209
|
|
|
210
|
+
`[FromRequestHeader("X-Header-Name")]` is the same populator reading a named request header instead
|
|
211
|
+
of a claim: also overwritten before validation and the handler (with the property's default when the
|
|
212
|
+
header is absent), also impossible to forge through the payload, and also inert unless
|
|
213
|
+
`AddContextualRequestPopulation` is registered — it is. A missing header is never a refusal, just a
|
|
214
|
+
default. **It is not an authorization signal**: a header is caller-supplied and carries no identity
|
|
215
|
+
guarantee, so never use it where `[FromClaim]` belongs. Neither shipped sample uses it; reach for it
|
|
216
|
+
for correlation ids, a tenant hint, or a client-version marker the handler wants without adding a
|
|
217
|
+
body field.
|
|
218
|
+
|
|
159
219
|
**Automated: `PrincipalProvider` + two save hooks.** A `[CrudCreate]`/`[CrudUpdate]`/`[CrudAction]`
|
|
160
220
|
generated request forwards only `System.ComponentModel.DataAnnotations` attributes, so it can never
|
|
161
221
|
carry `[FromClaim]`. Acting-user attribution instead goes through `PrincipalProvider`
|
|
162
|
-
(
|
|
222
|
+
(`<YourApp>.Api/Configs/Handlers/PrincipalProvider.cs`), which implements `IPrincipalProvider`
|
|
163
223
|
(`IDataOwnerProvider` + `ICurrentUserProvider` plus `ProfileId`/`Email`/`UserName`):
|
|
164
224
|
|
|
165
225
|
```csharp
|
|
@@ -257,7 +317,7 @@ pins the whole chain: an authenticated caller with no resolvable subject claim g
|
|
|
257
317
|
is never persisted at all (checked with `IgnoreQueryFilters()` directly against the database, not
|
|
258
318
|
just "unreadable over HTTP").
|
|
259
319
|
|
|
260
|
-
**Tests that pin this area** (all in
|
|
320
|
+
**Tests that pin this area** (all in `<YourApp>.App.Tests/Integration/...`):
|
|
261
321
|
|
|
262
322
|
- `AutomatedSample/V1/ProductOwnershipIsolationTests` — two distinct authenticated callers get
|
|
263
323
|
distinct ownership keys; neither can read or list the other's row; `oid` takes precedence over
|
|
@@ -325,7 +385,7 @@ that two callers are judged independently in either request order), `ProductSens
|
|
|
325
385
|
POST routes are not idempotent unless the route calls `.RequiredIdempotentKey()` explicitly, with
|
|
326
386
|
callers sending `X-Idempotency-Key`. The store is Redis when `ConnectionStrings:Redis` is set, else
|
|
327
387
|
an in-process in-memory store; both use `ConflictHandling = IdempotentConflictHandling.ConflictResponse`
|
|
328
|
-
(
|
|
388
|
+
(`<YourApp>.Api/Configs/AppConfig.cs`). See `dknet-crud` for how a handler's `Result`
|
|
329
389
|
maps to a response body, and `dknet-platform-config` for CORS, security headers, and rate limiting.
|
|
330
390
|
Status mapping: 400 validation, 401 no/invalid credential, 403 `OwnershipRequiredException` or a
|
|
331
391
|
failed authorization policy, 404 not found or filtered out by ownership, 409 a
|
|
@@ -365,7 +425,7 @@ public sealed class AuthOnApiFixture : TestApiFactoryBase, IAsyncLifetime
|
|
|
365
425
|
This is only safe because the test assembly disables collection parallelization — no other test's
|
|
366
426
|
host can boot while the variable is set, or it would leak into an unrelated test run.
|
|
367
427
|
|
|
368
|
-
`TestAuthHandler` (
|
|
428
|
+
`TestAuthHandler` (`<YourApp>.App.TestSupport/TestAuthHandler.cs`) replaces the real JWT bearer scheme
|
|
369
429
|
so a request can be authenticated without a live token. It issues a fixed name/subject and a scope
|
|
370
430
|
claim built from every `ProductScopes` entry by default, overridable per request via a header:
|
|
371
431
|
|
|
@@ -387,7 +447,7 @@ in the constructor, cleared in `Dispose`, `TestAuthHandler.Register(services)` i
|
|
|
387
447
|
`ConfigureTestServices`), then assert against `TestAuthHandler.CallerName` /
|
|
388
448
|
`TestAuthHandler.CallerProfileId` the same way `PurchaseOrderSecurityTests`/`ProductSecurityTests`
|
|
389
449
|
do. For a test that needs two distinct callers in one host (row-isolation tests),
|
|
390
|
-
|
|
450
|
+
`<YourApp>.App.TestSupport/MultiSubjectAuthHandler.cs` reads the subject from a request header instead
|
|
391
451
|
of a fixed constant — see `AuthOnMultiSubjectApiFixture` and `ProductOwnershipIsolationTests`. For a
|
|
392
452
|
caller authenticated with no name claim at all, see `AuthOnNoNameClaimApiFixture`. For a fixed-tenant
|
|
393
453
|
`OwnedBy` with a still-distinct acting `CreatedBy`, see `AuthOnFixedTenantApiFixture`.
|
|
@@ -399,8 +459,12 @@ the thing under test, and it is expected to fail start-up.
|
|
|
399
459
|
|
|
400
460
|
- **What you might expect:** calling `.RequireAuthorization(scope)` unconditionally on a route.
|
|
401
461
|
**What actually happens:** with `RequireAuthorization` off, no policies were ever registered, so
|
|
402
|
-
the call throws at request time.
|
|
403
|
-
`ProductV1Endpoint` does
|
|
462
|
+
the call throws at request time. Gate it on `FeatureOptions.RequireAuthorization`, as
|
|
463
|
+
`ProductV1Endpoint` does — or declare the scope with `[EndpointGroupScope]`, which is applied only
|
|
464
|
+
when that flag is on and therefore needs no branch at all.
|
|
465
|
+
- **What you might expect:** one `[EndpointGroupScope]` covering the methods you care about leaves
|
|
466
|
+
the rest of the group unguarded. **What actually happens:** it fails closed — a served method with
|
|
467
|
+
no declaration and no group default makes endpoint building throw, not silently pass through.
|
|
404
468
|
- **What you might expect:** giving a generated `[CrudAction]` a parameter meant to auto-populate
|
|
405
469
|
from the caller. **What actually happens:** it becomes an ordinary, caller-settable bound property
|
|
406
470
|
— there is no `[FromClaim]` seam on a generated request.
|