@polymorphism-tech/morph-spec 6.0.0 → 7.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.
- package/README.md +367 -366
- package/docs/CHEATSHEET.md +3 -3
- package/docs/QUICKSTART.md +4 -4
- package/framework/CLAUDE.md +13 -0
- package/framework/MORPH.md +57 -36
- package/framework/agents.json +132 -127
- package/framework/commands/morph-apply.md +4 -21
- package/framework/commands/morph-archive.md +1 -1
- package/framework/commands/morph-proposal.md +2 -0
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +1 -1
- package/framework/hooks/claude-code/core/protect-spec-files.js +1 -1
- package/framework/hooks/claude-code/core/state-sync.js +212 -0
- package/framework/hooks/shared/phase-utils.js +5 -4
- package/framework/hooks/shared/state-reader.js +1 -1
- package/framework/phases.json +1 -1
- package/framework/rubrics/backend.json +41 -36
- package/framework/rules/ai-agents-standards.md +45 -15
- package/framework/rules/morph-workflow.md +15 -34
- package/framework/rules/neon-standards.md +25 -26
- package/framework/schemas/tasks.schema.json +35 -0
- package/framework/skills/core/morph-apply/SKILL.md +154 -0
- package/framework/skills/core/morph-brainstorming/SKILL.md +53 -30
- package/framework/skills/core/morph-implement/SKILL.md +25 -14
- package/framework/skills/core/morph-plan/SKILL.md +91 -23
- package/framework/skills/core/morph-proposal/SKILL.md +137 -0
- package/framework/skills/core/morph-review/SKILL.md +43 -7
- package/framework/skills/core/morph-stack-scan/SKILL.md +40 -1
- package/framework/standards/STANDARDS.json +1729 -1134
- package/framework/standards/ai-agents/agent-session.md +160 -0
- package/framework/standards/ai-agents/batching.md +193 -0
- package/framework/standards/ai-agents/context-providers.md +188 -0
- package/framework/standards/ai-agents/mcp-server.md +250 -0
- package/framework/standards/ai-agents/mcp-tools.md +197 -0
- package/framework/standards/ai-agents/middleware-patterns.md +207 -0
- package/framework/standards/ai-agents/modalities-image-gen.md +173 -0
- package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -0
- package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -0
- package/framework/standards/ai-agents/modalities-vision.md +201 -0
- package/framework/standards/ai-agents/multi-agent-patterns.md +155 -0
- package/framework/standards/ai-agents/production.md +114 -258
- package/framework/standards/ai-agents/prompts-in-db.md +214 -0
- package/framework/standards/ai-agents/providers/model-registry.md +158 -0
- package/framework/standards/ai-agents/rag-custom-pgvector.md +243 -0
- package/framework/standards/ai-agents/service-tier-flex.md +135 -0
- package/framework/standards/ai-agents/setup.md +142 -316
- package/framework/standards/ai-agents/structured-output.md +249 -0
- package/framework/standards/ai-agents/sweet-spot.md +109 -0
- package/framework/standards/ai-agents/workflows.md +114 -284
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +433 -429
- package/framework/standards/backend/api/validation.md +92 -88
- package/framework/standards/backend/database/postgresql/database.md +336 -332
- package/framework/standards/backend/database/vector-search-rag.md +599 -541
- package/framework/standards/backend/dotnet/fluent-validation-vsa.md +4 -0
- package/framework/standards/backend/dotnet/result-pattern.md +4 -0
- package/framework/standards/backend/dotnet/scalar-ui.md +176 -172
- package/framework/standards/backend/dotnet/scrutor.md +4 -0
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +278 -274
- package/framework/standards/backend/integrations/asaas/asaas-api.md +260 -216
- package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +354 -350
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +30 -19
- package/framework/standards/backend/integrations/resend/resend-email.md +400 -385
- package/framework/standards/data/nosql/cache/redis.md +106 -97
- package/framework/standards/data/nosql/cosmos-db.md +127 -118
- package/framework/standards/data/vector-search/rag-chunking.md +113 -104
- package/framework/standards/frontend/design-system/ai-image-generation.md +4 -0
- package/framework/standards/frontend/design-system/animations.md +164 -160
- package/framework/standards/frontend/design-system/naming.md +68 -64
- package/framework/standards/frontend/gsap/gsap-core.md +14 -8
- package/framework/standards/frontend/gsap/gsap-plugins.md +26 -21
- package/framework/standards/frontend/gsap/gsap-scrolltrigger.md +9 -7
- package/framework/standards/frontend/nextjs/app-router.md +127 -123
- package/framework/standards/frontend/nextjs/components.md +136 -132
- package/framework/standards/frontend/nextjs/data-fetching.md +130 -126
- package/framework/standards/frontend/nextjs/forms.md +134 -128
- package/framework/standards/frontend/nextjs/motion-patterns.md +328 -322
- package/framework/standards/frontend/nextjs/naming-conventions.md +71 -67
- package/framework/standards/frontend/nextjs/nextjs-patterns.md +229 -225
- package/framework/standards/frontend/nextjs/project-structure.md +106 -102
- package/framework/standards/frontend/nextjs/state-management.md +76 -72
- package/framework/standards/frontend/nextjs/testing.md +115 -111
- package/framework/standards/infrastructure/docker/easypanel-deploy.md +196 -192
- package/framework/standards/infrastructure/neon/neon-pgrag.md +6 -2
- package/framework/standards/infrastructure/neon/neon-pgvector.md +4 -0
- package/framework/standards/infrastructure/neon/neon-setup.md +5 -1
- package/framework/standards/integration/mcp/mcp-tools.md +471 -462
- package/framework/templates/REGISTRY.json +1745 -1239
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -0
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -0
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -0
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -0
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -0
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -0
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -0
- package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -0
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -0
- package/framework/templates/docs/onboarding.md +4 -4
- package/framework/templates/tasks.template.json +17 -0
- package/package.json +89 -83
- package/src/commands/project/init.js +16 -0
- package/src/commands/validation/validate-feature.js +1 -1
- package/src/core/paths/output-schema.js +6 -6
- package/src/core/state/state-manager.js +1 -1
- package/src/core/templates/template-registry.js +289 -256
- package/src/lib/detectors/design-system-detector.js +1 -1
- package/src/lib/generators/context-generator.js +516 -518
- package/src/lib/generators/scaffold-generator.js +23 -26
- package/src/lib/installers/memory-installer.js +138 -0
- package/src/lib/phase-chain/phase-validator.js +4 -4
- package/src/lib/risk-detector.js +80 -0
- package/src/lib/tasks/tasks-json-parser.js +185 -0
- package/src/scripts/setup-infra.js +25 -0
- package/src/utils/hooks-installer.js +59 -10
- package/framework/standards/infrastructure/neon/neon-semantic-kernel.md +0 -310
- package/framework/templates/integrations/semantic-kernel-neon.cs +0 -125
|
@@ -1,274 +1,278 @@
|
|
|
1
|
-
# VSA Handler Patterns — Complete Feature Slice Guide
|
|
2
|
-
|
|
3
|
-
> **Scope:** dotnet-vsa
|
|
4
|
-
> **Layer:** 0 (always load)
|
|
5
|
-
> **Keywords:** vsa, handler, feature-slice, vertical-slice, minimal-api, endpoint
|
|
6
|
-
> **Load When:** dotnet VSA projects
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
...
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
public sealed
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
- **`
|
|
113
|
-
- **`
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
-
|
|
154
|
-
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
Task
|
|
183
|
-
Task
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
builder.Services.
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
builder.Services.
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
|
264
|
-
|
|
265
|
-
| Use
|
|
266
|
-
|
|
|
267
|
-
|
|
|
268
|
-
|
|
|
269
|
-
| Use
|
|
270
|
-
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
1
|
+
# VSA Handler Patterns — Complete Feature Slice Guide
|
|
2
|
+
|
|
3
|
+
> **Scope:** dotnet-vsa
|
|
4
|
+
> **Layer:** 0 (always load)
|
|
5
|
+
> **Keywords:** vsa, handler, feature-slice, vertical-slice, minimal-api, endpoint
|
|
6
|
+
> **Load When:** dotnet VSA projects
|
|
7
|
+
|
|
8
|
+
**Verified against:** .NET 10 (minimal APIs, primary constructors, `Guid.CreateVersion7()`). Last-verified: 2026-05-20.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
Each feature in VSA is a self-contained slice with 5 files in a feature folder. Handlers implement `IHandler<TRequest, TResponse>` (not MediatR). Dependencies use primary constructors. The data path is `Handler -> Repository -> UnitOfWork` with no service layer in between.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## IHandler\<TRequest, TResponse\>
|
|
19
|
+
|
|
20
|
+
A single-method interface — intentionally minimal:
|
|
21
|
+
|
|
22
|
+
```csharp
|
|
23
|
+
public interface IHandler<in TRequest, TResponse>
|
|
24
|
+
{
|
|
25
|
+
Task<TResponse> HandleAsync(TRequest command, CancellationToken cancellationToken);
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Why not MediatR:** No `IRequest`/`INotification` marker interfaces, no `Send()` indirection, no service locator. Handlers are resolved directly from DI by their interface type. Decorators (validation, logging) are applied via Scrutor, not MediatR pipeline behaviors.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Feature Folder Structure (5 Files Per Slice)
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
Features/
|
|
37
|
+
BookFeature/
|
|
38
|
+
CreateBook/
|
|
39
|
+
CreateBookRequest.cs (sealed record — can be inline in handler)
|
|
40
|
+
CreateBookResponse.cs (sealed record — can be inline in handler)
|
|
41
|
+
CreateBookHandler.cs (business logic)
|
|
42
|
+
CreateBookValidator.cs (FluentValidation)
|
|
43
|
+
CreateBookEndpoint.cs (minimal API mapping)
|
|
44
|
+
GetBookById/
|
|
45
|
+
...
|
|
46
|
+
UpdateBook/
|
|
47
|
+
...
|
|
48
|
+
DeleteBook/
|
|
49
|
+
...
|
|
50
|
+
BookErrors.cs (shared error factory for the feature)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Request/Response DTOs can be defined inline in the handler file for simple slices:
|
|
54
|
+
|
|
55
|
+
```csharp
|
|
56
|
+
namespace ProjectName.Features.BookFeature.CreateBook;
|
|
57
|
+
|
|
58
|
+
// Sealed records for DTOs — immutable, structural equality
|
|
59
|
+
public sealed record CreateBookRequest(
|
|
60
|
+
string Title, string Author, string ISBN, decimal Price, int PublishedYear);
|
|
61
|
+
|
|
62
|
+
public sealed record CreateBookResponse(
|
|
63
|
+
Guid Id, string Title, string Author, string ISBN, decimal Price, int PublishedYear);
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Complete Handler Example
|
|
69
|
+
|
|
70
|
+
```csharp
|
|
71
|
+
namespace ProjectName.Features.BookFeature.CreateBook;
|
|
72
|
+
|
|
73
|
+
using ProjectName.Abstractions;
|
|
74
|
+
using ProjectName.Entities;
|
|
75
|
+
using ProjectName.Repository;
|
|
76
|
+
|
|
77
|
+
public sealed record CreateBookRequest(
|
|
78
|
+
string Title, string Author, string ISBN, decimal Price, int PublishedYear);
|
|
79
|
+
|
|
80
|
+
public sealed record CreateBookResponse(
|
|
81
|
+
Guid Id, string Title, string Author, string ISBN, decimal Price, int PublishedYear);
|
|
82
|
+
|
|
83
|
+
// Primary constructor for DI — no field declarations needed
|
|
84
|
+
public sealed class CreateBookHandler(
|
|
85
|
+
IRepository<Book> bookRepo,
|
|
86
|
+
IUnitOfWork unitOfWork) : IHandler<CreateBookRequest, Result<CreateBookResponse>>
|
|
87
|
+
{
|
|
88
|
+
public async Task<Result<CreateBookResponse>> HandleAsync(
|
|
89
|
+
CreateBookRequest command, CancellationToken cancellationToken)
|
|
90
|
+
{
|
|
91
|
+
var book = new Book
|
|
92
|
+
{
|
|
93
|
+
Id = Guid.CreateVersion7(), // .NET 9+ — time-sortable UUID
|
|
94
|
+
Title = command.Title,
|
|
95
|
+
Author = command.Author,
|
|
96
|
+
ISBN = command.ISBN,
|
|
97
|
+
Price = command.Price,
|
|
98
|
+
PublishedYear = command.PublishedYear
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
await bookRepo.AddAsync(book, cancellationToken);
|
|
102
|
+
await unitOfWork.CommitAsync(cancellationToken);
|
|
103
|
+
|
|
104
|
+
return Result.Success(new CreateBookResponse(
|
|
105
|
+
book.Id, book.Title, book.Author, book.ISBN, book.Price, book.PublishedYear));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Key Patterns
|
|
111
|
+
|
|
112
|
+
- **`sealed class`** — handlers are never inherited
|
|
113
|
+
- **`sealed record`** — DTOs are immutable value types
|
|
114
|
+
- **Primary constructors** — `(IRepository<Book> bookRepo, IUnitOfWork unitOfWork)` replaces field + constructor boilerplate
|
|
115
|
+
- **`Guid.CreateVersion7()`** — time-sortable, index-friendly UUID (not `Guid.NewGuid()`)
|
|
116
|
+
- **`CancellationToken`** — passed through every async call
|
|
117
|
+
- **`Result<T>` return** — never throw for expected failures
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Complete Endpoint Example
|
|
122
|
+
|
|
123
|
+
```csharp
|
|
124
|
+
namespace ProjectName.Features.BookFeature.CreateBook;
|
|
125
|
+
|
|
126
|
+
using ProjectName.Abstractions;
|
|
127
|
+
using ProjectName.Constants;
|
|
128
|
+
using ProjectName.Extensions;
|
|
129
|
+
|
|
130
|
+
internal sealed class CreateBookEndpoint : IApiEndpoint
|
|
131
|
+
{
|
|
132
|
+
public void MapEndpoint(IEndpointRouteBuilder app)
|
|
133
|
+
{
|
|
134
|
+
app.MapPost("books", async (
|
|
135
|
+
IHandler<CreateBookRequest, Result<CreateBookResponse>> handler,
|
|
136
|
+
CreateBookRequest command,
|
|
137
|
+
CancellationToken cancellationToken) =>
|
|
138
|
+
{
|
|
139
|
+
var result = await handler.HandleAsync(command, cancellationToken);
|
|
140
|
+
return result.Match(
|
|
141
|
+
onSuccess: () => Results.Ok(result.Value),
|
|
142
|
+
onFailure: error => Results.BadRequest(error));
|
|
143
|
+
})
|
|
144
|
+
.WithTags(ApiTags.Books)
|
|
145
|
+
.Produces<CreateBookResponse>(StatusCodes.Status200OK)
|
|
146
|
+
.Produces(StatusCodes.Status400BadRequest);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Endpoint Conventions
|
|
152
|
+
|
|
153
|
+
- **`internal sealed`** — endpoints are not public API
|
|
154
|
+
- **`IApiEndpoint`** — single interface for auto-discovery
|
|
155
|
+
- **Handler injected via DI** — resolved as `IHandler<TReq, TRes>` parameter
|
|
156
|
+
- **`Match()` for responses** — never access `.Value` directly
|
|
157
|
+
- **`.WithTags()`** — always tag for Scalar API docs
|
|
158
|
+
- **`.Produces<T>()`** — always document response types
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## IApiEndpoint Interface
|
|
163
|
+
|
|
164
|
+
```csharp
|
|
165
|
+
public interface IApiEndpoint
|
|
166
|
+
{
|
|
167
|
+
void MapEndpoint(IEndpointRouteBuilder app);
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
All endpoint classes implementing this interface are auto-discovered and mapped via `MapApiEndpoints()` in Program.cs.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Repository + UnitOfWork (No Service Layer)
|
|
176
|
+
|
|
177
|
+
Handlers call repositories directly. There is no service/manager layer:
|
|
178
|
+
|
|
179
|
+
```csharp
|
|
180
|
+
public interface IRepository<T> where T : class
|
|
181
|
+
{
|
|
182
|
+
Task<IEnumerable<T>> GetAllAsync(CancellationToken cancellationToken = default);
|
|
183
|
+
Task<T?> GetByIdAsync(object id, CancellationToken cancellationToken = default);
|
|
184
|
+
Task<T> AddAsync(T entity, CancellationToken cancellationToken = default);
|
|
185
|
+
Task UpdateAsync(T entity, CancellationToken cancellationToken = default);
|
|
186
|
+
Task DeleteAsync(T entity, CancellationToken cancellationToken = default);
|
|
187
|
+
Task AddRangeAsync(IEnumerable<T> entities, CancellationToken cancellationToken = default);
|
|
188
|
+
void RemoveRange(IEnumerable<T> entities);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
public interface IUnitOfWork
|
|
192
|
+
{
|
|
193
|
+
Task<int> CommitAsync(CancellationToken cancellationToken);
|
|
194
|
+
Task RollbackAsync(CancellationToken cancellationToken = default);
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Pattern:** `Repository.AddAsync()` -> `UnitOfWork.CommitAsync()` — always commit explicitly.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Feature Error Factory
|
|
203
|
+
|
|
204
|
+
Each feature defines its own error constants:
|
|
205
|
+
|
|
206
|
+
```csharp
|
|
207
|
+
// Features/BookFeature/BookErrors.cs
|
|
208
|
+
public static class BookErrors
|
|
209
|
+
{
|
|
210
|
+
public static Error NotFound(Guid bookId) =>
|
|
211
|
+
Error.NotFound("Book.NotFound", $"Book with ID '{bookId}' was not found.");
|
|
212
|
+
|
|
213
|
+
public static Error DuplicateIsbn(string isbn) =>
|
|
214
|
+
Error.Conflict("Book.DuplicateIsbn", $"A book with ISBN '{isbn}' already exists.");
|
|
215
|
+
|
|
216
|
+
public static readonly Error InvalidPrice =
|
|
217
|
+
Error.Failure("Book.InvalidPrice", "Book price must be greater than zero.");
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Usage in handler:
|
|
222
|
+
|
|
223
|
+
```csharp
|
|
224
|
+
var existing = await bookRepo.GetByIdAsync(command.BookId, cancellationToken);
|
|
225
|
+
if (existing is null)
|
|
226
|
+
return BookErrors.NotFound(command.BookId); // implicit conversion to Result<T>
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Program.cs DI Registration Order
|
|
232
|
+
|
|
233
|
+
```csharp
|
|
234
|
+
// 1. OpenAPI
|
|
235
|
+
builder.Services.AddOpenApi();
|
|
236
|
+
|
|
237
|
+
// 2. Infrastructure (Repository, UnitOfWork, DbContext)
|
|
238
|
+
builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
|
|
239
|
+
builder.Services.AddScoped(typeof(IUnitOfWork), typeof(UnitOfWork));
|
|
240
|
+
builder.Services.AddSQLDatabaseConfiguration(builder.Configuration);
|
|
241
|
+
|
|
242
|
+
// 3. Endpoint auto-discovery
|
|
243
|
+
builder.Services.RegisterApiEndpointsFromAssembly(Assembly.GetExecutingAssembly());
|
|
244
|
+
|
|
245
|
+
// 4. Health checks
|
|
246
|
+
builder.Services.AddHealthChecksConfiguration();
|
|
247
|
+
|
|
248
|
+
// 5. Validators (MUST be before handlers)
|
|
249
|
+
builder.Services.AddValidatorsFromAssembly(typeof(CreateBookValidator).Assembly);
|
|
250
|
+
|
|
251
|
+
// 6. Handlers + Decorators (Scrutor auto-discovery)
|
|
252
|
+
builder.Services.AddHandlersFromAssembly(typeof(Program).Assembly);
|
|
253
|
+
|
|
254
|
+
// 7. Exception handling
|
|
255
|
+
builder.Services.AddExceptionHandler<CustomExceptionHandler>()
|
|
256
|
+
.AddProblemDetails();
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## Anti-Patterns
|
|
262
|
+
|
|
263
|
+
| Do NOT | Do Instead |
|
|
264
|
+
|--------|------------|
|
|
265
|
+
| Use MediatR `IRequest`/`Send()` | Use `IHandler<TReq, TRes>` resolved directly from DI |
|
|
266
|
+
| Create a service/manager layer | Handler calls Repository + UnitOfWork directly |
|
|
267
|
+
| Use `Guid.NewGuid()` | Use `Guid.CreateVersion7()` for time-sortable IDs |
|
|
268
|
+
| Make handler classes non-sealed | Always `sealed class` |
|
|
269
|
+
| Use mutable DTOs (class with setters) | Always `sealed record` with positional parameters |
|
|
270
|
+
| Omit `CancellationToken` from async methods | Pass `CancellationToken` through every async call |
|
|
271
|
+
| Put business logic in endpoints | Endpoints only map HTTP and call `Match()` |
|
|
272
|
+
| Share handlers across features | Each feature has its own handler, even if similar |
|
|
273
|
+
| Use constructor + field for DI | Use primary constructors: `class Handler(IDep dep)` |
|
|
274
|
+
| Group by technical concern (Controllers/, Services/) | Group by feature: `Features/BookFeature/CreateBook/` |
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
*MORPH-SPEC by Polymorphism Tech*
|