@drunkcoding/dknet-implementation-skills 0.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
|
@@ -24,32 +24,38 @@ behavior — which is exactly the confusion this section exists to prevent.
|
|
|
24
24
|
|
|
25
25
|
### At a glance: which one should I copy?
|
|
26
26
|
|
|
27
|
-
**
|
|
27
|
+
**Default to `auto` (mirror `Product`).** Declare the operation on the entity —
|
|
28
|
+
`[CrudCreate]`/`[CrudUpdate]`/`[CrudAction]`/`[RaisesEvent]`/`[GenerateDto]` — and let
|
|
29
|
+
`DKNet.SlimBus.Generators` emit the request, handler and route. Start here for every new aggregate
|
|
30
|
+
and only step down when one of the reasons below actually applies to the feature in front of you.
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
**Step down to `manual` (mirror `PurchaseOrder`)** when the feature needs any of:
|
|
33
|
+
|
|
34
|
+
- Idempotent writes — safe client retries on `POST`. The generated create route has no
|
|
35
|
+
`.RequiredIdempotentKey()` and none can be added to it.
|
|
30
36
|
- An attribute-declared (`DataAnnotations`) rule that must actually return `400`, not just be
|
|
31
|
-
present on the generated request
|
|
37
|
+
present on the generated request (see the gap below) — and that cannot be re-expressed as a
|
|
38
|
+
FluentValidation rule.
|
|
32
39
|
- An operation that writes more than one aggregate in one transaction.
|
|
33
40
|
- A filtered or customized list/get query beyond the generic list route's `filter`/`search`/`orderBy`
|
|
34
41
|
contract.
|
|
35
42
|
- The acting user must come from a claim on the request itself (`[FromClaim]`).
|
|
36
43
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
both answering `409` via a `PreconditionCodes`-prefixed error code.
|
|
44
|
+
None of these apply? Then `auto` is the answer, even for an aggregate with real business rules.
|
|
45
|
+
Specifically, **none of the following is a reason to hand-write**:
|
|
46
|
+
|
|
47
|
+
- **A rule that must refuse an operation.** A FluentValidation validator written against a generated
|
|
48
|
+
request still runs — `UseEndpointConfigs` applies `AddFluentValidationAutoValidation()` to every
|
|
49
|
+
endpoint group, generated routes included — so it can read stored data through `IRepositorySpec`
|
|
50
|
+
and refuse before the generated handler runs. `Product` ships two such rules
|
|
51
|
+
(`CreateProductRequestValidator`, `DeleteProductRequestValidator`), both answering `409` via a
|
|
52
|
+
`PreconditionCodes`-prefixed error code.
|
|
53
|
+
- **A DTO that must hide fields.** `[GenerateDto(..., Exclude = [...])]` narrows the shape, and
|
|
54
|
+
`[SensitiveData]` travels from the entity onto the generated DTO.
|
|
55
|
+
- **A derived response value.** A Mapster `IRegister` reaches the generated route's response with no
|
|
56
|
+
endpoint change (`ProductDto.GrossMargin`).
|
|
57
|
+
- **A domain event.** `[RaisesEvent]` raises it from the save hook; the consumer is hand-written
|
|
58
|
+
either way.
|
|
53
59
|
|
|
54
60
|
| Trade-off | `manual` | `auto` |
|
|
55
61
|
|---|---|---|
|
|
@@ -75,20 +81,20 @@ against the real tree under both sample features.
|
|
|
75
81
|
|
|
76
82
|
| # | Path | `manual` | `auto` |
|
|
77
83
|
|---|---|---|---|
|
|
78
|
-
| 1 |
|
|
79
|
-
| 2 |
|
|
80
|
-
| 3 |
|
|
81
|
-
| 4 |
|
|
82
|
-
| 5 |
|
|
83
|
-
| 6 |
|
|
84
|
-
| 7 |
|
|
85
|
-
| 8 |
|
|
86
|
-
| 9 |
|
|
87
|
-
| 10 |
|
|
88
|
-
| 11 |
|
|
89
|
-
| 12 |
|
|
90
|
-
| 13 |
|
|
91
|
-
| 14 |
|
|
84
|
+
| 1 | `<YourApp>.Domains/Features/<Feature>/Entities/` | entity + hand-written event record(s) | entity only — `[RaisesEvent]`/`[CrudCreate]`/`[CrudUpdate]`/`[CrudAction]` carry the rest |
|
|
85
|
+
| 2 | `<YourApp>.Infra/Features/<Feature>/Mappers/` | `IEntityTypeConfiguration<T>` | same — **no generator produces this** |
|
|
86
|
+
| 3 | `<YourApp>.Infra/Features/<Feature>/StaticData/` | optional `DataSeedingConfiguration<T>` | optional (neither sample ships one for `Product`) |
|
|
87
|
+
| 4 | `<YourApp>.Infra/Features/<Feature>/ExternalEvents/` | not used by `PurchaseOrder` | optional broker consumer (`ProductCreatedNotificationHandler`) |
|
|
88
|
+
| 5 | `<YourApp>.AppServices/<Feature>/V1/Actions/` | command requests + handlers | only operations excluded from the generated map (e.g. `Discontinue.cs`) |
|
|
89
|
+
| 6 | `<YourApp>.AppServices/<Feature>/V1/Queries/` | hand-written read requests + handlers | custom read shapes the generic list can't express (e.g. a price-summary query) |
|
|
90
|
+
| 7 | `<YourApp>.AppServices/<Feature>/V1/Specs/` | `Specification<T>` filters | specs backing a validator or an excluded query |
|
|
91
|
+
| 8 | `<YourApp>.AppServices/<Feature>/V1/Events/` | domain event consumers | domain event consumers only — the generator raises, it does not consume |
|
|
92
|
+
| 9 | `<YourApp>.AppServices/<Feature>/V1/Validators/` | not needed — validation is enforced on literal routes | validators against a **generated** request that must refuse (`CreateXRequestValidator`, `DeleteXRequestValidator`) |
|
|
93
|
+
| 10 | `<YourApp>.AppServices/<Feature>/V1/<Feature>Dto.cs` (+ optional `<Feature>MappingRegister.cs`) | hand-written DTO record | one `[GenerateDto(typeof(Entity))] public sealed partial record` line; a Mapster `IRegister` only if a response value needs deriving from more than one column |
|
|
94
|
+
| 11 | `<YourApp>.Api/ApiEndpoints/<Feature>/<Entity>V1Endpoint.cs` | every route a literal `Map*` call | one `group.Map<Entity>Crud(o => …)` call, plus any excluded route mapped literally below it |
|
|
95
|
+
| 12 | `<YourApp>.App.Tests/Unit/<Feature>/` | entity/validator/spec tests | entity/handler tests |
|
|
96
|
+
| 13 | `<YourApp>.App.Tests/Integration/<Feature>/V1/` | result-level handler + security tests | same |
|
|
97
|
+
| 14 | `<YourApp>.App.BDDTests/Features/<Plural>/` | `*.feature` + `Steps/*.cs` | same |
|
|
92
98
|
|
|
93
99
|
Generated code for `auto` lands in `obj/Generated/DKNet.SlimBus.Generators/` (requests, handlers,
|
|
94
100
|
route registration) and `obj/Generated/DKNet.EfCore.DtoGenerator/` (the DTO's generated members) —
|
|
@@ -101,18 +107,18 @@ the reason a feature delete is a command and not an `rm -rf`.
|
|
|
101
107
|
|
|
102
108
|
| Touchpoint | File | When it applies |
|
|
103
109
|
|---|---|---|
|
|
104
|
-
| Schema constant |
|
|
105
|
-
| Broker topology |
|
|
106
|
-
| Feature flag |
|
|
110
|
+
| Schema constant | `<YourApp>.Domains/Share/DomainSchemas.cs` | if the feature added its own `const string` (the samples instead use literal schema strings — `"manual_sample"`/`"sample"` — directly in their mapper's `ToTable` call) |
|
|
111
|
+
| Broker topology | `<YourApp>.Infra/Extensions/ServiceBusSetup.cs` | the `azb.Produce<T>`/`azb.Consume<T>` pair, e.g. `ProductCreatedEvent` on `product-tp`/`product-sub` |
|
|
112
|
+
| Feature flag | `<YourApp>.Share/Options/FeatureOptions.cs` + `FeatureManagement` section in every `appsettings*.json` | if the feature gated itself behind a flag |
|
|
107
113
|
| Auth scopes | the feature's own scopes class (e.g. `ProductScopes`) + its `foreach` registration in `Configs/Auth/AuthConfig.cs` | if the feature registered per-route scope policies |
|
|
108
|
-
| Precondition codes |
|
|
109
|
-
| Test-support visibility | `InternalsVisibleTo` in the owning project's `.csproj` (e.g.
|
|
110
|
-
| EF migration |
|
|
114
|
+
| Precondition codes | `<YourApp>.AppServices/Share/PreconditionCodes.cs` | if a validator added a `precondition.`-prefixed code for this feature |
|
|
115
|
+
| Test-support visibility | `InternalsVisibleTo` in the owning project's `.csproj` (e.g. `<YourApp>.Api.csproj` grants `<YourApp>.App.TestSupport` and `<YourApp>.App.Tests` visibility onto `internal` scope classes) | if the feature's `internal` types need to be visible to test doubles |
|
|
116
|
+
| EF migration | `<YourApp>.Infra/Migrations/` | see §4 — never hand-delete an applied migration |
|
|
111
117
|
|
|
112
118
|
Enumerate existing features at any time — no registry file to keep in sync:
|
|
113
119
|
|
|
114
120
|
```bash
|
|
115
|
-
ls ApiEndpoints
|
|
121
|
+
ls ApiEndpoints/<YourApp>.Domains/Features/
|
|
116
122
|
```
|
|
117
123
|
|
|
118
124
|
## 4. Migration rules on removal
|
|
@@ -120,11 +126,11 @@ ls ApiEndpoints/Minimal.Domains/Features/
|
|
|
120
126
|
The tables outlive the code. Decide by whether the feature's migration has been applied anywhere:
|
|
121
127
|
|
|
122
128
|
- **Not applied and it is the newest migration** —
|
|
123
|
-
`dotnet ef migrations remove -c CoreDbContext -p
|
|
129
|
+
`dotnet ef migrations remove -c CoreDbContext -p <YourApp>.Infra/<YourApp>.Infra.csproj` (run from
|
|
124
130
|
`ApiEndpoints/`).
|
|
125
131
|
- **Applied, or newer migrations sit on top of it** — do NOT touch the old migration. Delete the
|
|
126
132
|
entity and mapper, then
|
|
127
|
-
`dotnet ef migrations add Drop<Feature> -c CoreDbContext -p
|
|
133
|
+
`dotnet ef migrations add Drop<Feature> -c CoreDbContext -p <YourApp>.Infra/<YourApp>.Infra.csproj`
|
|
128
134
|
and let EF emit the drop. Rewriting applied history corrupts `__EFMigrationsHistory` for every
|
|
129
135
|
environment already running it.
|
|
130
136
|
|
|
@@ -16,12 +16,12 @@ and the database tables all live outside the feature folders.
|
|
|
16
16
|
## Inputs
|
|
17
17
|
|
|
18
18
|
`$ARGUMENTS` — the feature folder name (PascalCase, as it appears under
|
|
19
|
-
|
|
19
|
+
`<YourApp>.Domains/Features/`), plus optional `--dry-run`.
|
|
20
20
|
|
|
21
21
|
If no feature is named, list the candidates and stop:
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
ls ApiEndpoints
|
|
24
|
+
ls ApiEndpoints/<YourApp>.Domains/Features/
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
## Required reading
|
|
@@ -32,7 +32,7 @@ ls ApiEndpoints/Minimal.Domains/Features/
|
|
|
32
32
|
|
|
33
33
|
## Phase 0 — Confirm and inventory (always, even under `--dry-run`)
|
|
34
34
|
|
|
35
|
-
1. Resolve the feature: confirm `ApiEndpoints
|
|
35
|
+
1. Resolve the feature: confirm `ApiEndpoints/<YourApp>.Domains/Features/<Feature>/` exists. If it
|
|
36
36
|
does not, list the candidates and STOP — do not guess at a near-match.
|
|
37
37
|
2. Inventory every path that will be deleted, using the §2 footprint. Report actual matches only:
|
|
38
38
|
```bash
|
|
@@ -58,24 +58,24 @@ Do NOT reorder. Each step removes only things that nothing later in the list dep
|
|
|
58
58
|
intermediate build failure points at real coupling rather than at the ordering.
|
|
59
59
|
|
|
60
60
|
1. Docs — `docs/features/<slug>/` (or wherever the solution keeps feature docs).
|
|
61
|
-
2. BDD — `ApiEndpoints
|
|
61
|
+
2. BDD — `ApiEndpoints/<YourApp>.App.BDDTests/Features/<Plural>/` (both `*.feature` and the
|
|
62
62
|
generated `*.feature.cs`, plus `Steps/`).
|
|
63
|
-
3. Tests —
|
|
64
|
-
Also grep
|
|
65
|
-
4. Api —
|
|
66
|
-
5. AppServices —
|
|
67
|
-
6. Infra —
|
|
68
|
-
7. Domains —
|
|
63
|
+
3. Tests — `<YourApp>.App.Tests/Unit/<Feature>/` and `<YourApp>.App.Tests/Integration/<Feature>/`.
|
|
64
|
+
Also grep `<YourApp>.App.Tests/Architecture/` — a convention test may assert on this feature by name.
|
|
65
|
+
4. Api — `<YourApp>.Api/ApiEndpoints/<Feature>/`.
|
|
66
|
+
5. AppServices — `<YourApp>.AppServices/<Feature>/`.
|
|
67
|
+
6. Infra — `<YourApp>.Infra/Features/<Feature>/`.
|
|
68
|
+
7. Domains — `<YourApp>.Domains/Features/<Feature>/`.
|
|
69
69
|
|
|
70
70
|
## Phase 2 — Out-of-folder touchpoints
|
|
71
71
|
|
|
72
72
|
Work the §3 table. For each, edit surgically — remove the feature's lines, never the whole file:
|
|
73
73
|
|
|
74
|
-
1.
|
|
74
|
+
1. `<YourApp>.Domains/Share/DomainSchemas.cs` — drop the feature's `const string`, if it added one.
|
|
75
75
|
Leave `Migration` and `Profile` alone unless this feature owned one of them.
|
|
76
|
-
2.
|
|
76
|
+
2. `<YourApp>.Infra/Extensions/ServiceBusSetup.cs` — drop the matching `azb.Produce<T>` /
|
|
77
77
|
`azb.Consume<T>` pair and the now-unused `using`.
|
|
78
|
-
3.
|
|
78
|
+
3. `<YourApp>.Share/Options/FeatureOptions.cs` + every `appsettings*.json` `FeatureManagement` section
|
|
79
79
|
— drop the flag property and its JSON key together. A key with no property silently no-ops, so
|
|
80
80
|
an orphan here fails no test; delete both halves or neither.
|
|
81
81
|
4. Docs cross-links — the docs index page and any feature index that linked the slice.
|
|
@@ -84,8 +84,8 @@ Work the §3 table. For each, edit surgically — remove the feature's lines, ne
|
|
|
84
84
|
|
|
85
85
|
Apply §4 of the lifecycle skill. State which branch you took and why:
|
|
86
86
|
|
|
87
|
-
- Feature's migration is the newest and unapplied → `cd ApiEndpoints && dotnet ef migrations remove -c CoreDbContext -p
|
|
88
|
-
- Otherwise → `cd ApiEndpoints && dotnet ef migrations add Drop<Feature> -c CoreDbContext -p
|
|
87
|
+
- Feature's migration is the newest and unapplied → `cd ApiEndpoints && dotnet ef migrations remove -c CoreDbContext -p <YourApp>.Infra/<YourApp>.Infra.csproj`
|
|
88
|
+
- Otherwise → `cd ApiEndpoints && dotnet ef migrations add Drop<Feature> -c CoreDbContext -p <YourApp>.Infra/<YourApp>.Infra.csproj` and verify the generated `Up`
|
|
89
89
|
contains the expected `DropTable` calls and nothing else.
|
|
90
90
|
|
|
91
91
|
Read the generated migration before moving on. A drop migration that also touches an unrelated table
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: dknet-messaging-events
|
|
3
|
-
description: Explains how this template wires SlimMessageBus as its command/query/event backbone, how the
|
|
3
|
+
description: 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.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# DKNet messaging and events (SlimMessageBus)
|
|
@@ -31,12 +31,12 @@ Handlers never call `SaveChanges`. `AddSlimBusEfCoreInterceptor<CoreDbContext>()
|
|
|
31
31
|
successfully.
|
|
32
32
|
|
|
33
33
|
**Auto-discovery, no per-message registration.** `AutoDeclareFrom(serviceAssembly)` scans the
|
|
34
|
-
|
|
34
|
+
`<YourApp>.AppServices` assembly and declares every request/handler pair it finds by convention;
|
|
35
35
|
`AddServicesFromAssembly(serviceAssembly)` registers the discovered handler classes in DI. Adding a
|
|
36
36
|
new `*Request` + `*Handler` pair needs no wiring beyond writing the two classes — see the
|
|
37
37
|
`dknet-crud` skill for how requests, validators, and handlers are shaped.
|
|
38
38
|
|
|
39
|
-
## Wiring:
|
|
39
|
+
## Wiring: `<YourApp>.Infra/Extensions/ServiceBusSetup.cs`
|
|
40
40
|
|
|
41
41
|
```csharp
|
|
42
42
|
public static IServiceCollection AddServiceBus(
|
|
@@ -89,13 +89,20 @@ header envelope or JSON round-trip. `EnableBlockingPublish = false` means `bus.P
|
|
|
89
89
|
domain event does not wait for every subscriber to finish before returning — a slow or hung internal
|
|
90
90
|
consumer does not block the HTTP response.
|
|
91
91
|
|
|
92
|
-
## Domain events:
|
|
92
|
+
## Domain events: three raise styles, same publisher
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
messaging.
|
|
94
|
+
All are covered in full in the `dknet-entity` and `dknet-ddd-principles` skills; here only what
|
|
95
|
+
matters for messaging. Prefer them in this order — `[RaisesEvent]`, then `AddEvent<TEvent>()`, then
|
|
96
|
+
`AddEvent(instance)`.
|
|
96
97
|
|
|
97
|
-
- **Manual** — `PurchaseOrder`'s constructor calls
|
|
98
|
-
hand; the record is a plain hand-written type
|
|
98
|
+
- **Manual, instance** — `PurchaseOrder`'s constructor calls
|
|
99
|
+
`AddEvent(new PurchaseOrderCreatedEvent(...))` by hand; the record is a plain hand-written type
|
|
100
|
+
next to the entity. Last resort: use it only when the payload is not a projection of the entity.
|
|
101
|
+
- **Manual, type-only** — `AddEvent<TEvent>()` queues the event *type*; the publisher maps the
|
|
102
|
+
entity onto it via `IMapper` when the save succeeds, so there is no hand-written payload. It
|
|
103
|
+
**requires an `IMapper` registration** — without one the publisher throws `EventException` instead
|
|
104
|
+
of dropping the event. Use it when the decision to raise needs real logic but the payload does
|
|
105
|
+
not.
|
|
99
106
|
- **Declared** — `Product` carries `[RaisesEvent(EventOperations.Created, Include = [...])]` and
|
|
100
107
|
`[RaisesEvent(EventOperations.Updated, nameof(Price))]`; DKNet's EF Core save hook raises the event
|
|
101
108
|
itself after a successful save. Composed names fold the narrowing property in:
|
|
@@ -104,7 +111,7 @@ messaging.
|
|
|
104
111
|
property's value actually changed on that save — calling `ChangePrice` with the price it already
|
|
105
112
|
holds raises nothing.
|
|
106
113
|
|
|
107
|
-
Either way, the entity only **queues** the event.
|
|
114
|
+
Either way, the entity only **queues** the event. `<YourApp>.Infra/Services/EventPublisher.cs` is what
|
|
108
115
|
actually calls the bus:
|
|
109
116
|
|
|
110
117
|
```csharp
|
|
@@ -128,13 +135,13 @@ a specific order across multiple handlers of the same event, and multiple consum
|
|
|
128
135
|
allowed (both an internal and an external consumer subscribe to the same `ProductCreatedEvent`, see
|
|
129
136
|
below).
|
|
130
137
|
|
|
131
|
-
**Consumers are always hand-written.** Neither `[RaisesEvent]` nor `AddEvent`
|
|
132
|
-
only the raise side is automatic for the declared style.
|
|
138
|
+
**Consumers are always hand-written.** Neither `[RaisesEvent]` nor either `AddEvent` overload
|
|
139
|
+
generates a consumer — only the raise side is automatic for the declared style.
|
|
133
140
|
|
|
134
|
-
Internal consumers live in
|
|
141
|
+
Internal consumers live in `<YourApp>.AppServices/<Feature>/V1/Events/`:
|
|
135
142
|
|
|
136
143
|
```csharp
|
|
137
|
-
//
|
|
144
|
+
// <YourApp>.AppServices/ManualSample/V1/Events/PurchaseOrderCreatedEventHandler.cs
|
|
138
145
|
internal sealed class PurchaseOrderCreatedEventHandler(ILogger<PurchaseOrderCreatedEventHandler> logger)
|
|
139
146
|
: Fluents.EventsConsumers.IHandler<PurchaseOrderCreatedEvent>
|
|
140
147
|
{
|
|
@@ -152,7 +159,7 @@ internal sealed class PurchaseOrderCreatedEventHandler(ILogger<PurchaseOrderCrea
|
|
|
152
159
|
```
|
|
153
160
|
|
|
154
161
|
```csharp
|
|
155
|
-
//
|
|
162
|
+
// <YourApp>.AppServices/AutomatedSample/V1/Events/ProductEventHandlers.cs
|
|
156
163
|
internal sealed class ProductCreatedEventHandler(ILogger<ProductCreatedEventHandler> logger)
|
|
157
164
|
: Fluents.EventsConsumers.IHandler<ProductCreatedEvent>
|
|
158
165
|
{
|
|
@@ -171,7 +178,7 @@ A second child bus, `"AzureBus"`, is added only when **both** conditions hold:
|
|
|
171
178
|
|
|
172
179
|
| Condition | Where |
|
|
173
180
|
|---|---|
|
|
174
|
-
| `FeatureManagement:EnableServiceBus` is `true` |
|
|
181
|
+
| `FeatureManagement:EnableServiceBus` is `true` | `<YourApp>.Share/Options/FeatureOptions.cs` |
|
|
175
182
|
| `ConnectionStrings:AzureBus` is a non-empty connection string | checked in `AddServiceBus` |
|
|
176
183
|
|
|
177
184
|
```csharp
|
|
@@ -228,12 +235,12 @@ The same event type — `ProductCreatedEvent` — flows on both buses. There is
|
|
|
228
235
|
event record. The `Produce`/`Consume` declaration in `AddAzureBus` is what forwards an
|
|
229
236
|
already-declared internal event externally; nothing about the event itself changes.
|
|
230
237
|
|
|
231
|
-
External consumers live in
|
|
238
|
+
External consumers live in `<YourApp>.Infra/Features/<Feature>/ExternalEvents/`, are `internal
|
|
232
239
|
sealed`, and are discovered by the same `AddServicesFromAssembly(typeof(InfraSetup).Assembly)` call
|
|
233
240
|
inside `AddAzureBus` — no separate registration:
|
|
234
241
|
|
|
235
242
|
```csharp
|
|
236
|
-
//
|
|
243
|
+
// <YourApp>.Infra/Features/AutomatedSample/ExternalEvents/ProductCreatedNotificationHandler.cs
|
|
237
244
|
internal sealed class ProductCreatedNotificationHandler(ILogger<ProductCreatedNotificationHandler> logger)
|
|
238
245
|
: Fluents.EventsConsumers.IHandler<ProductCreatedEvent>
|
|
239
246
|
{
|
|
@@ -259,7 +266,7 @@ internal sealed class ProductCreatedNotificationHandler(ILogger<ProductCreatedNo
|
|
|
259
266
|
.WithConsumer<THandler>());
|
|
260
267
|
```
|
|
261
268
|
2. Write `THandler` as a `Fluents.EventsConsumers.IHandler<TEvent>` under
|
|
262
|
-
|
|
269
|
+
`<YourApp>.Infra/Features/<Feature>/ExternalEvents/`. External-system concerns belong in `Infra`,
|
|
263
270
|
never `AppServices`.
|
|
264
271
|
3. Nothing else — `azb.AddServicesFromAssembly(typeof(InfraSetup).Assembly)` already picks up the
|
|
265
272
|
new handler by assembly scan.
|
|
@@ -274,7 +281,7 @@ azb.Consume<TExternalEvent>(o => o.Path("<their-topic-name>")
|
|
|
274
281
|
.WithConsumer<THandler>());
|
|
275
282
|
```
|
|
276
283
|
|
|
277
|
-
`THandler` still goes in
|
|
284
|
+
`THandler` still goes in `<YourApp>.Infra/Features/<Feature>/ExternalEvents/` and still needs no
|
|
278
285
|
manual DI registration. Do not add a matching `azb.Produce<TExternalEvent>(...)` — that would make
|
|
279
286
|
this service claim ownership of an event type it does not raise.
|
|
280
287
|
|
|
@@ -289,13 +296,13 @@ from Azure Service Bus: `ProductCreatedEvent` is still published in-memory and h
|
|
|
289
296
|
|
|
290
297
|
## Local development
|
|
291
298
|
|
|
292
|
-
|
|
299
|
+
`<YourApp>.AppHost/AppHost.cs` (Aspire orchestration) wires only Redis and PostgreSQL today:
|
|
293
300
|
|
|
294
301
|
```csharp
|
|
295
302
|
var cache = builder.AddRedis("Redis");
|
|
296
303
|
var postgres = builder.AddPostgres("Postgres");
|
|
297
304
|
...
|
|
298
|
-
builder.AddProject("Api", "
|
|
305
|
+
builder.AddProject("Api", "../<YourApp>.Api/<YourApp>.Api.csproj")
|
|
299
306
|
.WithReference(cache, "Redis")
|
|
300
307
|
.WithReference(apDb, "AppDb")
|
|
301
308
|
//.WaitFor(bus)
|
|
@@ -304,7 +311,7 @@ builder.AddProject("Api", "../Minimal.Api/Minimal.Api.csproj")
|
|
|
304
311
|
```
|
|
305
312
|
|
|
306
313
|
The `.WaitFor(bus)` line is commented out and no `bus` resource is added above it — no Azure Service
|
|
307
|
-
Bus emulator is wired into `AppHost.cs` as shipped.
|
|
314
|
+
Bus emulator is wired into `AppHost.cs` as shipped. `<YourApp>.AppHost/Configs/busConfig.json` exists
|
|
308
315
|
and is copied to the build output, but nothing in `AppHost.cs` references it — it's a config file
|
|
309
316
|
waiting for an emulator resource, not something a consumer touches to run the app today.
|
|
310
317
|
|
|
@@ -314,7 +321,7 @@ DKNet also carries an `Aspire.Hosting.ServiceBus` project that runs the emulator
|
|
|
314
321
|
|
|
315
322
|
## Testing events
|
|
316
323
|
|
|
317
|
-
**BDD — log-capture pattern.**
|
|
324
|
+
**BDD — log-capture pattern.** `<YourApp>.App.TestSupport/TestLogCapture.cs` is an `ILoggerProvider`
|
|
318
325
|
that queues every formatted log line into an in-memory collection, registered as an additional
|
|
319
326
|
provider alongside the host's normal logging. A scenario asserts on the resulting text instead of on
|
|
320
327
|
internal call order:
|
|
@@ -381,15 +388,15 @@ that path as untested until you add integration coverage against a real namespac
|
|
|
381
388
|
- **What you might expect:** an `[RaisesEvent(EventOperations.Updated, ...)]` fires on every call to
|
|
382
389
|
the method that touches that property. **What actually happens:** it only fires when the value
|
|
383
390
|
actually changed on that save — see `dknet-entity` for the mechanics.
|
|
384
|
-
- **What you might expect:** placing a new event consumer in
|
|
385
|
-
the others. **What actually happens:** discovery only scans the
|
|
386
|
-
(internal) and the
|
|
387
|
-
|
|
391
|
+
- **What you might expect:** placing a new event consumer in `<YourApp>.Api` gets it discovered like
|
|
392
|
+
the others. **What actually happens:** discovery only scans the `<YourApp>.AppServices` assembly
|
|
393
|
+
(internal) and the `<YourApp>.Infra` assembly (external, inside `AddAzureBus`). A consumer in
|
|
394
|
+
`<YourApp>.Api` is never registered.
|
|
388
395
|
- **What you might expect:** setting `EnableServiceBus: true` is enough to start producing to Azure.
|
|
389
396
|
**What actually happens:** `ConnectionStrings:AzureBus` must also be a non-empty string. Either one
|
|
390
397
|
missing and the `AzureBus` child bus, and everything registered only on it, silently does not exist
|
|
391
398
|
— no error, no log, just no external traffic.
|
|
392
399
|
- **What you might expect:** an external consumer belongs next to the internal one, in
|
|
393
|
-
|
|
394
|
-
belong in
|
|
400
|
+
`<YourApp>.AppServices/<Feature>/V1/Events/`. **What actually happens:** external-system consumers
|
|
401
|
+
belong in `<YourApp>.Infra/Features/<Feature>/ExternalEvents/` — that is the assembly `AddAzureBus`
|
|
395
402
|
scans, and it keeps the external-system dependency out of `AppServices`.
|
|
@@ -5,7 +5,7 @@ description: Add DKNet's Core, EF Core, messaging/CQRS, or blob-storage NuGet pa
|
|
|
5
5
|
|
|
6
6
|
# Skill: Adopting DKNet Packages in an Existing Project
|
|
7
7
|
|
|
8
|
-
This skill is for a project that already exists with its own namespaces and folder layout — it does not assume `dotnet new dknet-minimal` was run, and never references
|
|
8
|
+
This skill is for a project that already exists with its own namespaces and folder layout — it does not assume `dotnet new dknet-minimal` was run, and never references the template's `<YourApp>.*` types. If you're scaffolding a brand-new solution from the template instead, use **dknet-project-structure** and the other `dknet-*` skills.
|
|
9
9
|
|
|
10
10
|
Each package below is independent — install only what the feature needs. All packages target **.NET 10.0+** (EF Core packages additionally need **EF Core 10.0+**); consult `Directory.Packages.props` (or your project's own central version file) before adding a version attribute per-project.
|
|
11
11
|
|
|
@@ -232,7 +232,7 @@ Swapping providers later (e.g. `.Local` in dev, `.AzureStorage` in production) o
|
|
|
232
232
|
|
|
233
233
|
- [ ] Only the packages the feature actually needs were added (no blanket "add everything")
|
|
234
234
|
- [ ] EF Core additions layer onto the existing `DbContext`/provider — no assumption of a specific database engine
|
|
235
|
-
- [ ] No
|
|
235
|
+
- [ ] No template-generated `<YourApp>.*` namespace or template folder path (`<YourApp>.Domains`, `<YourApp>.AppServices`, …) appears anywhere in the guidance followed
|
|
236
236
|
- [ ] Repositories/specs/handlers registered in DI (`AddSpecRepo`, `AddSlimBusEfCoreInterceptor`, `AddAzureStorageAdapter`, etc.) — nothing relies on auto-discovery unless the package documents it
|
|
237
237
|
- [ ] For blob storage, exactly one provider package installed alongside `Abstractions`
|
|
238
238
|
- [ ] `dotnet build` passes with the new package references
|
|
@@ -245,7 +245,7 @@ Swapping providers later (e.g. `.Local` in dev, `.AzureStorage` in production) o
|
|
|
245
245
|
| Building a dynamic predicate directly against `DbContext` without `.AsExpandable()` | Required for LinqKit to translate the expression; the `IRepositorySpec` extensions already apply it |
|
|
246
246
|
| Installing more than one blob storage provider package for the same `IBlobService` | Register exactly one — the last registration wins and the others are dead weight |
|
|
247
247
|
| Assuming a specific EF Core provider (SQL Server, Postgres, …) is required | Every package here is provider-agnostic; it only needs a working `DbContext` |
|
|
248
|
-
| Copying
|
|
248
|
+
| Copying the template's `<YourApp>.*` namespaces/paths from the template's docs | This skill — and any project using it — has its own namespaces; the template's layout doesn't apply |
|
|
249
249
|
|
|
250
250
|
## Next Steps
|
|
251
251
|
|
|
@@ -11,7 +11,7 @@ instead — this skill only covers cross-cutting platform wiring.
|
|
|
11
11
|
|
|
12
12
|
## Start-up order
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
`<YourApp>.Api/Program.cs`, in the order it actually runs:
|
|
15
15
|
|
|
16
16
|
```csharp
|
|
17
17
|
var builder = WebApplication.CreateBuilder(args);
|
|
@@ -114,7 +114,7 @@ registration (needed because `ConfigureHttpJsonOptions` has no service-provider
|
|
|
114
114
|
|
|
115
115
|
## `FeatureManagement` flags
|
|
116
116
|
|
|
117
|
-
Section name is `FeatureManagement` (`FeatureOptions.Name`), bound in
|
|
117
|
+
Section name is `FeatureManagement` (`FeatureOptions.Name`), bound in `<YourApp>.Api/Program.cs` via
|
|
118
118
|
`GetSection(FeatureOptions.Name).Get<FeatureOptions>()`. **Every JSON key must spell a
|
|
119
119
|
`FeatureOptions` property name exactly** — `Get<FeatureOptions>()` silently ignores an unknown key
|
|
120
120
|
instead of failing, so a typo no-ops rather than erroring.
|
|
@@ -131,7 +131,7 @@ instead of failing, so a typo no-ops rather than erroring.
|
|
|
131
131
|
| `EnableRateLimit` | `true` | `true` | `false` | `false` | `Configs/RateLimits/RateLimitConfig.cs` |
|
|
132
132
|
| `EnableRequestBounds` | `true` | `true` | **`false`** | — | `Configs/RequestBoundsConfig.cs` |
|
|
133
133
|
| `EnableSecurityHeaders` | `true` | `true` | **`false`** | — | `Configs/SecurityHeadersConfig.cs` |
|
|
134
|
-
| `EnableServiceBus` | `false` | `true` | `false` | — |
|
|
134
|
+
| `EnableServiceBus` | `false` | `true` | `false` | — | `<YourApp>.Infra/Extensions/ServiceBusSetup.cs` (Azure child bus only) |
|
|
135
135
|
| `EnableSwagger` | `false` | `false` | `true` | — | `Configs/Swagger/SwaggerConfig.cs` |
|
|
136
136
|
| `EnableVersioning` | `true` | `true` | — | — | `Configs/VersioningConfig.cs` |
|
|
137
137
|
| `RequireAuthorization` | `false` | **`true`** | `false` | `false` | `Configs/Auth/AuthConfig.cs` |
|
|
@@ -149,7 +149,7 @@ alone does nothing — the Azure child bus is added only when the flag is `true`
|
|
|
149
149
|
`ConnectionStrings:AzureBus` is non-empty; the in-memory child bus that carries internal
|
|
150
150
|
command/event dispatch is unconditional.
|
|
151
151
|
|
|
152
|
-
**Adding a flag**: add the `bool` property to
|
|
152
|
+
**Adding a flag**: add the `bool` property to `<YourApp>.Share/Options/FeatureOptions.cs`, add the
|
|
153
153
|
same-spelled key to every `appsettings*.json` that needs a non-default value, and consume it either
|
|
154
154
|
as `features.YourFlag` inside `AppConfig.cs`/`ServiceConfigs.cs` (both already receive a
|
|
155
155
|
`FeatureOptions features` parameter) or via `IOptions<FeatureOptions>` injected anywhere else in DI.
|
|
@@ -158,7 +158,7 @@ as `features.YourFlag` inside `AppConfig.cs`/`ServiceConfigs.cs` (both already r
|
|
|
158
158
|
|
|
159
159
|
| Section | Keys | Shipped default | Reads |
|
|
160
160
|
|---|---|---|---|
|
|
161
|
-
| `ConnectionStrings` | `AppDb`, `Redis`, `AzureBus`, `AzureAppConfig` | all `""` except overlays | `AppDb` →
|
|
161
|
+
| `ConnectionStrings` | `AppDb`, `Redis`, `AzureBus`, `AzureAppConfig` | all `""` except overlays | `AppDb` → `<YourApp>.Infra/Extensions/InfraSetup.cs`, `DbMigration.cs`; `Redis` → `CacheConfig.cs`, `AppConfig.cs` (idempotency store); `AzureBus` → `ServiceBusSetup.cs`; `AzureAppConfig` → `AzureAppConfigSetup.cs` |
|
|
162
162
|
| `Authentication:Schemes:Bearer` | `MetadataAddress`, `ValidAudiences`, `ValidIssuer` | placeholder tenant/audience | bound by ASP.NET Core's own `AddJwtBearer()`; registered only when `RequireAuthorization` is on |
|
|
163
163
|
| `Cors` | `AllowedOrigins` (`[]`), `AllowedMethods` (`GET,POST,PUT,PATCH`), `AllowedHeaders` (`Authorization,Content-Type,Accept,X-Idempotency-Key`) | empty origins ⇒ CORS not wired at all | `Configs/CrosConfig.cs` |
|
|
164
164
|
| `Security` | `TrustedProxies` (IP list), `TrustedNetworks` (CIDR list) | both `[]` | `Configs/ForwardedHeadersConfig.cs` — both empty ⇒ `ForwardedHeaders.None` |
|
|
@@ -167,10 +167,10 @@ as `features.YourFlag` inside `AppConfig.cs`/`ServiceConfigs.cs` (both already r
|
|
|
167
167
|
| `RateLimit` | `DefaultRequestLimit`, `DefaultConcurrentLimit`, `TimeWindowInSeconds` | class default `2/2/1s`; base file `100/20/1s`; Development `1/1/10s` | `Configs/RateLimits/RateLimitConfig.cs` |
|
|
168
168
|
| `AzureAppConfiguration` | `KeyPrefix`, `Label`, `CacheExpirationInSeconds`, `LoadFeatureFlags`, `FeatureFlagPrefix` | ships in base file | **dead** — see below |
|
|
169
169
|
| `AzureAppConfig` | `ConnectionStringName` (`AzureAppConfig`), `Label` (`null`→`SharedConsts.ApiName`), `LoadFeatureFlags`, `FeatureFlagPrefix`, `RefreshIntervalInMinutes` | not shipped in base file | `Configs/AzureAppConfig/AzureAppConfigSetup.cs` — the last three properties are declared but never read; refresh is hard-coded to 30 minutes |
|
|
170
|
-
| `OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_SERVICE_NAME` | flat keys, not a section | `http://localhost:4317`;
|
|
170
|
+
| `OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_SERVICE_NAME` | flat keys, not a section | `http://localhost:4317`; `<YourApp>.Api` | `Configs/LogConfigs.cs` reads only the endpoint key as a presence check; `OTEL_SERVICE_NAME` is never read by template code |
|
|
171
171
|
| `AzureMonitor:ConnectionString` | — | `""` | `Configs/LogConfigs.cs` — non-blank adds `UseAzureMonitor()` |
|
|
172
172
|
| `DKNet:ListQuery` | `DefaultPageSize`, `MaxPageSize`, `DefaultActivityWindowMonths` | package defaults (1000/1000/3) | consumed by the generic `MapGetList<TEntity,TKey,TDto>` route |
|
|
173
|
-
| `SampleData:RecordsPerEntity` | — | `10000` |
|
|
173
|
+
| `SampleData:RecordsPerEntity` | — | `10000` | `<YourApp>.AppHost/appsettings.json`, read by `AppHost.cs` only — never present in the API |
|
|
174
174
|
|
|
175
175
|
`AzureAppConfiguration` (note the longer name) is a real trap: it ships in the base
|
|
176
176
|
`appsettings.json` but binds to nothing — `AzureAppConfigOptions.Name` is `"AzureAppConfig"`, a
|
|
@@ -265,7 +265,7 @@ option nor an option's value; `JobRegistry.Jobs` maps recognized names (case-ins
|
|
|
265
265
|
message-bus connection opened.
|
|
266
266
|
|
|
267
267
|
```bash
|
|
268
|
-
dotnet run --project ApiEndpoints
|
|
268
|
+
dotnet run --project ApiEndpoints/<YourApp>.Api -- migration
|
|
269
269
|
```
|
|
270
270
|
|
|
271
271
|
`RunDbMigrationWhenAppStart` is the in-process alternative: same `MigrationJob.RunAsync` call, made
|
|
@@ -277,13 +277,13 @@ Keep a job as small as `MigrationJob`: it receives the builder as it stood right
|
|
|
277
277
|
`AddLogConfig`, with no DI container built yet, so it constructs what it needs directly from
|
|
278
278
|
`builder.Configuration`/`builder.Services.BuildServiceProvider()` rather than resolving from a host.
|
|
279
279
|
|
|
280
|
-
## Aspire (
|
|
280
|
+
## Aspire (`<YourApp>.AppHost`)
|
|
281
281
|
|
|
282
282
|
`AppHost.cs` provisions `Redis` and `Postgres` (with an `AppDb` database), starts the `Api` project
|
|
283
|
-
by path (
|
|
283
|
+
by path (`../<YourApp>.Api/<YourApp>.Api.csproj` — a literal path string, not `AddProject<T>`, so it
|
|
284
284
|
survives `sourceName` rewriting even for a dotted name), and calls `.WaitFor(cache).WaitFor(apDb)`.
|
|
285
285
|
Azure Service Bus is **not** wired — `.WaitFor(bus)` is commented out in source; only Redis and
|
|
286
|
-
PostgreSQL resources exist.
|
|
286
|
+
PostgreSQL resources exist. `<YourApp>.AppHost/Configs/busConfig.json` (an Azure Service Bus emulator
|
|
287
287
|
topology file for the `product-tp`/`product-sub` topic) sits in the project but nothing in
|
|
288
288
|
`AppHost.cs` references it — it is not currently wired to any resource.
|
|
289
289
|
|
|
@@ -297,15 +297,15 @@ role-gated filtering is visible on a freshly started host.
|
|
|
297
297
|
|
|
298
298
|
```bash
|
|
299
299
|
# Full stack — Redis + PostgreSQL via Docker, sample data generated
|
|
300
|
-
dotnet run --project ApiEndpoints
|
|
300
|
+
dotnet run --project ApiEndpoints/<YourApp>.AppHost
|
|
301
301
|
|
|
302
302
|
# API only — no containers, needs ConnectionStrings:AppDb supplied yourself
|
|
303
|
-
dotnet run --project ApiEndpoints
|
|
303
|
+
dotnet run --project ApiEndpoints/<YourApp>.Api
|
|
304
304
|
```
|
|
305
305
|
|
|
306
306
|
## Test hosts
|
|
307
307
|
|
|
308
|
-
|
|
308
|
+
`<YourApp>.App.TestSupport/TestApiFactoryBase.cs` is the shared `WebApplicationFactory<Program>` both
|
|
309
309
|
xUnit and BDD suites subclass. It always: `UseEnvironment("Testing")`; pushes
|
|
310
310
|
`FeatureManagement:RunDbMigrationWhenAppStart=false`, `EnableSwagger=false`,
|
|
311
311
|
`EnableAzureAppConfig=false`, `ConnectionStrings:AppDb=UseInMemory` through
|