@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
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# {FeatureName} — Architecture
|
|
2
2
|
|
|
3
|
+
> Diagrams below are written as Mermaid so this template stays readable on its own. Render the
|
|
4
|
+
> architecture, sequence, data-flow and lifecycle diagrams with **archify**
|
|
5
|
+
> (<https://github.com/tt-a1i/archify>) and replace each fenced block with an image link to the
|
|
6
|
+
> exported SVG in `diagrams/`, keeping the archify JSON source beside it. Keep `erDiagram` as
|
|
7
|
+
> Mermaid — archify has no table-schema type. See the `dknet-docs` skill, *Diagrams*.
|
|
8
|
+
|
|
3
9
|
## Vertical Slice Overview
|
|
4
10
|
|
|
5
11
|
This feature follows the DKNet vertical slice architecture.
|
|
@@ -9,11 +15,11 @@ Each layer has a single, focused responsibility for this feature.
|
|
|
9
15
|
graph TD
|
|
10
16
|
Client["Client / Browser"]
|
|
11
17
|
|
|
12
|
-
subgraph API["
|
|
18
|
+
subgraph API["<YourApp>.Api"]
|
|
13
19
|
EP["{EntityName}V1Endpoint\n(IEndpointConfig)"]
|
|
14
20
|
end
|
|
15
21
|
|
|
16
|
-
subgraph AppServices["
|
|
22
|
+
subgraph AppServices["<YourApp>.AppServices"]
|
|
17
23
|
REQ["Request Types\n(Create / Update / Delete\n+ custom actions)"]
|
|
18
24
|
VAL["Validators\n(FluentValidation)"]
|
|
19
25
|
HDL["Command Handlers\n(IHandler)"]
|
|
@@ -21,11 +27,11 @@ graph TD
|
|
|
21
27
|
EVT["Domain Events\n({EntityName}CreatedEvent etc.)"]
|
|
22
28
|
end
|
|
23
29
|
|
|
24
|
-
subgraph Domains["
|
|
30
|
+
subgraph Domains["<YourApp>.Domains"]
|
|
25
31
|
ENT["{EntityName}\n(AggregateRoot)"]
|
|
26
32
|
end
|
|
27
33
|
|
|
28
|
-
subgraph Infra["
|
|
34
|
+
subgraph Infra["<YourApp>.Infra"]
|
|
29
35
|
MAP["{EntityName}Mapper.cs\n(EF Core Config)"]
|
|
30
36
|
REPO["IRepositorySpec\n(EF Core + Spec)"]
|
|
31
37
|
EVH["Event Handlers\n(Azure Bus / In-Memory)"]
|
|
@@ -160,7 +166,7 @@ graph LR
|
|
|
160
166
|
|
|
161
167
|
| Layer | Responsibility in this feature |
|
|
162
168
|
|-------|-------------------------------|
|
|
163
|
-
|
|
|
164
|
-
|
|
|
165
|
-
|
|
|
166
|
-
|
|
|
169
|
+
| `<YourApp>.Api` | Route mapping only; zero business logic |
|
|
170
|
+
| `<YourApp>.AppServices` | Command handling, validation, event publishing |
|
|
171
|
+
| `<YourApp>.Domains` | Entity state, domain rules, invariants |
|
|
172
|
+
| `<YourApp>.Infra` | Persistence, EF Core config, message bus setup |
|
|
@@ -50,7 +50,7 @@ erDiagram
|
|
|
50
50
|
|
|
51
51
|
## EF Core Mapping Configuration
|
|
52
52
|
|
|
53
|
-
Source: `ApiEndpoints
|
|
53
|
+
Source: `ApiEndpoints/<YourApp>.Infra/Features/{EntityFolder}/Mappers/{EntityName}Mapper.cs`
|
|
54
54
|
|
|
55
55
|
Key mapping decisions:
|
|
56
56
|
|
|
@@ -96,4 +96,4 @@ Key mapping decisions:
|
|
|
96
96
|
| `Initial_{EntityName}` | Create initial `{EntityTableName}` table |
|
|
97
97
|
| `Add_{Field}_To_{EntityName}` | {Reason for the change} |
|
|
98
98
|
|
|
99
|
-
> Keep this table updated when running `dotnet ef migrations add <Name> -c CoreDbContext -p
|
|
99
|
+
> Keep this table updated when running `dotnet ef migrations add <Name> -c CoreDbContext -p <YourApp>.Infra/<YourApp>.Infra.csproj`.
|
|
@@ -104,14 +104,14 @@ This feature does not currently consume events from other features.
|
|
|
104
104
|
|
|
105
105
|
## Event Bus Configuration
|
|
106
106
|
|
|
107
|
-
Events are dispatched via the
|
|
107
|
+
Events are dispatched via the solution's SlimMessageBus with two bus types:
|
|
108
108
|
|
|
109
109
|
| Bus Type | When Active | Purpose |
|
|
110
110
|
|----------|-------------|---------|
|
|
111
111
|
| **In-Memory** | Always (all environments) | Same-process handlers; local side effects |
|
|
112
112
|
| **Azure Service Bus** | When `ConnectionStrings:AzureBus` is non-empty | Cross-service/process messaging |
|
|
113
113
|
|
|
114
|
-
See `ApiEndpoints
|
|
114
|
+
See `ApiEndpoints/<YourApp>.Infra/Extensions/ServiceBusSetup.cs` for wiring configuration.
|
|
115
115
|
|
|
116
116
|
```mermaid
|
|
117
117
|
graph LR
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: dknet-dto-mapping
|
|
3
|
-
description: Design response DTOs and Mapster mapping for a DKNet.Minimal feature — hand-written
|
|
3
|
+
description: 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.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# DTO and Mapster mapping
|
|
@@ -8,27 +8,15 @@ description: Design response DTOs and Mapster mapping for a DKNet.Minimal featur
|
|
|
8
8
|
Response DTO shape and how it gets filled from the entity. For request contracts, validators and
|
|
9
9
|
handlers, load `dknet-crud`. For paged query projections, load `dknet-queries-specs`.
|
|
10
10
|
|
|
11
|
-
## Two DTO shapes
|
|
11
|
+
## Two DTO shapes — `[GenerateDto]` first
|
|
12
12
|
|
|
13
|
-
**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
public string CustomerName { get; init; } = null!;
|
|
21
|
-
public decimal Amount { get; init; }
|
|
22
|
-
public PurchaseOrderStatus Status { get; init; }
|
|
23
|
-
public string CreatedBy { get; init; } = null!;
|
|
24
|
-
}
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
This carries **no attribute at all**, yet `mapper.Map<PurchaseOrderDto>(order)` and
|
|
28
|
-
`mapper.ResultOf<PurchaseOrderDto>(order)` both work correctly. Mapster's global convention
|
|
29
|
-
(`TypeAdapterConfig.GlobalSettings.Default`, configured once in `AppSetup.cs`) maps any two types by
|
|
30
|
-
flexible name matching with no registration required — a hand-written DTO whose property names
|
|
31
|
-
match the entity's needs nothing further.
|
|
13
|
+
**Default to `[GenerateDto]`.** It keeps the DTO in step with the entity (a new property appears on
|
|
14
|
+
the next build), carries `[SensitiveData]`/`[MaxLength]` across from the entity for free, and is what
|
|
15
|
+
the generated CRUD and generic list routes read as their contract. Narrow it with `Exclude`/`Include`
|
|
16
|
+
rather than abandoning it, and add a computed member on your own partial declaration rather than
|
|
17
|
+
hand-writing the whole record. Hand-write a DTO only when the response must intentionally expose
|
|
18
|
+
less than the entity *and* the feature has no generated route, or when a value needs CLR-only
|
|
19
|
+
computation that could never be EF-translated.
|
|
32
20
|
|
|
33
21
|
**Generated** — one attribute on an empty `partial record`:
|
|
34
22
|
|
|
@@ -67,6 +55,26 @@ that's how `SupplierCostPrice`/`SupplierReferenceCode` stay gated per caller (se
|
|
|
67
55
|
`GrossMargin` is the file's one hand-written addition, on the *other*, non-generated partial
|
|
68
56
|
declaration — the generator's output file is never edited directly.
|
|
69
57
|
|
|
58
|
+
**Hand-written (fallback)** — a plain record, exactly the fields you list:
|
|
59
|
+
|
|
60
|
+
```csharp
|
|
61
|
+
// ManualSample/V1/PurchaseOrderDto.cs
|
|
62
|
+
public sealed record PurchaseOrderDto
|
|
63
|
+
{
|
|
64
|
+
public Guid Id { get; init; }
|
|
65
|
+
public string CustomerName { get; init; } = null!;
|
|
66
|
+
public decimal Amount { get; init; }
|
|
67
|
+
public PurchaseOrderStatus Status { get; init; }
|
|
68
|
+
public string CreatedBy { get; init; } = null!;
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
This carries **no attribute at all**, yet `mapper.Map<PurchaseOrderDto>(order)` and
|
|
73
|
+
`mapper.ResultOf<PurchaseOrderDto>(order)` both work correctly. Mapster's global convention
|
|
74
|
+
(`TypeAdapterConfig.GlobalSettings.Default`, configured once in `AppSetup.cs`) maps any two types by
|
|
75
|
+
flexible name matching with no registration required — a hand-written DTO whose property names
|
|
76
|
+
match the entity's needs nothing further.
|
|
77
|
+
|
|
70
78
|
## `[GenerateDto]` options
|
|
71
79
|
|
|
72
80
|
```csharp
|
|
@@ -96,7 +104,7 @@ real mapped columns covering the same intent.
|
|
|
96
104
|
|
|
97
105
|
## Mapster global configuration
|
|
98
106
|
|
|
99
|
-
|
|
107
|
+
`<YourApp>.AppServices/AppSetup.cs`, run once at startup:
|
|
100
108
|
|
|
101
109
|
```csharp
|
|
102
110
|
TypeAdapterConfig.GlobalSettings.Default.NameMatchingStrategy(NameMatchingStrategy.Flexible);
|
|
@@ -109,7 +117,7 @@ services.AddSingleton(TypeAdapterConfig.GlobalSettings)
|
|
|
109
117
|
.AddScoped<IMapper, ServiceMapper>();
|
|
110
118
|
```
|
|
111
119
|
|
|
112
|
-
`ScanMaps()` (
|
|
120
|
+
`ScanMaps()` (`<YourApp>.AppServices/Extensions/MapsToExtensions.cs`) reflects over the assembly for
|
|
113
121
|
every type carrying `[MapsFrom(typeof(Entity))]` or `[GenerateDto(typeof(Entity))]` and calls
|
|
114
122
|
`config.NewConfig(entityType, dtoType)` for each — this is what makes a generated/`[MapsFrom]`-typed
|
|
115
123
|
pair eagerly compiled and validated at startup, rather than resolved lazily by the `Default`
|
|
@@ -118,7 +126,7 @@ fallback rule on first use. **Ordering matters**: only *after* that loop does it
|
|
|
118
126
|
merges their `ForType` customizations onto the config the loop just built. Reversing the order would
|
|
119
127
|
have the convention's `NewConfig` wipe out the `IRegister`'s merge.
|
|
120
128
|
|
|
121
|
-
`[MapsFrom]` on a hand-written DTO (
|
|
129
|
+
`[MapsFrom]` on a hand-written DTO (`<YourApp>.AppServices.Extensions.MapsFromAttribute`) is **not
|
|
122
130
|
required for the mapping to work** — `PurchaseOrderDto` proves that with zero attributes and zero
|
|
123
131
|
registration. Reach for it only when you also want to attach a Mapster `IRegister` customization
|
|
124
132
|
(a computed property, a rename) to that specific entity/DTO pair: `[MapsFrom]` puts the hand-written
|
|
@@ -215,7 +223,7 @@ carries its paging metadata (page number, total count) forward onto the DTO-type
|
|
|
215
223
|
|
|
216
224
|
## JSON contract
|
|
217
225
|
|
|
218
|
-
|
|
226
|
+
`<YourApp>.Share/SharedConsts.JsonSerializerOptions` is the one source of truth for serialization
|
|
219
227
|
shape — camelCase property names, nulls omitted (`DefaultIgnoreCondition.WhenWritingNull`), enums as
|
|
220
228
|
camelCase strings (`JsonStringEnumConverter(JsonNamingPolicy.CamelCase)`). `ServiceConfigs.AddOptions`
|
|
221
229
|
copies these settings onto ASP.NET Core's own `JsonOptions` via `ConfigureHttpJsonOptions`, so a
|
|
@@ -236,10 +244,10 @@ change to the DTO type itself.
|
|
|
236
244
|
| Derived/computed value | Any C# expression, `AfterMapping` included | Only via a hand-written partial member + `IRegister`, and only if EF-translatable |
|
|
237
245
|
| Needs `[MapsFrom]`? | Only to attach an `IRegister` customization | N/A — `[GenerateDto]` already opts in |
|
|
238
246
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
247
|
+
Start at `[GenerateDto]` — always, when the entity carries `[CrudCreate]`/`[CrudUpdate]`, and by
|
|
248
|
+
default otherwise too. Drop to a hand-written record only when the response must intentionally expose
|
|
249
|
+
less than the entity *and* no generated route reads it, or when a value needs CLR-only computation
|
|
250
|
+
(`AfterMapping`, external lookups) that will never back a generated list route.
|
|
243
251
|
|
|
244
252
|
## Common mistakes
|
|
245
253
|
|
|
@@ -14,7 +14,7 @@ template — not `[RaisesEvent]`, not `[CrudCreate]`/`[CrudUpdate]`/`[CrudAction
|
|
|
14
14
|
|
|
15
15
|
## Mapper
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
`<YourApp>.Infra/Features/ManualSample/Mappers/PurchaseOrderConfigs.cs` — the whole file:
|
|
18
18
|
|
|
19
19
|
```csharp
|
|
20
20
|
internal sealed class PurchaseOrderConfigs : DefaultEntityTypeConfiguration<PurchaseOrder>
|
|
@@ -32,7 +32,7 @@ internal sealed class PurchaseOrderConfigs : DefaultEntityTypeConfiguration<Purc
|
|
|
32
32
|
}
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
`<YourApp>.Infra/Features/AutomatedSample/Mappers/ProductConfigs.cs` — the whole file:
|
|
36
36
|
|
|
37
37
|
```csharp
|
|
38
38
|
internal sealed class ProductConfigs : DefaultEntityTypeConfiguration<Product>
|
|
@@ -52,7 +52,7 @@ internal sealed class ProductConfigs : DefaultEntityTypeConfiguration<Product>
|
|
|
52
52
|
}
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
Location:
|
|
55
|
+
Location: `<YourApp>.Infra/Features/<Feature>/Mappers/<Entity>Configs.cs`. Discovered by
|
|
56
56
|
`UseAutoConfigModel([typeof(CoreDbContext).Assembly, typeof(Sequences).Assembly])` — an **assembly
|
|
57
57
|
scan for every `IEntityTypeConfiguration<T>`**, not a Scrutor convention scan (see Infra services,
|
|
58
58
|
below, for what Scrutor actually is and isn't used for here). This call is wired in **both**
|
|
@@ -103,7 +103,7 @@ configured where its owner is, not in a shared file.
|
|
|
103
103
|
|
|
104
104
|
## Static data seeding
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
`<YourApp>.Infra/Features/ManualSample/StaticData/PurchaseOrderStaticData.cs` — the whole file:
|
|
107
107
|
|
|
108
108
|
```csharp
|
|
109
109
|
internal sealed class PurchaseOrderStaticData : DataSeedingConfiguration<PurchaseOrder>
|
|
@@ -127,7 +127,7 @@ internal sealed class PurchaseOrderStaticData : DataSeedingConfiguration<Purchas
|
|
|
127
127
|
}
|
|
128
128
|
```
|
|
129
129
|
|
|
130
|
-
Location:
|
|
130
|
+
Location: `<YourApp>.Infra/Features/<Feature>/StaticData/<Entity>StaticData.cs`. Inherit the
|
|
131
131
|
**base class** `DataSeedingConfiguration<T>` (`DKNet.EfCore.Extensions.Configurations`) — not an
|
|
132
132
|
`IDataSeedingConfiguration<T>` interface. Override the `protected` `GetDataAsync(CancellationToken)`
|
|
133
133
|
and return the fixed rows via the entity's `internal` rehydration constructor (known `Guid`s,
|
|
@@ -149,9 +149,9 @@ first seeded.
|
|
|
149
149
|
for every `IDataSeedingConfiguration` implementer by assembly scan (same style as
|
|
150
150
|
`UseAutoConfigModel`, not Scrutor) and must be called in **both**:
|
|
151
151
|
|
|
152
|
-
-
|
|
152
|
+
- `<YourApp>.Infra/Extensions/InfraSetup.cs` → `AddInfraServices` (the DI-registered `CoreDbContext`
|
|
153
153
|
the running app uses)
|
|
154
|
-
-
|
|
154
|
+
- `<YourApp>.Infra/Extensions/InfraMigration.cs` → `MigrateDb` (a **separate** `CoreDbContext` built
|
|
155
155
|
for the startup-migration path; seeding runs as part of `db.Database.MigrateAsync()`)
|
|
156
156
|
|
|
157
157
|
This is a real bug the template hit once already: `PurchaseOrderStaticData` was correctly discovered
|
|
@@ -159,22 +159,22 @@ by the DI-path context but the migration path built its own context without the
|
|
|
159
159
|
`.UseAutoDataSeeding(...)` call, so seed rows never appeared in a real database even though the
|
|
160
160
|
migration itself ran. When adding new seed data, verify both call sites, not just one.
|
|
161
161
|
|
|
162
|
-
**No test fixture wires this.** Neither
|
|
163
|
-
|
|
162
|
+
**No test fixture wires this.** Neither `<YourApp>.App.Tests`' `ApiFixture` nor
|
|
163
|
+
`<YourApp>.App.BDDTests`' `BddApiFactory` calls `.UseAutoDataSeeding(...)` on their in-memory
|
|
164
164
|
`DbContext` — that wiring exists only in the two real composition-root call sites above. Tests that
|
|
165
165
|
actually exercise seeded data:
|
|
166
166
|
|
|
167
|
-
-
|
|
167
|
+
- `<YourApp>.App.Tests/Unit/ManualSample/PurchaseOrderStaticDataTests.cs` — invokes the `protected
|
|
168
168
|
GetDataAsync` via reflection (nothing public exposes it for a direct call) and asserts the three
|
|
169
169
|
fixed rows, owned by `SharedConsts.SystemAccount`, with distinct `Id`s.
|
|
170
|
-
-
|
|
170
|
+
- `<YourApp>.App.Tests/Integration/ManualSample/V1/InfraMigrationSeedingTests.cs` — runs
|
|
171
171
|
`InfraMigration.MigrateDb` itself against a real, ephemeral Postgres `Testcontainers` instance,
|
|
172
172
|
then reads `/v1/purchase-orders` over HTTP. This is the one test that fails if
|
|
173
173
|
`.UseAutoDataSeeding(...)` is ever removed from `InfraMigration.MigrateDb`.
|
|
174
174
|
|
|
175
175
|
## `CoreDbContext`
|
|
176
176
|
|
|
177
|
-
|
|
177
|
+
`<YourApp>.Infra/Contexts/CoreDbContext.cs` — `internal class CoreDbContext(DbContextOptions options,
|
|
178
178
|
IEnumerable<IDataOwnerProvider>? dataKeyProviders = null) : DbContext(options), IDataOwnerDbContext`.
|
|
179
179
|
No `DbSet<T>` declarations anywhere — the model is built entirely from the `IEntityTypeConfiguration<T>`
|
|
180
180
|
scan. It exposes `AccessibleKeys` from the first registered `IDataOwnerProvider`
|
|
@@ -235,7 +235,7 @@ internal static DbContextOptionsBuilder UseNpgsqlWithMigration(
|
|
|
235
235
|
.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery));
|
|
236
236
|
```
|
|
237
237
|
|
|
238
|
-
|
|
238
|
+
`<YourApp>.Infra/Extensions/InfraMigration.cs` — the startup-migration path, in full:
|
|
239
239
|
|
|
240
240
|
```csharp
|
|
241
241
|
public static async Task MigrateDb(string connectionString)
|
|
@@ -253,7 +253,7 @@ public static async Task MigrateDb(string connectionString)
|
|
|
253
253
|
```
|
|
254
254
|
|
|
255
255
|
`AddSpecRepo<CoreDbContext>()` wires `IRepositorySpec` (see `dknet-queries-specs`).
|
|
256
|
-
`AddEventPublisher<CoreDbContext, EventPublisher>()` wires
|
|
256
|
+
`AddEventPublisher<CoreDbContext, EventPublisher>()` wires `<YourApp>.Infra/Services/EventPublisher.cs`
|
|
257
257
|
— a `DefaultEventPublisher` override that does `bus.Publish(eventObj)` — as the sink both raise
|
|
258
258
|
styles (`AddEvent`/`[RaisesEvent]`) publish through after a successful save.
|
|
259
259
|
|
|
@@ -263,28 +263,28 @@ Always from the solution's `ApiEndpoints/` directory (there is no wrapper script
|
|
|
263
263
|
`dotnet ef` command):
|
|
264
264
|
|
|
265
265
|
```bash
|
|
266
|
-
dotnet ef migrations add <Name> -c CoreDbContext -p
|
|
267
|
-
dotnet ef migrations remove -c CoreDbContext -p
|
|
266
|
+
dotnet ef migrations add <Name> -c CoreDbContext -p <YourApp>.Infra/<YourApp>.Infra.csproj
|
|
267
|
+
dotnet ef migrations remove -c CoreDbContext -p <YourApp>.Infra/<YourApp>.Infra.csproj
|
|
268
268
|
```
|
|
269
269
|
|
|
270
|
-
Inspect the generated migration under
|
|
270
|
+
Inspect the generated migration under `<YourApp>.Infra/Migrations/` before continuing — confirm the
|
|
271
271
|
table, columns, and any index/constraint match what the mapper declares. Never edit an
|
|
272
272
|
already-applied migration; add a new one instead. `Architecture/MigrationSchemaTests.cs` pins
|
|
273
273
|
specific facts about the compiled model: `Product.Name` has a **unique** single-column index,
|
|
274
274
|
the model declares a sequence in schema `seq` and a sequence named `Seq_Membership` (never one named
|
|
275
|
-
after `Sequences.None`), the provider is not SQL Server, and both
|
|
276
|
-
|
|
275
|
+
after `Sequences.None`), the provider is not SQL Server, and both `<YourApp>.AppHost` and
|
|
276
|
+
`<YourApp>.Infra` reference PostgreSQL/Npgsql packages, never SQL Server ones. At application start,
|
|
277
277
|
`FeatureManagement:RunDbMigrationWhenAppStart` (or the `migration` launch argument — `dotnet run
|
|
278
|
-
--project ApiEndpoints
|
|
278
|
+
--project ApiEndpoints/<YourApp>.Api -- migration`) is what actually calls this path; removing a
|
|
279
279
|
feature's tables is a **drop migration**, covered by `dknet-feature-lifecycle`, not here.
|
|
280
280
|
|
|
281
281
|
## Infra domain services
|
|
282
282
|
|
|
283
|
-
|
|
283
|
+
`<YourApp>.Domains/Services/` holds the interface, `<YourApp>.Infra/Services/` the implementation,
|
|
284
284
|
`InfraSetup.AddInfraServices` the registration — **by explicit `AddScoped<TInterface,
|
|
285
285
|
TImplementation>()`, one line per service.** There is no Scrutor convention scan anywhere in this
|
|
286
|
-
template's
|
|
287
|
-
Mapster's `TypeAdapterConfig.Scan` in
|
|
286
|
+
template's `<YourApp>.Infra` registration path — the only `Scan(` call in the whole solution is
|
|
287
|
+
Mapster's `TypeAdapterConfig.Scan` in `<YourApp>.AppServices/Extensions/MapsToExtensions.cs`, unrelated
|
|
288
288
|
to service registration. Do not rely on a naming convention or namespace
|
|
289
289
|
(`.Services`/`.Repos`) to get a service registered — add the `AddScoped` line yourself. `internal
|
|
290
290
|
sealed` is still required by the same architecture rule that covers mappers/handlers/validators
|
|
@@ -294,7 +294,7 @@ implementation like `MembershipService` isn't targeted by that rule either, but
|
|
|
294
294
|
`internal sealed` convention regardless).
|
|
295
295
|
|
|
296
296
|
```csharp
|
|
297
|
-
//
|
|
297
|
+
// <YourApp>.Infra/Services/SequenceService.cs — internal abstract base, one per sequence-backed service
|
|
298
298
|
internal abstract class SequenceService(DbContext dbContext, Sequences sequence) : ISequenceServices
|
|
299
299
|
{
|
|
300
300
|
public virtual async ValueTask<string> NextValueAsync() =>
|
|
@@ -303,7 +303,7 @@ internal abstract class SequenceService(DbContext dbContext, Sequences sequence)
|
|
|
303
303
|
: Guid.NewGuid().ToString();
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
-
//
|
|
306
|
+
// <YourApp>.Infra/Services/MembershipService.cs — the whole file
|
|
307
307
|
internal sealed class MembershipService(CoreDbContext dbContext)
|
|
308
308
|
: SequenceService(dbContext, Sequences.Membership), IMembershipService;
|
|
309
309
|
```
|
|
@@ -311,19 +311,19 @@ internal sealed class MembershipService(CoreDbContext dbContext)
|
|
|
311
311
|
`NextValueAsync()` formats the next value with the sequence's `FormatString` on PostgreSQL
|
|
312
312
|
(`NextSeqValueWithFormat`), and falls back to a plain `Guid` when the context isn't Npgsql — so a
|
|
313
313
|
unit test against an in-memory/SQLite context never needs a real Postgres sequence. Add your own
|
|
314
|
-
service the same way: an interface in
|
|
315
|
-
implementation in
|
|
314
|
+
service the same way: an interface in `<YourApp>.Domains/Services/`, an `internal sealed`
|
|
315
|
+
implementation in `<YourApp>.Infra/Services/`, one `AddScoped` line in `AddInfraServices`.
|
|
316
316
|
|
|
317
|
-
## Architecture tests constraining
|
|
317
|
+
## Architecture tests constraining `<YourApp>.Infra`
|
|
318
318
|
|
|
319
319
|
`Architecture/InfraTests.cs`:
|
|
320
320
|
|
|
321
321
|
- `AllEfConfigClassesShouldBeInternalAndSealed` — every `IEntityTypeConfiguration<T>` implementer.
|
|
322
322
|
- `AllSeedingDataClassesShouldBeInternalAndSealed` — every `IDataSeedingConfiguration` implementer.
|
|
323
323
|
- `AllHandlerClassesShouldBeInternalAndSealed` — every `IRequestHandler<>`/`IRequestHandler<,>`/
|
|
324
|
-
`IConsumer<>` implementer (covers
|
|
324
|
+
`IConsumer<>` implementer (covers `<YourApp>.Infra/Features/*/ExternalEvents/*` consumers).
|
|
325
325
|
- `AllValidatorClassesShouldBeInternalAndSealed` — every `AbstractValidator<T>` subclass found in
|
|
326
|
-
|
|
326
|
+
`<YourApp>.Infra` (none shipped there today; validators live in `<YourApp>.AppServices`).
|
|
327
327
|
- `AllEnumProperties_StoringToDb_ShouldHaveStringConversion` — every enum property in the built
|
|
328
328
|
model must have `ProviderClrType == typeof(string)`.
|
|
329
329
|
- `NoEntityString_ShouldBe_ConfiguredAs_Max` — every mapped `string` property must have an explicit
|
|
@@ -334,34 +334,34 @@ implementation in `Minimal.Infra/Services/`, one `AddScoped` line in `AddInfraSe
|
|
|
334
334
|
|
|
335
335
|
## Step-by-step
|
|
336
336
|
|
|
337
|
-
1. Create
|
|
337
|
+
1. Create `<YourApp>.Infra/Features/<Feature>/Mappers/<Entity>Configs.cs`: `internal sealed class
|
|
338
338
|
<Entity>Configs : DefaultEntityTypeConfiguration<Entity>`, call `base.Configure(builder)` first,
|
|
339
339
|
then indexes, `HasMaxLength`/`HasPrecision` on every column, `HasConversion<string>()` on every
|
|
340
340
|
enum, `ToTable("<Plural>", "<schema>")`.
|
|
341
|
-
2. If the feature needs reference data, create
|
|
341
|
+
2. If the feature needs reference data, create `<YourApp>.Infra/Features/<Feature>/StaticData/<Entity>StaticData.cs`:
|
|
342
342
|
`internal sealed class <Entity>StaticData : DataSeedingConfiguration<Entity>`, override
|
|
343
343
|
`GetDataAsync`, build rows via the entity's `internal` rehydration constructor with fixed `Guid`s
|
|
344
344
|
and `SharedConsts.SystemAccount`.
|
|
345
|
-
3. If the feature needs a new domain service, add the interface under
|
|
346
|
-
an `internal sealed` implementation under
|
|
345
|
+
3. If the feature needs a new domain service, add the interface under `<YourApp>.Domains/Services/`,
|
|
346
|
+
an `internal sealed` implementation under `<YourApp>.Infra/Services/`, and one `AddScoped<...>()`
|
|
347
347
|
line in `InfraSetup.AddInfraServices`.
|
|
348
348
|
4. Generate and inspect the migration from `ApiEndpoints/`:
|
|
349
|
-
`dotnet ef migrations add <Name> -c CoreDbContext -p
|
|
349
|
+
`dotnet ef migrations add <Name> -c CoreDbContext -p <YourApp>.Infra/<YourApp>.Infra.csproj`.
|
|
350
350
|
5. `dotnet build -c Release` and `dotnet test --settings coverage.runsettings` from the solution
|
|
351
351
|
root.
|
|
352
352
|
|
|
353
353
|
## Validation checklist
|
|
354
354
|
|
|
355
355
|
- [ ] Mapper inherits `DefaultEntityTypeConfiguration<TEntity>` and calls `base.Configure(builder)` first
|
|
356
|
-
- [ ] Mapper class is `internal sealed`, under
|
|
356
|
+
- [ ] Mapper class is `internal sealed`, under `<YourApp>.Infra/Features/<Feature>/Mappers/`
|
|
357
357
|
- [ ] Every mapped `string` has an explicit `HasMaxLength`; every mapped enum has `HasConversion<string>()`
|
|
358
358
|
- [ ] A real business uniqueness needs `.IsUnique()` on the index, not just a validator check
|
|
359
359
|
- [ ] `ToTable("Name", "schema")` set — literal string or a `DomainSchemas` constant
|
|
360
360
|
- [ ] If seeding: class is `internal sealed`, extends `DataSeedingConfiguration<T>` (not an
|
|
361
361
|
interface), uses the entity's rehydration constructor, and `UseAutoDataSeeding(...)` is
|
|
362
362
|
present in **both** `InfraSetup.AddInfraServices` and `InfraMigration.MigrateDb`
|
|
363
|
-
- [ ] If adding a domain service: interface in
|
|
364
|
-
implementation in
|
|
363
|
+
- [ ] If adding a domain service: interface in `<YourApp>.Domains/Services/`, `internal sealed`
|
|
364
|
+
implementation in `<YourApp>.Infra/Services/`, explicit `AddScoped<...>()` line added — no
|
|
365
365
|
convention scan will pick it up on its own
|
|
366
366
|
- [ ] Migration generated from `ApiEndpoints/` and inspected before continuing
|
|
367
367
|
- [ ] `dotnet build -c Release` passes
|