@polymorphism-tech/morph-spec 7.7.2 → 7.8.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 +2 -2
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +1 -1
- package/framework/MORPH.md +3 -2
- package/framework/agents.json +1 -1
- package/framework/evals/scenarios/dotnet-crud.yaml +1 -1
- package/framework/rules/dotnet-templates.md +0 -1
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +1 -1
- package/framework/skills/core/morph-brainstorming/SKILL.md +2 -2
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +4 -3
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- package/framework/skills/core/morph-stack-scan/SKILL.md +5 -5
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/standards/STANDARDS.json +1 -1
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +34 -35
- package/framework/standards/backend/api/validation.md +2 -2
- package/framework/standards/backend/dotnet/scrutor.md +1 -3
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +42 -37
- package/framework/standards/data/nosql/cache/redis.md +2 -2
- package/framework/templates/CLAUDE.md.core.template +1 -1
- package/framework/templates/README.md +0 -1
- package/framework/templates/REGISTRY.json +0 -21
- package/framework/templates/code/dotnet/abstractions/handler.cs +0 -3
- package/framework/templates/code/dotnet/constants/api-tags.cs +1 -1
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +4 -5
- package/framework/templates/code/dotnet/feature/handler.cs.hbs +23 -24
- package/framework/templates/code/dotnet/jobs/job.cs +15 -15
- package/framework/templates/code/dotnet/test.cs +72 -101
- package/package.json +1 -1
- package/src/commands/project/doctor.js +4 -1
- package/src/commands/project/update.js +2 -0
- package/src/scripts/setup-infra.js +4 -1
- package/src/utils/banner.js +53 -37
- package/src/utils/file-copier.js +6 -1
- package/framework/templates/code/dotnet/backend/repository.cs +0 -141
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> Spec-driven development framework for multi-stack projects. Turns feature requests into implementation-ready code through structured, AI-orchestrated phases.
|
|
4
4
|
|
|
5
5
|
**Package:** `@polymorphism-tech/morph-spec`
|
|
6
|
-
**Version:** 7.
|
|
6
|
+
**Version:** 7.8.0
|
|
7
7
|
**Requires:** Node.js 22+, Claude Code
|
|
8
8
|
|
|
9
9
|
---
|
|
@@ -365,4 +365,4 @@ Code generated by morph-spec (contracts, templates, implementation output) belon
|
|
|
365
365
|
|
|
366
366
|
---
|
|
367
367
|
|
|
368
|
-
*morph-spec v7.
|
|
368
|
+
*morph-spec v7.8.0 by [Polymorphism Tech](https://polymorphism.tech)*
|
package/claude-plugin.json
CHANGED
package/docs/CHEATSHEET.md
CHANGED
package/docs/QUICKSTART.md
CHANGED
package/framework/CLAUDE.md
CHANGED
|
@@ -17,7 +17,7 @@ Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
|
|
|
17
17
|
- Modificar o contrato do plano depois do Gate 2: `spec.md`, `mandate.md`, e os campos de plano de `tasks.json` (`title`, `description`, `dependencies`, `effort`, `doneCriteria`, `group`) estão congelados.
|
|
18
18
|
- Tocar os campos de contrato do plano em `tasks.json` após o Gate 2 (`title`, `description`, `dependencies`, `effort`, `doneCriteria`, `group`) — o hook `protect-spec-files` faz diff semântico e bloqueia mudanças neles, seja via `Write` ou `Edit`. (Os campos mutáveis `status`/`outputs`/`notes` continuam graváveis por `Write` ou `Edit` — ver SEMPRE.)
|
|
19
19
|
- Listar perguntas em texto simples — sempre use a tool `AskUserQuestion` (1-4 perguntas por chamada).
|
|
20
|
-
- Usar `Guid.NewGuid()`, classes não-sealed sem justificativa, exceptions como fluxo de controle, camada de Service/Manager entre handler e dados, ou SQL cru sem justificativa.
|
|
20
|
+
- Usar `Guid.NewGuid()`, classes não-sealed sem justificativa, exceptions como fluxo de controle, camada de Service/Manager entre handler e dados, repository genérico (`IRepository<T>`/`IUnitOfWork`), ou SQL cru sem justificativa.
|
|
21
21
|
|
|
22
22
|
**SEMPRE:**
|
|
23
23
|
- Auto-advance entre tarefas durante a fase implement — não pause sem ambiguidade bloqueante.
|
package/framework/MORPH.md
CHANGED
|
@@ -48,10 +48,11 @@ Padrões proibidos automaticamente injetados no mandate:
|
|
|
48
48
|
- `Guid.NewGuid()` → `Guid.CreateVersion7()`
|
|
49
49
|
- Classes não-sealed sem justificativa
|
|
50
50
|
- Exceptions como fluxo de controle
|
|
51
|
-
- Camada de Service/Manager entre handler e dados — o handler fala com `
|
|
51
|
+
- Camada de Service/Manager entre handler e dados — o handler fala com o `ApplicationDbContext` direto, nunca com um service
|
|
52
|
+
- Repository genérico (`IRepository<T>`/`IUnitOfWork`) — redundante sobre EF Core: `DbSet<T>` já é repository, `DbContext` já é unit of work
|
|
52
53
|
- SQL cru (`FromSqlRaw`, `ExecuteQuery(string)`) sem justificativa em `decisions.md` — use LINQ
|
|
53
54
|
|
|
54
|
-
Acesso a dados em VSA
|
|
55
|
+
Acesso a dados em VSA: o handler injeta `ApplicationDbContext` direto no slice — `DbSet<T>` já é repository, `DbContext` já é unit of work. Para concorrência ou SQL específico, crie um store dedicado hand-written com justificativa em `decisions.md`. NUNCA camada de Service/Manager; NUNCA repository genérico.
|
|
55
56
|
|
|
56
57
|
---
|
|
57
58
|
|
package/framework/agents.json
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"backend-dotnet-fluent-validation-vsa"
|
|
23
23
|
],
|
|
24
24
|
"mcp": ["context7"],
|
|
25
|
-
"persona": "Você é um engenheiro .NET sênior. Pensa em VSA com handlers diretos, result pattern, sealed classes, Guid.CreateVersion7(). Acesso a dados
|
|
25
|
+
"persona": "Você é um engenheiro .NET sênior. Pensa em VSA com handlers diretos, result pattern, sealed classes, Guid.CreateVersion7(). Acesso a dados: ApplicationDbContext injetado direto no slice (DbSet<T> já é repository, DbContext já é unit of work); para concorrência ou SQL específico, store dedicado hand-written com justificativa em decisions.md; nunca uma camada de Service/Manager; nunca repository genérico (IRepository<T>/IUnitOfWork). Não usa exceptions como fluxo. Consulta o standard antes de escrever cada slice. Nomeia tudo em PascalCase para tipos, camelCase para parâmetros."
|
|
26
26
|
},
|
|
27
27
|
"ef-modeler": {
|
|
28
28
|
"title": "EF Core / Database Modeler",
|
|
@@ -28,7 +28,7 @@ dimensions:
|
|
|
28
28
|
spec:
|
|
29
29
|
specificity: "API contracts with sealed record DTOs, Result<T> return types, FluentValidation rules, specific HTTP status codes"
|
|
30
30
|
data_model: "Product entity with all fields, EF Core configuration, Neon PostgreSQL constraints"
|
|
31
|
-
architecture: "VSA feature folders, IHandler pattern, Scrutor pipeline, IApiEndpoint discovery,
|
|
31
|
+
architecture: "VSA feature folders, IHandler pattern, Scrutor pipeline, IApiEndpoint discovery, ApplicationDbContext direto no slice (sem repository genérico)"
|
|
32
32
|
completeness: "All CRUD endpoints, pagination params (page/pageSize), search filters, error responses via Result<T>"
|
|
33
33
|
tasks:
|
|
34
34
|
granularity: "Each task 15-60 minutes, no mega-tasks"
|
|
@@ -73,7 +73,6 @@ For each new feature slice, follow the patterns in these templates:
|
|
|
73
73
|
|
|
74
74
|
| Template | Path | Purpose |
|
|
75
75
|
|----------|------|---------|
|
|
76
|
-
| Repository | `.morph/framework/templates/code/dotnet/backend/repository.cs` | EF Core repository |
|
|
77
76
|
| Migration | `.morph/framework/templates/code/dotnet/database/migration.cs` | EF Core migration |
|
|
78
77
|
| Hangfire Job | `.morph/framework/templates/code/dotnet/jobs/job.cs` | Background job |
|
|
79
78
|
| MAF Agent | `.morph/framework/templates/code/dotnet/jobs/agent.cs` | MS Agent Framework |
|
|
@@ -4,7 +4,7 @@ description: Guia a criação de uma landing page nova ponta-a-ponta a partir do
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao iniciar um novo projeto de LP com o template Umbraco genérico
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "7.
|
|
7
|
+
cliVersion: "7.8.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# create-umbraco-lp
|
|
@@ -4,7 +4,7 @@ description: Migra uma landing page existente (React/Vite, Next.js estático ou
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao receber instrução "use /migrate-lp-to-umbraco {projeto}" do usuário, ou quando o usuário descrever necessidade de migrar uma LP para Umbraco
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "7.
|
|
7
|
+
cliVersion: "7.8.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# migrate-lp-to-umbraco
|
|
@@ -4,7 +4,7 @@ description: Orquestra a execução autônoma de tasks de uma feature morph-spec
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: após Gate 2 aprovado, ao receber instrução "use /morph-apply" ou "implementar feature"
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "7.
|
|
7
|
+
cliVersion: "7.8.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-apply
|
|
@@ -4,7 +4,7 @@ description: Entende o negócio, os usuários e a arquitetura antes de qualquer
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao receber um novo request de feature, antes de morph-plan
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "7.
|
|
7
|
+
cliVersion: "7.8.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-brainstorming
|
|
@@ -27,7 +27,7 @@ O harness conhece o stack por padrão. Não pergunte sobre tecnologia a menos qu
|
|
|
27
27
|
| Frontend | Next.js App Router + Tailwind + Framer Motion + Shadcn |
|
|
28
28
|
| Jobs async | Hangfire — apenas se a feature requer processamento em background |
|
|
29
29
|
|
|
30
|
-
**Padrões proibidos:** `Guid.NewGuid()`, classes não-sealed sem justificativa, exceptions sem result pattern, camada de Service/Manager entre handler e dados, SQL cru sem justificativa. Acesso a dados em VSA
|
|
30
|
+
**Padrões proibidos:** `Guid.NewGuid()`, classes não-sealed sem justificativa, exceptions sem result pattern, camada de Service/Manager entre handler e dados, repository genérico (`IRepository<T>`/`IUnitOfWork`), SQL cru sem justificativa. Acesso a dados em VSA: `ApplicationDbContext` direto no slice (`DbSet<T>` já é repository, `DbContext` já é unit of work); para concorrência ou SQL específico, store dedicado hand-written com justificativa em `decisions.md`.
|
|
31
31
|
|
|
32
32
|
## O que fazer
|
|
33
33
|
|
|
@@ -4,7 +4,7 @@ description: Avalia a qualidade de uma tarefa ou fase com rubricas graduadas 0-1
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao completar uma tarefa ou ao finalizar uma fase de implementação
|
|
6
6
|
user-invokable: false
|
|
7
|
-
cliVersion: "7.
|
|
7
|
+
cliVersion: "7.8.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-eval
|
|
@@ -4,7 +4,7 @@ description: Guia a produção dos três artefatos de planejamento de uma featur
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao iniciar a fase plan de uma feature, após o Gate 1 (proposal aprovado)
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "7.
|
|
7
|
+
cliVersion: "7.8.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-plan
|
|
@@ -34,9 +34,10 @@ Ao iniciar a fase plan — após o `proposal.md` ser aprovado e antes de qualque
|
|
|
34
34
|
- `Guid.NewGuid()` → usar `Guid.CreateVersion7()`
|
|
35
35
|
- Classes não-sealed sem justificativa documentada
|
|
36
36
|
- Exceptions como fluxo de controle → usar result pattern com `result.Match()`
|
|
37
|
-
- Camada de Service/Manager entre handler e dados → handler usa `
|
|
37
|
+
- Camada de Service/Manager entre handler e dados → handler usa `ApplicationDbContext` direto no slice
|
|
38
|
+
- Repository genérico (`IRepository<T>`/`IUnitOfWork`) → redundante sobre EF Core: `DbSet<T>` já é repository, `DbContext` já é unit of work
|
|
38
39
|
- SQL cru (`FromSqlRaw`, `ExecuteQuery(string)`) sem justificativa em `decisions.md` → usar LINQ
|
|
39
|
-
- Política de acesso a dados (registre no mandate
|
|
40
|
+
- Política de acesso a dados (registre no mandate): `ApplicationDbContext` injetado direto no slice — para CRUD e para queries específicas (joins, projeção, paginação). Para concorrência ou SQL específico, store dedicado hand-written com justificativa em `decisions.md`.
|
|
40
41
|
- **Features com agentes de IA (MAF):** se a feature envolve um agente (Microsoft Agent Framework), **use a tool Read** nos standards relevantes de `framework/standards/ai-agents/*.md` (ex.: `setup`, `agent-session`, `middleware-patterns`, `mcp-server`, `mcp-tools`, `multi-agent-patterns`, `production`) — filtre pelo que a feature exige via STANDARDS.json. Esses standards carregam as **convenções de projeto** (single-agent por padrão — Workflows só para fan-in/out real; provider, caching, observability, A2A) que uma fonte externa de docs (ex.: context7) não conhece. Padrão de divisão: **superfície da API** pode vir de docs ao vivo/context7 (mais frescas para um SDK em evolução), mas **as convenções vêm dos standards** — registre ambas no mandate. Não pule os `ai-agents/*.md` só porque o STANDARDS.json index lista que eles existem.
|
|
41
42
|
- Checklist de done com critérios verificáveis por tarefa.
|
|
42
43
|
|
|
@@ -4,7 +4,7 @@ description: Orquestra a fase de proposta+plano de uma feature morph-spec. Encad
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao receber instrução "use /morph-proposal" do usuário, ou ao iniciar uma nova feature
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "7.
|
|
7
|
+
cliVersion: "7.8.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-proposal
|
|
@@ -4,7 +4,7 @@ description: Conduz a fase de revisão e produz o review-report.md para o Gate 3
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao iniciar a fase review, após todas as tasks de tasks.json estarem com status `done`
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "7.
|
|
7
|
+
cliVersion: "7.8.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-review
|
|
@@ -4,7 +4,7 @@ description: Varre o codebase existente, detecta stack atual, compara com o stac
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao iniciar uma feature em projeto brownfield (codebase já existente)
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "7.
|
|
7
|
+
cliVersion: "7.8.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-stack-scan
|
|
@@ -29,7 +29,7 @@ Quando o projeto já tem código existente e você precisa entender o estado atu
|
|
|
29
29
|
| Frontend | Next.js App Router + Tailwind + Framer Motion + Shadcn |
|
|
30
30
|
| Jobs | Hangfire (apenas quando necessário) |
|
|
31
31
|
|
|
32
|
-
**Padrões proibidos:** `Guid.NewGuid()`, classes não-sealed sem justificativa, exceptions como fluxo de controle, camada de Service/Manager entre handler e dados, SQL cru sem justificativa. Acesso a dados em VSA
|
|
32
|
+
**Padrões proibidos:** `Guid.NewGuid()`, classes não-sealed sem justificativa, exceptions como fluxo de controle, camada de Service/Manager entre handler e dados, repository genérico (`IRepository<T>`/`IUnitOfWork`), SQL cru sem justificativa. Acesso a dados em VSA: `ApplicationDbContext` direto no slice (`DbSet<T>` já é repository, `DbContext` já é unit of work); para concorrência ou SQL específico, store dedicado hand-written com justificativa em `decisions.md`.
|
|
33
33
|
|
|
34
34
|
## Greenfield short-circuit
|
|
35
35
|
|
|
@@ -62,7 +62,7 @@ Antes de despachar Explore, verifique se o projeto é greenfield:
|
|
|
62
62
|
- Erros: Result pattern (`result.Match()`)
|
|
63
63
|
|
|
64
64
|
## Padrões proibidos
|
|
65
|
-
`Guid.NewGuid()`, classes não-sealed sem justificativa, exceptions como fluxo de controle, camada de Service/Manager entre handler e dados, SQL cru sem justificativa. Acesso a dados em VSA
|
|
65
|
+
`Guid.NewGuid()`, classes não-sealed sem justificativa, exceptions como fluxo de controle, camada de Service/Manager entre handler e dados, repository genérico (`IRepository<T>`/`IUnitOfWork`), SQL cru sem justificativa. Acesso a dados em VSA: `ApplicationDbContext` direto no slice (`DbSet<T>` já é repository, `DbContext` já é unit of work); para concorrência ou SQL específico, store dedicado hand-written com justificativa em `decisions.md`.
|
|
66
66
|
|
|
67
67
|
## Recomendação
|
|
68
68
|
Compliance trivial (greenfield). Prosseguir para morph-brainstorming.
|
|
@@ -82,7 +82,7 @@ Antes de despachar Explore, verifique se o projeto é greenfield:
|
|
|
82
82
|
BACKEND:
|
|
83
83
|
- Todos os .csproj: qual TargetFramework? Quais PackageReferences relevantes?
|
|
84
84
|
- Procure por: Guid.NewGuid() vs Guid.CreateVersion7(), sealed classes, result pattern vs exceptions
|
|
85
|
-
- Estrutura de src/: segue feature-folder (VSA)? Tem Controllers? Tem IRepository<T
|
|
85
|
+
- Estrutura de src/: segue feature-folder (VSA)? Tem Controllers? Tem repository genérico (IRepository<T>/IUnitOfWork)? Se tiver, reporte como código morto: liste onde está registrado no DI e quantos consumidores reais existem em src/ e tests/
|
|
86
86
|
- Migrations/: qual ORM? Neon/Postgres ou outro banco?
|
|
87
87
|
- Microsoft.Agents.* ou Microsoft.Extensions.AI nos PackageReferences?
|
|
88
88
|
|
|
@@ -102,7 +102,7 @@ Antes de despachar Explore, verifique se o projeto é greenfield:
|
|
|
102
102
|
| Severidade | Critério |
|
|
103
103
|
|------------|---------|
|
|
104
104
|
| **Bloqueante** | Runtime diferente (.NET 8 ou anterior), ORM diferente (Dapper puro sem EF Core), banco diferente (MySQL, SQL Server) |
|
|
105
|
-
| **Alto** | `Guid.NewGuid()` generalizado, camada de Service/Manager entre handler e dados, SQL cru generalizado, exceptions como fluxo de controle |
|
|
105
|
+
| **Alto** | `Guid.NewGuid()` generalizado, camada de Service/Manager entre handler e dados, repository genérico (`IRepository<T>`/`IUnitOfWork`) presente — código morto: migração é deletar a pasta `Repository/` + as 2 linhas de DI, SQL cru generalizado, exceptions como fluxo de controle |
|
|
106
106
|
| **Médio** | Classes não-sealed, Pages Router em vez de App Router, biblioteca de UI diferente |
|
|
107
107
|
| **Baixo** | Versões de pacotes desatualizadas mas compatíveis |
|
|
108
108
|
|
|
@@ -4,7 +4,7 @@ description: Localiza e carrega um standard específico do STANDARDS.json sob de
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: quando precisar consultar um standard específico durante a implementação
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "7.
|
|
7
|
+
cliVersion: "7.8.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-standards
|
|
@@ -883,7 +883,7 @@
|
|
|
883
883
|
"endpoint": "#complete-endpoint-example",
|
|
884
884
|
"registration": "#programcs-di-registration-order"
|
|
885
885
|
},
|
|
886
|
-
"digest": "Each VSA feature is 5 files: Request, Response, Handler, Validator, Endpoint. Use IHandler<TReq,TRes> (not MediatR). Sealed records for DTOs, primary constructors for DI, Guid.CreateVersion7() for IDs. Handler
|
|
886
|
+
"digest": "Each VSA feature is 5 files: Request, Response, Handler, Validator, Endpoint. Use IHandler<TReq,TRes> (not MediatR). Sealed records for DTOs, primary constructors for DI, Guid.CreateVersion7() for IDs. Handler injects ApplicationDbContext directly — no service layer, no generic repository (DbSet<T> is the repository, DbContext is the unit of work). CancellationToken in every async call."
|
|
887
887
|
},
|
|
888
888
|
{
|
|
889
889
|
"id": "backend-integrations-asaas-asaas-api",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
Padrão arquitetural baseado em **KanaiyaKatarmal/VerticalSliceArchitectureTemplate** (.NET 10, Minimal APIs, EF Core 10).
|
|
12
12
|
Cada feature é uma fatia vertical completa: endpoint → handler → validator → entity → DB.
|
|
13
|
-
Sem MediatR, sem AggregateRoot, sem Domain Events, sem CQRS formal
|
|
13
|
+
Sem MediatR, sem AggregateRoot, sem Domain Events, sem CQRS formal, sem repository genérico
|
|
14
|
+
(divergência intencional do template base: o handler usa `ApplicationDbContext` direto).
|
|
14
15
|
|
|
15
16
|
---
|
|
16
17
|
|
|
@@ -54,11 +55,6 @@ Sem MediatR, sem AggregateRoot, sem Domain Events, sem CQRS formal.
|
|
|
54
55
|
├── Pipelines/
|
|
55
56
|
│ ├── ValidationDecorator.cs # Decorator que executa validators
|
|
56
57
|
│ └── LoggingDecorator.cs # Decorator que loga request/failure
|
|
57
|
-
├── Repository/
|
|
58
|
-
│ ├── IRepository.cs # IRepository<T> genérico
|
|
59
|
-
│ ├── Repository.cs # Repository<T> : IRepository<T>
|
|
60
|
-
│ ├── IUnitOfWork.cs # IUnitOfWork
|
|
61
|
-
│ └── UnitOfWork.cs # UnitOfWork
|
|
62
58
|
└── Program.cs
|
|
63
59
|
```
|
|
64
60
|
|
|
@@ -190,8 +186,7 @@ public sealed record CreateBookRequest(string Title, string Author, string ISBN,
|
|
|
190
186
|
public sealed record CreateBookResponse(Guid Id, string Title, string Author, string ISBN, decimal Price, int PublishedYear);
|
|
191
187
|
|
|
192
188
|
public sealed class CreateBookHandler(
|
|
193
|
-
|
|
194
|
-
IUnitOfWork _unitOfWork) : IHandler<CreateBookRequest, Result<CreateBookResponse>>
|
|
189
|
+
ApplicationDbContext _db) : IHandler<CreateBookRequest, Result<CreateBookResponse>>
|
|
195
190
|
{
|
|
196
191
|
public async Task<Result<CreateBookResponse>> HandleAsync(
|
|
197
192
|
CreateBookRequest command, CancellationToken cancellationToken)
|
|
@@ -205,8 +200,8 @@ public sealed class CreateBookHandler(
|
|
|
205
200
|
Price = command.Price,
|
|
206
201
|
PublishedYear = command.PublishedYear
|
|
207
202
|
};
|
|
208
|
-
|
|
209
|
-
await
|
|
203
|
+
_db.Books.Add(book);
|
|
204
|
+
await _db.SaveChangesAsync(cancellationToken);
|
|
210
205
|
return Result.Success(new CreateBookResponse(
|
|
211
206
|
book.Id, book.Title, book.Author, book.ISBN, book.Price, book.PublishedYear));
|
|
212
207
|
}
|
|
@@ -306,38 +301,41 @@ Ordem de execução (innermost-first): `LoggingDecorator → ValidationDecorator
|
|
|
306
301
|
|
|
307
302
|
---
|
|
308
303
|
|
|
309
|
-
##
|
|
304
|
+
## Acesso a Dados — DbContext direto no slice
|
|
305
|
+
|
|
306
|
+
O handler injeta `ApplicationDbContext` direto. Sobre EF Core um repository genérico é
|
|
307
|
+
redundante: `DbSet<T>` já é repository, `DbContext` já é unit of work. **Nunca crie
|
|
308
|
+
`IRepository<T>`/`IUnitOfWork`** — a auditoria de projetos reais mostrou que o par nasce
|
|
309
|
+
registrado no DI e morre órfão, sem consumidores.
|
|
310
310
|
|
|
311
311
|
```csharp
|
|
312
|
-
//
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
312
|
+
// CRUD por chave — direto no DbSet
|
|
313
|
+
var book = await _db.Books.FindAsync([command.Id], cancellationToken);
|
|
314
|
+
_db.Books.Add(book);
|
|
315
|
+
await _db.SaveChangesAsync(cancellationToken);
|
|
316
|
+
|
|
317
|
+
// Queries específicas — LINQ no slice (joins, projeção, paginação, filtros compostos)
|
|
318
|
+
var page = await _db.Books
|
|
319
|
+
.Where(b => b.Author == command.Author)
|
|
320
|
+
.OrderBy(b => b.Title)
|
|
321
|
+
.Skip(command.Skip).Take(command.Take)
|
|
322
|
+
.Select(b => new BookDto(b.Id, b.Title))
|
|
323
|
+
.ToListAsync(cancellationToken);
|
|
324
324
|
```
|
|
325
325
|
|
|
326
|
-
**
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
326
|
+
**Stores dedicados (exceção justificada):** quando um slice exige controle de concorrência
|
|
327
|
+
(ex.: `SELECT ... FOR UPDATE`, retry de `DbUpdateConcurrencyException`) ou SQL específico
|
|
328
|
+
que o LINQ não expressa, escreva um store hand-written e purpose-built (ex.: `OutboxStore`,
|
|
329
|
+
`LeaseStore`) com métodos nomeados pela intenção — e registre a justificativa em
|
|
330
|
+
`decisions.md`. Nunca um genérico por entidade.
|
|
331
|
+
|
|
332
|
+
Veja `backend/dotnet/vsa-handler-patterns.md` → "Data Access: DbContext in the Slice".
|
|
331
333
|
|
|
332
334
|
---
|
|
333
335
|
|
|
334
336
|
## DI Registration (Program.cs)
|
|
335
337
|
|
|
336
338
|
```csharp
|
|
337
|
-
// Repositories + Unit of Work
|
|
338
|
-
builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
|
|
339
|
-
builder.Services.AddScoped(typeof(IUnitOfWork), typeof(UnitOfWork));
|
|
340
|
-
|
|
341
339
|
// EF Core (SQLite/SqlServer/PostgreSQL)
|
|
342
340
|
builder.Services.AddSQLDatabaseConfiguration(builder.Configuration);
|
|
343
341
|
|
|
@@ -385,8 +383,8 @@ app.MapApiEndpoints();
|
|
|
385
383
|
4. **`result.Match()`** nos endpoints — nunca acessar `result.Value` sem checar `IsSuccess`.
|
|
386
384
|
5. **IApiEndpoint `internal sealed`** — endpoints não são públicos.
|
|
387
385
|
6. **`{Entity}Errors` compartilhado** por todas as operações da feature (1 arquivo por feature, não por slice).
|
|
388
|
-
7.
|
|
389
|
-
8. **`
|
|
386
|
+
7. **`ApplicationDbContext` direto no slice**: CRUD por chave via `DbSet<T>`; queries específicas (joins, projeção, paginação) via LINQ no handler. Nunca uma Application Service layer entre handler e dados; nunca repository genérico (`IRepository<T>`/`IUnitOfWork`).
|
|
387
|
+
8. **`SaveChangesAsync()`** explícito em operações de escrita (Create, Update, Delete).
|
|
390
388
|
9. **Sem validator em GetAll** — GetAllBooksHandler não tem validator (sem parâmetros para validar).
|
|
391
389
|
|
|
392
390
|
---
|
|
@@ -399,7 +397,8 @@ app.MapApiEndpoints();
|
|
|
399
397
|
| `DomainEvent` | VSA não tem Domain Events — reações são handlers separados |
|
|
400
398
|
| `MediatR` / `IRequest` / `IRequestHandler` | Substituído por `IHandler<TRequest, TResponse>` próprio |
|
|
401
399
|
| DTOs compartilhados entre features | Cada slice define seus próprios records |
|
|
402
|
-
| Application Service layer | Handler acessa
|
|
400
|
+
| Application Service layer | Handler acessa o `ApplicationDbContext` diretamente |
|
|
401
|
+
| Repository genérico (`IRepository<T>`/`IUnitOfWork`) | `DbSet<T>` já é repository, `DbContext` já é unit of work — vira código morto |
|
|
403
402
|
| CQRS formal (Commands/Queries) | Handler simples, sem distinção Command/Query |
|
|
404
403
|
| Shared Validators em Application layer | Validator fica no mesmo namespace do slice |
|
|
405
404
|
|
|
@@ -62,7 +62,7 @@ public enum OrderStatus
|
|
|
62
62
|
```csharp
|
|
63
63
|
public async Task<Result> ProcessOrderAsync(Guid orderId, CancellationToken ct)
|
|
64
64
|
{
|
|
65
|
-
var order = await
|
|
65
|
+
var order = await _db.Orders.FindAsync([orderId], ct);
|
|
66
66
|
if (order == null) return Result.Failure("Order not found");
|
|
67
67
|
|
|
68
68
|
// State validation (invalid states)
|
|
@@ -74,7 +74,7 @@ public async Task<Result> ProcessOrderAsync(Guid orderId, CancellationToken ct)
|
|
|
74
74
|
if (order.RequiresPayment && !order.IsPaid) return Result.Failure("Payment required");
|
|
75
75
|
|
|
76
76
|
order.MarkAsProcessing();
|
|
77
|
-
await
|
|
77
|
+
await _db.SaveChangesAsync(ct);
|
|
78
78
|
return Result.Success();
|
|
79
79
|
}
|
|
80
80
|
```
|
|
@@ -126,9 +126,7 @@ builder.Services.AddHandlersFromAssembly(typeof(Program).Assembly);
|
|
|
126
126
|
The full DI registration sequence in VSA:
|
|
127
127
|
|
|
128
128
|
```csharp
|
|
129
|
-
// 1. Infrastructure
|
|
130
|
-
builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
|
|
131
|
-
builder.Services.AddScoped(typeof(IUnitOfWork), typeof(UnitOfWork));
|
|
129
|
+
// 1. Infrastructure (DbContext)
|
|
132
130
|
builder.Services.AddSQLDatabaseConfiguration(builder.Configuration);
|
|
133
131
|
|
|
134
132
|
// 2. Endpoints (auto-discovery)
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
## Overview
|
|
13
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 ->
|
|
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 -> ApplicationDbContext` — no service layer, no generic repository in between.
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
@@ -71,8 +71,8 @@ public sealed record CreateBookResponse(
|
|
|
71
71
|
namespace ProjectName.Features.BookFeature.CreateBook;
|
|
72
72
|
|
|
73
73
|
using ProjectName.Abstractions;
|
|
74
|
+
using ProjectName.Database;
|
|
74
75
|
using ProjectName.Entities;
|
|
75
|
-
using ProjectName.Repository;
|
|
76
76
|
|
|
77
77
|
public sealed record CreateBookRequest(
|
|
78
78
|
string Title, string Author, string ISBN, decimal Price, int PublishedYear);
|
|
@@ -82,8 +82,7 @@ public sealed record CreateBookResponse(
|
|
|
82
82
|
|
|
83
83
|
// Primary constructor for DI — no field declarations needed
|
|
84
84
|
public sealed class CreateBookHandler(
|
|
85
|
-
|
|
86
|
-
IUnitOfWork unitOfWork) : IHandler<CreateBookRequest, Result<CreateBookResponse>>
|
|
85
|
+
ApplicationDbContext db) : IHandler<CreateBookRequest, Result<CreateBookResponse>>
|
|
87
86
|
{
|
|
88
87
|
public async Task<Result<CreateBookResponse>> HandleAsync(
|
|
89
88
|
CreateBookRequest command, CancellationToken cancellationToken)
|
|
@@ -98,8 +97,8 @@ public sealed class CreateBookHandler(
|
|
|
98
97
|
PublishedYear = command.PublishedYear
|
|
99
98
|
};
|
|
100
99
|
|
|
101
|
-
|
|
102
|
-
await
|
|
100
|
+
db.Books.Add(book);
|
|
101
|
+
await db.SaveChangesAsync(cancellationToken);
|
|
103
102
|
|
|
104
103
|
return Result.Success(new CreateBookResponse(
|
|
105
104
|
book.Id, book.Title, book.Author, book.ISBN, book.Price, book.PublishedYear));
|
|
@@ -111,7 +110,7 @@ public sealed class CreateBookHandler(
|
|
|
111
110
|
|
|
112
111
|
- **`sealed class`** — handlers are never inherited
|
|
113
112
|
- **`sealed record`** — DTOs are immutable value types
|
|
114
|
-
- **Primary constructors** — `(
|
|
113
|
+
- **Primary constructors** — `(ApplicationDbContext db)` replaces field + constructor boilerplate
|
|
115
114
|
- **`Guid.CreateVersion7()`** — time-sortable, index-friendly UUID (not `Guid.NewGuid()`)
|
|
116
115
|
- **`CancellationToken`** — passed through every async call
|
|
117
116
|
- **`Result<T>` return** — never throw for expected failures
|
|
@@ -172,36 +171,35 @@ All endpoint classes implementing this interface are auto-discovered and mapped
|
|
|
172
171
|
|
|
173
172
|
---
|
|
174
173
|
|
|
175
|
-
##
|
|
174
|
+
## Data Access: DbContext in the Slice (No Service Layer, No Generic Repository)
|
|
176
175
|
|
|
177
|
-
Handlers
|
|
176
|
+
Handlers inject `ApplicationDbContext` directly. Over EF Core a generic repository is
|
|
177
|
+
redundant — `DbSet<T>` **is** the repository, `DbContext` **is** the unit of work. A
|
|
178
|
+
generic `IRepository<T>`/`IUnitOfWork` pair adds indirection with zero value and, in
|
|
179
|
+
audited real projects, ends up registered in DI with zero consumers (dead code).
|
|
180
|
+
|
|
181
|
+
CRUD by key goes straight to the `DbSet`:
|
|
178
182
|
|
|
179
183
|
```csharp
|
|
180
|
-
public
|
|
184
|
+
public sealed class DeleteBookHandler(
|
|
185
|
+
ApplicationDbContext db) : IHandler<DeleteBookRequest, Result>
|
|
181
186
|
{
|
|
182
|
-
Task<
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
void RemoveRange(IEnumerable<T> entities);
|
|
189
|
-
}
|
|
187
|
+
public async Task<Result> HandleAsync(
|
|
188
|
+
DeleteBookRequest command, CancellationToken cancellationToken)
|
|
189
|
+
{
|
|
190
|
+
var book = await db.Books.FindAsync([command.Id], cancellationToken);
|
|
191
|
+
if (book is null)
|
|
192
|
+
return BookErrors.NotFound(command.Id);
|
|
190
193
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
194
|
+
db.Books.Remove(book);
|
|
195
|
+
await db.SaveChangesAsync(cancellationToken);
|
|
196
|
+
return Result.Success();
|
|
197
|
+
}
|
|
195
198
|
}
|
|
196
199
|
```
|
|
197
200
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
### When to Use DbContext Directly Instead
|
|
201
|
-
|
|
202
|
-
`IRepository<T>` is the right tool for CRUD by key. It is **not** the right tool for
|
|
203
|
-
slice-specific queries — joins, projections, pagination, composite filters. For those,
|
|
204
|
-
inject `ApplicationDbContext` directly into the handler and write the LINQ in the slice:
|
|
201
|
+
Slice-specific queries — joins, projections, pagination, composite filters — are LINQ
|
|
202
|
+
written in the slice:
|
|
205
203
|
|
|
206
204
|
```csharp
|
|
207
205
|
public sealed class SearchBooksHandler(
|
|
@@ -222,8 +220,16 @@ public sealed class SearchBooksHandler(
|
|
|
222
220
|
}
|
|
223
221
|
```
|
|
224
222
|
|
|
225
|
-
|
|
226
|
-
|
|
223
|
+
### Dedicated Stores (the justified exception)
|
|
224
|
+
|
|
225
|
+
When a slice needs concurrency control (`SELECT ... FOR UPDATE`, lease/outbox semantics,
|
|
226
|
+
`DbUpdateConcurrencyException` retry loops) or SQL that LINQ cannot express, write a
|
|
227
|
+
**hand-written, purpose-built store** with intention-named methods (e.g. `OutboxStore.ClaimBatchAsync`,
|
|
228
|
+
`LeaseStore.TryAcquireAsync`) — and document the justification in `decisions.md`.
|
|
229
|
+
Never a generic-per-entity abstraction.
|
|
230
|
+
|
|
231
|
+
**Rule of thumb:** everything → `ApplicationDbContext` in the slice. Concurrency or
|
|
232
|
+
specific SQL → dedicated hand-written store + `decisions.md` entry. Never reach for raw SQL
|
|
227
233
|
(`FromSqlRaw`) unless a query genuinely cannot be expressed in LINQ — and document that
|
|
228
234
|
in `decisions.md`.
|
|
229
235
|
|
|
@@ -251,7 +257,7 @@ public static class BookErrors
|
|
|
251
257
|
Usage in handler:
|
|
252
258
|
|
|
253
259
|
```csharp
|
|
254
|
-
var existing = await
|
|
260
|
+
var existing = await db.Books.FindAsync([command.BookId], cancellationToken);
|
|
255
261
|
if (existing is null)
|
|
256
262
|
return BookErrors.NotFound(command.BookId); // implicit conversion to Result<T>
|
|
257
263
|
```
|
|
@@ -264,9 +270,7 @@ if (existing is null)
|
|
|
264
270
|
// 1. OpenAPI
|
|
265
271
|
builder.Services.AddOpenApi();
|
|
266
272
|
|
|
267
|
-
// 2. Infrastructure (
|
|
268
|
-
builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
|
|
269
|
-
builder.Services.AddScoped(typeof(IUnitOfWork), typeof(UnitOfWork));
|
|
273
|
+
// 2. Infrastructure (DbContext)
|
|
270
274
|
builder.Services.AddSQLDatabaseConfiguration(builder.Configuration);
|
|
271
275
|
|
|
272
276
|
// 3. Endpoint auto-discovery
|
|
@@ -293,7 +297,8 @@ builder.Services.AddExceptionHandler<CustomExceptionHandler>()
|
|
|
293
297
|
| Do NOT | Do Instead |
|
|
294
298
|
|--------|------------|
|
|
295
299
|
| Use MediatR `IRequest`/`Send()` | Use `IHandler<TReq, TRes>` resolved directly from DI |
|
|
296
|
-
| Create a service/manager layer | Handler
|
|
300
|
+
| Create a service/manager layer | Handler uses `ApplicationDbContext` directly |
|
|
301
|
+
| Add a generic repository (`IRepository<T>`/`IUnitOfWork`) | `DbSet<T>` is the repository; `DbContext` is the unit of work |
|
|
297
302
|
| Use `Guid.NewGuid()` | Use `Guid.CreateVersion7()` for time-sortable IDs |
|
|
298
303
|
| Make handler classes non-sealed | Always `sealed class` |
|
|
299
304
|
| Use mutable DTOs (class with setters) | Always `sealed record` with positional parameters |
|
|
@@ -302,7 +307,7 @@ builder.Services.AddExceptionHandler<CustomExceptionHandler>()
|
|
|
302
307
|
| Share handlers across features | Each feature has its own handler, even if similar |
|
|
303
308
|
| Use constructor + field for DI | Use primary constructors: `class Handler(IDep dep)` |
|
|
304
309
|
| Group by technical concern (Controllers/, Services/) | Group by feature: `Features/BookFeature/CreateBook/` |
|
|
305
|
-
|
|
|
310
|
+
| Route data access through any indirection layer | Inject `DbContext` and write LINQ in the slice; dedicated store only with `decisions.md` justification |
|
|
306
311
|
| Use raw SQL (`FromSqlRaw` / `ExecuteQuery(string)`) by default | Use LINQ; raw SQL only with a `decisions.md` justification |
|
|
307
312
|
|
|
308
313
|
---
|
|
@@ -32,7 +32,7 @@ builder.Services.AddStackExchangeRedisCache(opts =>
|
|
|
32
32
|
public class CachedProductService : IProductService
|
|
33
33
|
{
|
|
34
34
|
private readonly IDistributedCache _cache;
|
|
35
|
-
private readonly
|
|
35
|
+
private readonly ApplicationDbContext _db;
|
|
36
36
|
|
|
37
37
|
public async Task<Product?> GetAsync(Guid id, CancellationToken ct = default)
|
|
38
38
|
{
|
|
@@ -44,7 +44,7 @@ public class CachedProductService : IProductService
|
|
|
44
44
|
return JsonSerializer.Deserialize<Product>(cached);
|
|
45
45
|
|
|
46
46
|
// Cache miss → read from DB
|
|
47
|
-
var product = await
|
|
47
|
+
var product = await _db.Products.FindAsync([id], ct);
|
|
48
48
|
if (product == null) return null;
|
|
49
49
|
|
|
50
50
|
// Cache with TTL
|