@drunkcoding/dknet-implementation-skills 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/.claude-plugin/marketplace.json +22 -0
  2. package/.claude-plugin/plugin.json +38 -0
  3. package/LICENSE +21 -0
  4. package/README.md +261 -0
  5. package/agents/dknet-architect.md +49 -0
  6. package/agents/dknet-bdd-engineer.md +62 -0
  7. package/agents/dknet-implementer.md +75 -0
  8. package/package.json +52 -0
  9. package/plugin.json +26 -0
  10. package/skills/README.md +47 -0
  11. package/skills/dknet-auth-and-ownership/SKILL.md +418 -0
  12. package/skills/dknet-bdd-tests/SKILL.md +355 -0
  13. package/skills/dknet-bdd-tests/checklist.md +39 -0
  14. package/skills/dknet-crud/SKILL.md +483 -0
  15. package/skills/dknet-ddd-principles/SKILL.md +87 -0
  16. package/skills/dknet-docs/SKILL.md +296 -0
  17. package/skills/dknet-docs/checklist.md +58 -0
  18. package/skills/dknet-docs/templates/README-template.md +68 -0
  19. package/skills/dknet-docs/templates/api-reference-template.md +275 -0
  20. package/skills/dknet-docs/templates/architecture-template.md +166 -0
  21. package/skills/dknet-docs/templates/data-model-template.md +99 -0
  22. package/skills/dknet-docs/templates/events-template.md +155 -0
  23. package/skills/dknet-dto-mapping/SKILL.md +278 -0
  24. package/skills/dknet-efcore-config/SKILL.md +379 -0
  25. package/skills/dknet-endpoint/SKILL.md +458 -0
  26. package/skills/dknet-entity/SKILL.md +483 -0
  27. package/skills/dknet-feature/SKILL.md +139 -0
  28. package/skills/dknet-feature-lifecycle/SKILL.md +144 -0
  29. package/skills/dknet-feature-remove/SKILL.md +131 -0
  30. package/skills/dknet-messaging-events/SKILL.md +395 -0
  31. package/skills/dknet-package-adoption/SKILL.md +252 -0
  32. package/skills/dknet-platform-config/SKILL.md +342 -0
  33. package/skills/dknet-project-structure/SKILL.md +148 -0
  34. package/skills/dknet-queries-specs/SKILL.md +330 -0
  35. package/skills/dknet-scaffold/SKILL.md +209 -0
  36. package/skills/dknet-unit-tests/SKILL.md +382 -0
@@ -0,0 +1,342 @@
1
+ ---
2
+ name: dknet-platform-config
3
+ description: 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.
4
+ ---
5
+
6
+ # DKNet platform configuration
7
+
8
+ Everything a generated solution wires before a business feature exists. For a feature's own
9
+ vertical slice (entity → endpoint), use `dknet-project-structure` and `dknet-feature-lifecycle`
10
+ instead — this skill only covers cross-cutting platform wiring.
11
+
12
+ ## Start-up order
13
+
14
+ `Minimal.Api/Program.cs`, in the order it actually runs:
15
+
16
+ ```csharp
17
+ var builder = WebApplication.CreateBuilder(args);
18
+ var feature = builder.Configuration.GetSection(FeatureOptions.Name).Get<FeatureOptions>() ?? new FeatureOptions();
19
+
20
+ builder.AddLogConfig(feature).AddAzureAppConfig(feature);
21
+
22
+ var jobSelection = JobSelector.Select(args, JobRegistry.Jobs.Keys.ToArray());
23
+ if (jobSelection.HasJobName)
24
+ {
25
+ if (!jobSelection.IsRecognized) { /* print known jobs, exit 1 */ }
26
+ return await JobRegistry.Jobs[jobSelection.RequestedJobName!](builder);
27
+ }
28
+
29
+ builder.AddFluentValidationConfig();
30
+ await builder.RunMigrationAsync(feature); // only if RunDbMigrationWhenAppStart
31
+
32
+ builder.Services
33
+ .AddOptions(builder.Configuration)
34
+ .AddAppConfig(feature, builder.Configuration)
35
+ .AddContextualRequestPopulation(); // populates [FromClaim] members
36
+
37
+ await builder.Build().UseAppConfig(a => a.UseEndpointConfigs(o =>
38
+ {
39
+ o.RequireAuthorization = feature.RequireAuthorization;
40
+ o.EnableVersioning = feature.EnableVersioning;
41
+ o.ConfigureGroup = (group, _) => group.AddFluentValidationAutoValidation();
42
+ }, typeof(Program).Assembly));
43
+ ```
44
+
45
+ Why this order matters:
46
+
47
+ - `AddLogConfig` and `AddAzureAppConfig` run **before** the job-name check, so a job (the
48
+ `migration` job included) resolves configuration from exactly the same sources — Azure App
49
+ Configuration included — as the serving path.
50
+ - Job dispatch happens **before any service is registered** — a job binds no HTTP listener and
51
+ opens no message-bus connection. See [Launch-time jobs](#launch-time-jobs).
52
+ - `FeatureOptions` is bound once, at the very top, from `builder.Configuration` as it stood at that
53
+ instant. A `WebApplicationFactory`'s `ConfigureAppConfiguration` override merges its settings
54
+ later — too late to change a flag. Only an `appsettings.{Environment}.json` file or a
55
+ `FeatureManagement__<Flag>` environment variable lands early enough. This is why
56
+ `TestApiFactoryBase` (below) pushes overrides through `ConfigureAppConfiguration` with an
57
+ in-memory collection, not through `ConfigureServices`.
58
+ - `RunMigrationAsync` runs after validation config but before any request-serving service is
59
+ registered, so a failed migration (`RunDbMigrationWhenAppStart`) throws and the process never
60
+ starts serving with an un-migrated schema.
61
+
62
+ `AppConfig.AddAppConfig` (service registration) and `AppConfig.UseAppConfig` (middleware) are the
63
+ two composition points everything else in this skill hangs off:
64
+
65
+ ```csharp
66
+ // AddAppConfig — service registration order
67
+ if (EnableAntiforgery) AddAntiforgeryConfig();
68
+ if (RequireAuthorization && EnableDemoAuthentication) throw new InvalidOperationException(...); // mutually exclusive
69
+ if (RequireAuthorization) AddAuthConfig(); else if (EnableDemoAuthentication) AddDemoAuthConfig();
70
+ if (EnableSwagger) AddOpenApiDoc();
71
+ if (EnableHttps) AddHttpsConfig(configuration);
72
+ if (EnableRateLimit) AddRateLimitConfig(configuration);
73
+ if (EnableVersioning) AddAppVersioning();
74
+ AddForwardedHeadersConfig(features, configuration).AddSecurityHeadersConfig(features).AddRequestBoundsConfig(features, configuration);
75
+ AddHttpContextAccessor().AddFeatureManagement();
76
+ CacheConfig(configuration);
77
+ // Redis connection string set -> AddIdempotencyWithRedisStore, else AddIdempotentKey (in-memory)
78
+ AddCrosConfig(configuration).AddAllAppServices(configuration, features).AddHealthzConfig(features);
79
+ ```
80
+
81
+ ```csharp
82
+ // UseAppConfig — middleware order, with the reason for each position
83
+ UseAzureAppConfig() // logs that it's enabled
84
+ .UseForwardedHeadersConfig() // must rewrite RemoteIpAddress before CORS/rate-limit read it
85
+ .UseSecurityHeadersConfig() // wraps everything downstream (200/404/500 alike)
86
+ .UseAntiforgeryConfig()
87
+ .UseCrosConfig()
88
+ .UseHttpsConfig()
89
+ .UseHealthzConfig();
90
+ UseRouting();
91
+ UseRequestBoundsConfig();
92
+ UseRateLimitConfig();
93
+ UseAuthConfig(); // must be after UseRouting
94
+ // -- UseEndpointConfigs runs here (extra?.Invoke(app)) --
95
+ UseOpenApiDoc(); // must be after UseEndpoints
96
+ ```
97
+
98
+ Every `Add*Config`/`Use*Config` pair follows the same shape: `Add*` calls
99
+ `services.MarkConfigAdded(nameof(XConfig))` (a keyed singleton scoped to that host's own
100
+ `IServiceProvider` — see `HostConfigMarker`), and `Use*` checks
101
+ `app.Services.IsConfigAdded(nameof(XConfig))` before doing anything. This is why two
102
+ `WebApplicationFactory` instances built in the same test process never leak each other's feature
103
+ state, and why a flag that's off means the corresponding middleware is never added, not merely
104
+ skipped at request time.
105
+
106
+ `ServiceConfigs.AddAllAppServices` (called from inside `AddAppConfig`) wires the acting-user
107
+ providers (`IPrincipalProvider` as both `IDataOwnerProvider` and `ICurrentUserProvider`, see
108
+ `dknet-auth-and-ownership`), then `AddAppServices().AddInfraServices().AddServiceBus(...)`.
109
+ `ServiceConfigs.AddOptions` (called from `Program.cs`, before `AddAppConfig`) binds
110
+ `FeatureOptions` for `IOptions<FeatureOptions>` injection, configures JSON serializer options
111
+ (naming policy, ignore conditions, converters — see `SharedConsts.JsonSerializerOptions`), and
112
+ wires role-aware sensitive-data filtering onto the same `JsonOptions` instance via a factory
113
+ registration (needed because `ConfigureHttpJsonOptions` has no service-provider access).
114
+
115
+ ## `FeatureManagement` flags
116
+
117
+ Section name is `FeatureManagement` (`FeatureOptions.Name`), bound in `Minimal.Api/Program.cs` via
118
+ `GetSection(FeatureOptions.Name).Get<FeatureOptions>()`. **Every JSON key must spell a
119
+ `FeatureOptions` property name exactly** — `Get<FeatureOptions>()` silently ignores an unknown key
120
+ instead of failing, so a typo no-ops rather than erroring.
121
+
122
+ | Flag | Class default | base `appsettings.json` | `Development` overlay | `Testing` overlay | Wires |
123
+ |---|---|---|---|---|---|
124
+ | `EnableAntiforgery` | `false` | `false` | `false` | — | `Configs/Antiforgery/AntiforgeryConfig.cs` |
125
+ | `EnableAzureAppConfig` | `false` | `false` | `false` | — | `Configs/AzureAppConfig/AzureAppConfigSetup.cs` |
126
+ | `EnableDemoAuthentication` | `false` | — (false) | **`true`** | **`true`** | `Configs/Auth/DemoAuthConfig.cs` |
127
+ | `EnableForwardedHeaders` | `true` | `true` | **`false`** | — | `Configs/ForwardedHeadersConfig.cs` |
128
+ | `EnableHealthCheck` | `true` | — | — | — | `Configs/Healthz/HealthzConfig.cs` |
129
+ | `EnableHttps` | `false` | **`true`** | `false` | `false` | `Configs/HttpsConfig.cs` |
130
+ | `EnableOpenTelemetry` | `false` | `false` | — | — | `Configs/LogConfigs.cs` |
131
+ | `EnableRateLimit` | `true` | `true` | `false` | `false` | `Configs/RateLimits/RateLimitConfig.cs` |
132
+ | `EnableRequestBounds` | `true` | `true` | **`false`** | — | `Configs/RequestBoundsConfig.cs` |
133
+ | `EnableSecurityHeaders` | `true` | `true` | **`false`** | — | `Configs/SecurityHeadersConfig.cs` |
134
+ | `EnableServiceBus` | `false` | `true` | `false` | — | `Minimal.Infra/Extensions/ServiceBusSetup.cs` (Azure child bus only) |
135
+ | `EnableSwagger` | `false` | `false` | `true` | — | `Configs/Swagger/SwaggerConfig.cs` |
136
+ | `EnableVersioning` | `true` | `true` | — | — | `Configs/VersioningConfig.cs` |
137
+ | `RequireAuthorization` | `false` | **`true`** | `false` | `false` | `Configs/Auth/AuthConfig.cs` |
138
+ | `RunDbMigrationWhenAppStart` | `false` | `false` | `true` | — | `Configs/DbMigration.cs` |
139
+
140
+ `—` means the file doesn't name the key; the value falls through to the column on its left. The
141
+ base file is what a deployed service runs with (no `appsettings.Production.json` ships), so it
142
+ always carries the secure value; relaxation lives in the `Development`/`Testing` overlays. Never
143
+ turn a security flag off in the base file — `SecureDefaultAppSettingsTests` (in the shipped test
144
+ suite) fails the build if you do.
145
+
146
+ Two mutual-exclusion rules, both enforced at start-up (`InvalidOperationException`, not a silent
147
+ pick): `RequireAuthorization` and `EnableDemoAuthentication` cannot both be `true`. `EnableServiceBus`
148
+ alone does nothing — the Azure child bus is added only when the flag is `true` **and**
149
+ `ConnectionStrings:AzureBus` is non-empty; the in-memory child bus that carries internal
150
+ command/event dispatch is unconditional.
151
+
152
+ **Adding a flag**: add the `bool` property to `Minimal.Share/Options/FeatureOptions.cs`, add the
153
+ same-spelled key to every `appsettings*.json` that needs a non-default value, and consume it either
154
+ as `features.YourFlag` inside `AppConfig.cs`/`ServiceConfigs.cs` (both already receive a
155
+ `FeatureOptions features` parameter) or via `IOptions<FeatureOptions>` injected anywhere else in DI.
156
+
157
+ ## Configuration sections
158
+
159
+ | Section | Keys | Shipped default | Reads |
160
+ |---|---|---|---|
161
+ | `ConnectionStrings` | `AppDb`, `Redis`, `AzureBus`, `AzureAppConfig` | all `""` except overlays | `AppDb` → `Minimal.Infra/Extensions/InfraSetup.cs`, `DbMigration.cs`; `Redis` → `CacheConfig.cs`, `AppConfig.cs` (idempotency store); `AzureBus` → `ServiceBusSetup.cs`; `AzureAppConfig` → `AzureAppConfigSetup.cs` |
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
+ | `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
+ | `Security` | `TrustedProxies` (IP list), `TrustedNetworks` (CIDR list) | both `[]` | `Configs/ForwardedHeadersConfig.cs` — both empty ⇒ `ForwardedHeaders.None` |
165
+ | `Https` | `HstsMaxAgeDays` | `365` | `Configs/HttpsConfig.cs` — preload only when ≥ 365 |
166
+ | `RequestBounds` | `RequestTimeoutSeconds` (30), `MaxRequestBodySizeBytes` (1048576), `RequestHeadersTimeoutSeconds` (10) | as class defaults | `Configs/RequestBoundsConfig.cs` |
167
+ | `RateLimit` | `DefaultRequestLimit`, `DefaultConcurrentLimit`, `TimeWindowInSeconds` | class default `2/2/1s`; base file `100/20/1s`; Development `1/1/10s` | `Configs/RateLimits/RateLimitConfig.cs` |
168
+ | `AzureAppConfiguration` | `KeyPrefix`, `Label`, `CacheExpirationInSeconds`, `LoadFeatureFlags`, `FeatureFlagPrefix` | ships in base file | **dead** — see below |
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`; `Minimal.Api` | `Configs/LogConfigs.cs` reads only the endpoint key as a presence check; `OTEL_SERVICE_NAME` is never read by template code |
171
+ | `AzureMonitor:ConnectionString` | — | `""` | `Configs/LogConfigs.cs` — non-blank adds `UseAzureMonitor()` |
172
+ | `DKNet:ListQuery` | `DefaultPageSize`, `MaxPageSize`, `DefaultActivityWindowMonths` | package defaults (1000/1000/3) | consumed by the generic `MapGetList<TEntity,TKey,TDto>` route |
173
+ | `SampleData:RecordsPerEntity` | — | `10000` | `Minimal.AppHost/appsettings.json`, read by `AppHost.cs` only — never present in the API |
174
+
175
+ `AzureAppConfiguration` (note the longer name) is a real trap: it ships in the base
176
+ `appsettings.json` but binds to nothing — `AzureAppConfigOptions.Name` is `"AzureAppConfig"`, a
177
+ different section name, and `KeyPrefix`/`CacheExpirationInSeconds` aren't even properties on that
178
+ options class. Setting any key under `AzureAppConfiguration` changes no behavior.
179
+
180
+ Override any key as an environment variable by replacing `:` with `__`, e.g.
181
+ `FeatureManagement__RequireAuthorization=false`. Environment variables outrank every JSON file but
182
+ lose to Azure App Configuration (added last, after `WebApplication.CreateBuilder(args)` has already
183
+ merged everything else).
184
+
185
+ ## Platform features, one at a time
186
+
187
+ - **Forwarded headers** (`Configs/ForwardedHeadersConfig.cs`) — honors `X-Forwarded-For`/`-Proto`
188
+ only from `Security:TrustedProxies` (exact `IPAddress.Parse`) or `Security:TrustedNetworks`
189
+ (`IPNetwork.Parse` CIDR ranges); both empty clears the seeded loopback trust and sets
190
+ `ForwardedHeaders.None`, not merely an unmodified default. Must run before rate limiting, which
191
+ partitions on the rewritten `RemoteIpAddress`.
192
+ - **Security headers** (`Configs/SecurityHeadersConfig.cs`, package `OwaspHeaders.Core`) — writes
193
+ the OWASP set (`X-Frame-Options`, CSP, `X-Content-Type-Options`, `Referrer-Policy`,
194
+ `Cache-Control`, `X-Permitted-Cross-Domain-Policies`, `X-XSS-Protection`,
195
+ `Cross-Origin-Resource-Policy`) via `Response.OnStarting`, so it survives `Response.Clear()` in
196
+ the exception handler — 200, 404 and unhandled 500 all carry it. `/docs` gets a relaxed CSP
197
+ (`script-src 'self' 'unsafe-inline'`) for Scalar's inline bootstrap; every other path keeps the
198
+ strict policy. `Strict-Transport-Security` is owned by `HttpsConfig`, never duplicated here.
199
+ - **Request bounds** (`Configs/RequestBoundsConfig.cs`) — sets `KestrelServerOptions.Limits.MaxRequestBodySize`,
200
+ `RequestHeadersTimeout`, and a default `RequestTimeoutPolicy` (→ `504` on expiry). Also sets
201
+ `AddServerHeader = false`, unconditionally, not configurable.
202
+ - **CORS** (`Configs/CrosConfig.cs`) — plain config array, not a `FeatureManagement` flag; empty
203
+ `AllowedOrigins` means neither `AddCors` nor `UseCors` is registered. A key present but empty
204
+ (`[]`) for methods/headers is honored as "nothing allowed," distinct from the key being absent.
205
+ - **HTTPS/HSTS** (`Configs/HttpsConfig.cs`) — always adds both `UseHsts()` and
206
+ `UseHttpsRedirection()`; the redirect only fires when ASP.NET Core can determine an HTTPS port,
207
+ which the published container image does not expose by default — set `ASPNETCORE_HTTPS_PORT` if
208
+ you need the redirect behind a TLS-terminating ingress.
209
+ - **Antiforgery** (`Configs/Antiforgery/AntiforgeryConfig.cs`) — off by default in every shipped
210
+ file; cookie-based double-submit with `SameSite=Strict`, `Secure=Always`.
211
+ - **Rate limiting** (`Configs/RateLimits/*`) — chained `PartitionedRateLimiter` (fixed-window +
212
+ concurrency), both `QueueLimit = 0` so an over-limit request is `429` immediately, never queued.
213
+ Partition key: `User.Identity.Name`, else `Connection.RemoteIpAddress` (post-forwarded-headers),
214
+ else request host (`RateLimitKeyProvider`). Both `IRateLimitKeyProvider` and
215
+ `IRateLimitOptionsProvider` are replaceable DI seams. 429 is decided before authentication runs
216
+ (`UseRateLimitConfig` sits before `UseAuthConfig` in the pipeline).
217
+ - **API versioning** (`Configs/VersioningConfig.cs`) — URL-segment versioning, default `1.0`,
218
+ `AssumeDefaultVersionWhenUnspecified = true`.
219
+ - **Health checks** (`Configs/Healthz/HealthzConfig.cs`) — `/healthz` and `/` are anonymous,
220
+ status-only (`{"status":"..."}`, no check name/duration/exception text). `/healthz/detail` is the
221
+ full per-check report and requires authorization, but only when `AuthConfig` actually wired
222
+ `UseAuthorization()` — with `RequireAuthorization` off, `/healthz/detail` is anonymous too.
223
+ `HealthCheckHandler` is a template stub that always reports healthy; replace it for a real
224
+ readiness check.
225
+ - **OpenAPI/Scalar** (`Configs/Swagger/SwaggerConfig.cs`) — document + UI at `/docs`
226
+ (`SwaggerConfig.DocsPath`), gated on `EnableSwagger`. Outside `Development`, both require
227
+ authorization independent of `EnableSwagger` — but again only enforceable while `AuthConfig` is
228
+ wired. Health routes are hand-described into the OpenAPI document (`MapHealthChecks` leaves no
229
+ `MethodInfo` for ApiExplorer to see).
230
+ - **Logging + OpenTelemetry** (`Configs/LogConfigs.cs`) — with `EnableOpenTelemetry` off (shipped
231
+ default), only `#if DEBUG` console logging is added and standard `Logging:LogLevel` filtering
232
+ applies. On, it clears providers, adds ASP.NET Core + HttpClient tracing/metrics, and the console
233
+ exporter follows the **runtime environment** (`IsDevelopment()`), not the build configuration —
234
+ a `Release` build run as `Development` still gets console traces. OTLP and Azure Monitor exporters
235
+ are additive, each gated on its own connection value being non-blank.
236
+ - **Azure App Configuration** (`Configs/AzureAppConfig/AzureAppConfigSetup.cs`) — appended to
237
+ `builder.Configuration` after every other source, so it outranks environment variables and
238
+ command-line args too. Uses `DefaultAzureCredential`; a missing/blank connection string is a
239
+ silent no-op even with the flag on.
240
+ - **Cache** (`Configs/CacheConfig.cs`) — `AddDistributedMemoryCache()` when `ConnectionStrings:Redis`
241
+ is unset, else `AddStackExchangeRedisCache`; `AddHybridCache()` is always registered on top but no
242
+ shipped feature consumes it yet.
243
+ - **Idempotency store** (in `AppConfig.AddAppConfig`) — Redis-backed
244
+ (`AddIdempotencyWithRedisStore`) when `ConnectionStrings:Redis` is set, else in-memory
245
+ (`AddIdempotentKey`), both with `ConflictHandling = IdempotentConflictHandling.ConflictResponse`
246
+ (not `CachedResult` — verify against source if a doc says otherwise). Per-route opt-in only; see
247
+ `dknet-crud` and `dknet-endpoint` for `.RequiredIdempotentKey()`.
248
+ - **JSON options** (`ServiceConfigs.AddOptions`) — naming policy, ignore conditions and converters
249
+ come from `SharedConsts.JsonSerializerOptions`; role-aware sensitive-data filtering
250
+ (`[SensitiveData]`) is wired onto the same `JsonOptions` instance via a factory registration. Full
251
+ detail in `dknet-auth-and-ownership`.
252
+ - **Error responses** (`Configs/FluentValidationConfig.cs`, `AddErrorResponses`) — one registration
253
+ answers every refusal: an error code prefixed `precondition.` (`PreconditionCodes.Prefix`) → `409`;
254
+ an unhandled `OwnershipRequiredException` → `403`; everything else keeps its existing status. Full
255
+ detail in `dknet-crud`.
256
+ - **Auth and ownership** — see `dknet-auth-and-ownership` for `AuthConfig`/`DemoAuthConfig`, scope
257
+ policies, `[FromClaim]`, and `IDataOwnerProvider`/`ICurrentUserProvider`.
258
+
259
+ ## Launch-time jobs
260
+
261
+ One process, one entry point. `JobSelector.Select` reads the first CLI argument that is neither an
262
+ option nor an option's value; `JobRegistry.Jobs` maps recognized names (case-insensitive) to
263
+ `Func<WebApplicationBuilder, Task<int>>`. Only `"migration"` ships, running
264
+ `InfraMigration.MigrateDb` and returning `0`/`1` — no host is built, no listener bound, no
265
+ message-bus connection opened.
266
+
267
+ ```bash
268
+ dotnet run --project ApiEndpoints/Minimal.Api -- migration
269
+ ```
270
+
271
+ `RunDbMigrationWhenAppStart` is the in-process alternative: same `MigrationJob.RunAsync` call, made
272
+ from `Program.cs` before serving, in every environment and build configuration. Use it for a single
273
+ replica; use the job for anything with more than one replica racing the same migration.
274
+
275
+ Adding a job is one entry in `JobRegistry.Jobs` — no new branch in `Program.cs`, no new project.
276
+ Keep a job as small as `MigrationJob`: it receives the builder as it stood right after
277
+ `AddLogConfig`, with no DI container built yet, so it constructs what it needs directly from
278
+ `builder.Configuration`/`builder.Services.BuildServiceProvider()` rather than resolving from a host.
279
+
280
+ ## Aspire (`Minimal.AppHost`)
281
+
282
+ `AppHost.cs` provisions `Redis` and `Postgres` (with an `AppDb` database), starts the `Api` project
283
+ by path (`../Minimal.Api/Minimal.Api.csproj` — a literal path string, not `AddProject<T>`, so it
284
+ survives `sourceName` rewriting even for a dotted name), and calls `.WaitFor(cache).WaitFor(apDb)`.
285
+ Azure Service Bus is **not** wired — `.WaitFor(bus)` is commented out in source; only Redis and
286
+ PostgreSQL resources exist. `Minimal.AppHost/Configs/busConfig.json` (an Azure Service Bus emulator
287
+ topology file for the `product-tp`/`product-sub` topic) sits in the project but nothing in
288
+ `AppHost.cs` references it — it is not currently wired to any resource.
289
+
290
+ After resources are up, `SampleDataGenerator.RunAsync` (subscribed on
291
+ `AfterResourcesCreatedEvent`) populates `Product` and `PurchaseOrder` with
292
+ `SampleData:RecordsPerEntity` records each (default `10000`; `0` or negative skips generation
293
+ entirely). It polls up to 60s for the migration-created schema, never tops up a database that
294
+ already holds rows, and generates one fixed demonstration product
295
+ (`Demo-Product-With-Supplier-Data`) carrying both `[SensitiveData]` supplier properties so
296
+ role-gated filtering is visible on a freshly started host.
297
+
298
+ ```bash
299
+ # Full stack — Redis + PostgreSQL via Docker, sample data generated
300
+ dotnet run --project ApiEndpoints/Minimal.AppHost
301
+
302
+ # API only — no containers, needs ConnectionStrings:AppDb supplied yourself
303
+ dotnet run --project ApiEndpoints/Minimal.Api
304
+ ```
305
+
306
+ ## Test hosts
307
+
308
+ `Minimal.App.TestSupport/TestApiFactoryBase.cs` is the shared `WebApplicationFactory<Program>` both
309
+ xUnit and BDD suites subclass. It always: `UseEnvironment("Testing")`; pushes
310
+ `FeatureManagement:RunDbMigrationWhenAppStart=false`, `EnableSwagger=false`,
311
+ `EnableAzureAppConfig=false`, `ConnectionStrings:AppDb=UseInMemory` through
312
+ `ConfigureAppConfiguration` (early enough to beat the `Program.cs` bind — see
313
+ [Start-up order](#start-up-order)); swaps `CoreDbContext` onto `AddDbContextWithHook` +
314
+ `UseInMemoryDatabase` (not plain `AddDbContext`, which would silently drop the DKNet events hook);
315
+ and substitutes `IMembershipService` with `TestMembershipService`.
316
+
317
+ Two `protected virtual` seams: `AddFeatureOverrides(IDictionary<string,string?>)` to extend the
318
+ config-override set for one suite, and `ConfigureTestServices(IServiceCollection)` (call
319
+ `base.ConfigureTestServices` first) to swap further services. Combined with the `Testing` overlay
320
+ (`RequireAuthorization=false`, `EnableDemoAuthentication=true`, `EnableHttps=false`,
321
+ `EnableRateLimit=false`), the effective test host runs with authorization off, the demo identity
322
+ authenticated on every call, no HTTPS redirect, and no rate limiting — a business test that needs a
323
+ different combination adds its own fixture subclass rather than changing these defaults.
324
+
325
+ ## Common mistakes
326
+
327
+ - **What you might expect**: setting a `FeatureManagement` flag through a `WebApplicationFactory`'s
328
+ `ConfigureAppConfiguration` in-memory collection changes behavior in a test.
329
+ **What actually happens**: it's ignored. **Why**: `Program.cs` binds `FeatureOptions` before a
330
+ factory's overrides are merged; only an `appsettings.{Environment}.json` file or a
331
+ `FeatureManagement__<Flag>` environment variable lands early enough.
332
+ - **What you might expect**: `AzureAppConfiguration:*` keys in `appsettings.json` configure the
333
+ Azure App Configuration integration. **What actually happens**: nothing — that section name binds
334
+ to nothing; the real section is `AzureAppConfig` (no trailing "-uration").
335
+ - **What you might expect**: turning `EnableServiceBus` off disables the message bus.
336
+ **What actually happens**: only the Azure child bus goes away; the in-memory child bus that
337
+ carries every internal command/event/query still runs.
338
+ - **What you might expect**: a validator or `[Range]` attribute on a request is what determines
339
+ `400` vs `201`. **What actually happens for platform wiring specifically**: FluentValidation runs
340
+ on every endpoint group via `AddFluentValidationAutoValidation()` in `ConfigureGroup` — that part
341
+ always works. What silently doesn't work is a DataAnnotations attribute on a **generated** CRUD
342
+ request; see `dknet-crud` and `dknet-endpoint` for the enforcement gap.
@@ -0,0 +1,148 @@
1
+ ---
2
+ name: dknet-project-structure
3
+ description: 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.
4
+ ---
5
+
6
+ # DKNet project structure
7
+
8
+ Read this before touching any layer. `AGENTS.md` at the solution root carries a condensed version
9
+ of this same map — read it too if you're unsure which project a file belongs in.
10
+
11
+ ## Layer boundaries and dependency direction
12
+
13
+ ```
14
+ Minimal.Api → entry point, endpoints, auth, OpenAPI
15
+ ↓
16
+ Minimal.AppServices → CQRS handlers, validators, DTOs, domain event handlers
17
+ ↓
18
+ Minimal.Domains → entities, aggregate roots, domain service contracts
19
+ ↑
20
+ Minimal.Infra → EF Core (CoreDbContext), repos, event publisher, service bus
21
+ (wires into Api via InfraSetup.AddInfraServices)
22
+
23
+ Minimal.Share → shared constants/options/base types (read by all layers)
24
+ Minimal.AppHost → Aspire orchestration only (Redis + PostgreSQL + Minimal.Api), no business logic
25
+ ```
26
+
27
+ Every project reference points inward: `Minimal.Domains` references only `Minimal.Share`;
28
+ `Minimal.AppServices` depends only on `Domains` (+ `Share`); `Minimal.Infra` and `Minimal.Api`
29
+ depend on both, never the reverse. An outward reference (`Domains` → `AppServices`, say) is a
30
+ circular project reference MSBuild refuses outright — the compiler holds this boundary, not a test.
31
+
32
+ ## The six feature-carrying projects
33
+
34
+ | Project | Owns |
35
+ |---|---|
36
+ | `Minimal.Domains` | Entities, aggregate roots, owned types, domain service **contracts** (`IDomainService`) |
37
+ | `Minimal.Infra` | EF Core mappers, seed data, repositories, `EventPublisher`, service-bus topology, domain service **implementations** |
38
+ | `Minimal.AppServices` | Command/query requests, handlers, FluentValidation validators, specs, DTOs, domain-event consumers |
39
+ | `Minimal.Api` | `IEndpointConfig` route groups, auth policies, platform `Configs/` |
40
+ | `Minimal.Share` | Cross-cutting constants/options (`FeatureOptions`, `SharedConsts`) read by every layer |
41
+ | `Minimal.AppHost` | Aspire orchestration (Redis, PostgreSQL, sample-data generation) — no business logic |
42
+
43
+ `Minimal.App.Tests` (xUnit + Shouldly), `Minimal.App.BDDTests` (Reqnroll + NUnit) and
44
+ `Minimal.App.TestSupport` (shared `WebApplicationFactory` base) round out the solution but carry no
45
+ feature code of their own.
46
+
47
+ ## Vertical-slice folder footprint for one feature
48
+
49
+ Every business feature is a folder name (`<Feature>`) repeated across projects. Verified against
50
+ the two shipped samples, `ManualSample` and `AutomatedSample`, under `ApiEndpoints/`:
51
+
52
+ | Layer | `mode=manual` | `mode=auto` |
53
+ |---|---|---|
54
+ | Domains | `Minimal.Domains/Features/<Feature>/Entities/` — hand-written mutation + `AddEvent(...)` | same path — class-level `[RaisesEvent]`, `[CrudCreate]` ctor, `[CrudUpdate]`/`[CrudAction]` methods |
55
+ | Infra | `Minimal.Infra/Features/<Feature>/Mappers/` (`IEntityTypeConfiguration<T>`), optional `StaticData/` (seed data) | same `Mappers/` (still hand-written — no generator produces it), optional `ExternalEvents/` (broker consumers) |
56
+ | AppServices | `Minimal.AppServices/<Feature>/V1/Actions/`, `Queries/`, `Specs/`, `Events/`, `<Feature>Dto.cs` | `Minimal.AppServices/<Feature>/V1/<Feature>Dto.cs` (one `[GenerateDto]` line), `Events/` (consumers only — generator raises, doesn't consume), plus optional `Validators/` (precondition rules against a generated request), `Actions/` (any operation dropped out of the generated map), `Queries/`/`Specs/` (custom read shapes), and a Mapster `IRegister` for any hand-added DTO property |
57
+ | Api | `Minimal.Api/ApiEndpoints/<Feature>/<Entity>V1Endpoint.cs` — every route a literal `Map*` call | same file — one `group.Map<Entity>Crud(o => …)` call, plus any routes excluded from it mapped literally below |
58
+ | Tests | `Minimal.App.Tests/Unit/<Feature>/`, `Minimal.App.Tests/Integration/<Feature>/V1/` | same |
59
+ | BDD | `Minimal.App.BDDTests/Features/<Plural>/*.feature` + `Steps/*.cs` | same |
60
+
61
+ The domain/AppServices feature folder name doesn't have to match the BDD folder's plural — the two
62
+ samples happen to (`ManualSample`↔`PurchaseOrders`, `AutomatedSample`↔`Products`).
63
+
64
+ ## Auto-discovery — what gets found without registering it
65
+
66
+ | What | Found by | Scans |
67
+ |---|---|---|
68
+ | HTTP route group | `IEndpointConfig` | `UseEndpointConfigs`, assembly scan of `Minimal.Api` |
69
+ | Command/query request+handler | `Fluents.Requests.*`/`Fluents.Queries.*` | `AutoDeclareFrom`/`AddServicesFromAssembly` on the in-memory bus, scanning `Minimal.AppServices` |
70
+ | Request validator | `AbstractValidator<TRequest>` | `AddValidatorsFromAssembly(typeof(AppSetup).Assembly, includeInternalTypes: true)` |
71
+ | EF Core table mapping | `IEntityTypeConfiguration<T>` | `UseAutoConfigModel([...])` — must be wired in **both** `InfraSetup.AddInfraServices` and `InfraMigration.MigrateDb` |
72
+ | Seed data | `DataSeedingConfiguration<T>` (base class, not an interface) | `UseAutoDataSeeding([...])` — same both-places rule; wiring only one is a real bug this template hit once |
73
+ | Repos/domain services | — (not auto-discovered) | Explicit `AddScoped<IService, Service>()` line in `InfraSetup.AddInfraServices`; keep implementations `internal sealed` under `Minimal.Infra/Services/` |
74
+ | DTO mapping | `[MapsFrom(typeof(Entity))]` or `[GenerateDto(typeof(Entity))]` | Mapster `ScanMaps()` in `Minimal.AppServices/AppSetup.cs` |
75
+ | Custom Mapster config | `IRegister` | `config.Scan(assembly)`, same `AppSetup.cs` |
76
+ | Internal event consumer | `Fluents.EventsConsumers.IHandler<TEvent>` in `AppServices` | `AddServicesFromAssembly` on the in-memory child bus |
77
+ | External (broker) event consumer | same interface in `Minimal.Infra/Features/<Feature>/ExternalEvents/` | `AddServicesFromAssembly` on the Azure child bus — reached only via an explicit `azb.Produce`/`azb.Consume` pair in `ServiceBusSetup.cs` |
78
+
79
+ ## The two shipped samples
80
+
81
+ | Feature folder | Entity | Flow | Read for |
82
+ |---|---|---|---|
83
+ | `ManualSample` | `PurchaseOrder` | Hand-written every layer | Enforced DataAnnotations, idempotent create, `[FromClaim]` acting user, custom list filter |
84
+ | `AutomatedSample` | `Product` | `[CrudCreate]`/`[CrudUpdate]`/`[CrudAction]`/`[RaisesEvent]`/`[GenerateDto]` | Generated CRUD + events, composite endpoint (generated routes + hand-written ones below them) |
85
+
86
+ Full trade-off table: `dknet-feature-lifecycle` §1.
87
+
88
+ ## Commands
89
+
90
+ Run from the solution root — `dotnet build`/`dotnet test` need no explicit `.sln` path:
91
+
92
+ ```bash
93
+ dotnet build -c Release
94
+ dotnet test --settings coverage.runsettings --collect:"XPlat Code Coverage"
95
+ dotnet run --project ApiEndpoints/Minimal.Api # API only
96
+ dotnet run --project ApiEndpoints/Minimal.AppHost # Redis + PostgreSQL via Aspire
97
+
98
+ cd ApiEndpoints
99
+ dotnet ef migrations add <Name> -c CoreDbContext -p Minimal.Infra/Minimal.Infra.csproj
100
+ dotnet ef migrations remove -c CoreDbContext -p Minimal.Infra/Minimal.Infra.csproj
101
+ ```
102
+
103
+ See `dknet-scaffold` for the install/generate steps and how these project names map onto your own
104
+ solution's names.
105
+
106
+ ## Which skill for what
107
+
108
+ Reference (read, don't invoke with args):
109
+
110
+ | Skill | For |
111
+ |---|---|
112
+ | `dknet-project-structure` | This page — orientation, always read first |
113
+ | `dknet-ddd-principles` | Aggregate boundaries, entity vs. value object, when to raise a domain event |
114
+ | `dknet-feature-lifecycle` | Choosing manual vs. auto, a feature's full file footprint, add/remove order |
115
+ | `dknet-scaffold` | Installing the template, generating a solution, first run, deleting the samples |
116
+ | `dknet-entity` | Entity class mechanics (`AggregateRoot`, ctor rules, mutation methods) |
117
+ | `dknet-efcore-config` | `IEntityTypeConfiguration<T>` mappers, seed data, domain-service wiring |
118
+ | `dknet-crud` | Commands: requests, validators, handlers, both flows |
119
+ | `dknet-queries-specs` | Queries: specs, hand-written read handlers, the generic list route |
120
+ | `dknet-dto-mapping` | DTO shape, Mapster config, `[GenerateDto]` vs. hand-written record |
121
+ | `dknet-endpoint` | `IEndpointConfig`, literal routes vs. `Map<Entity>Crud()`, idempotency |
122
+ | `dknet-messaging-events` | Domain events, `[RaisesEvent]`, in-memory vs. Azure Service Bus |
123
+ | `dknet-auth-and-ownership` | Auth policies, `[FromClaim]`, `IDataOwnerProvider`/`ICurrentUserProvider` |
124
+ | `dknet-platform-config` | Everything else the template wires: start-up order, flags, config sections, jobs, Aspire, test hosts |
125
+ | `dknet-unit-tests` | xUnit + `ApiFixture` integration tests |
126
+ | `dknet-bdd-tests` | Reqnroll + NUnit scenarios |
127
+ | `dknet-docs` | Feature README + architecture diagrams |
128
+ | `dknet-package-adoption` | Adopting DKNet packages into a non-template project |
129
+
130
+ Workflow (invoke with `/`, takes arguments):
131
+
132
+ | Command | Does |
133
+ |---|---|
134
+ | `/dknet-feature <Feature> <Entity> [mode=manual\|auto] [props…]` | End-to-end slice: plan → domain → CRUD → endpoint → tests → BDD → docs |
135
+ | `/dknet-feature-remove <Feature>` | Retire a slice end-to-end, including touchpoints and a drop migration |
136
+ | `/dknet-entity`, `/dknet-crud`, `/dknet-endpoint`, `/dknet-unit-tests`, `/dknet-bdd-tests`, `/dknet-docs` | Individual phases of the same lifecycle |
137
+
138
+ Subagents (Claude Code only, used by the workflow commands): `dknet-architect` (plans),
139
+ `dknet-implementer` (writes code across layers), `dknet-bdd-engineer` (BDD scenarios).
140
+
141
+ ## Read-first ordering
142
+
143
+ 1. This skill.
144
+ 2. `dknet-ddd-principles`, if the aggregate shape or event placement isn't obvious.
145
+ 3. `dknet-feature-lifecycle` §1, to pick manual vs. auto.
146
+ 4. The layer skill for whatever you're about to write (`dknet-entity` → `dknet-efcore-config`
147
+ → `dknet-crud`/`dknet-queries-specs`/`dknet-dto-mapping` → `dknet-endpoint`).
148
+ 5. `dknet-platform-config` only when the change is cross-cutting, not feature-scoped.