@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.
Files changed (42) hide show
  1. package/README.md +2 -2
  2. package/claude-plugin.json +1 -1
  3. package/docs/CHEATSHEET.md +1 -1
  4. package/docs/QUICKSTART.md +1 -1
  5. package/framework/CLAUDE.md +1 -1
  6. package/framework/MORPH.md +3 -2
  7. package/framework/agents.json +1 -1
  8. package/framework/evals/scenarios/dotnet-crud.yaml +1 -1
  9. package/framework/rules/dotnet-templates.md +0 -1
  10. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  11. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  12. package/framework/skills/core/morph-apply/SKILL.md +1 -1
  13. package/framework/skills/core/morph-brainstorming/SKILL.md +2 -2
  14. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  15. package/framework/skills/core/morph-implement/SKILL.md +1 -1
  16. package/framework/skills/core/morph-plan/SKILL.md +4 -3
  17. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  18. package/framework/skills/core/morph-review/SKILL.md +1 -1
  19. package/framework/skills/core/morph-stack-scan/SKILL.md +5 -5
  20. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  21. package/framework/standards/STANDARDS.json +1 -1
  22. package/framework/standards/architecture/vertical-slice/vertical-slice.md +34 -35
  23. package/framework/standards/backend/api/validation.md +2 -2
  24. package/framework/standards/backend/dotnet/scrutor.md +1 -3
  25. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +42 -37
  26. package/framework/standards/data/nosql/cache/redis.md +2 -2
  27. package/framework/templates/CLAUDE.md.core.template +1 -1
  28. package/framework/templates/README.md +0 -1
  29. package/framework/templates/REGISTRY.json +0 -21
  30. package/framework/templates/code/dotnet/abstractions/handler.cs +0 -3
  31. package/framework/templates/code/dotnet/constants/api-tags.cs +1 -1
  32. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +4 -5
  33. package/framework/templates/code/dotnet/feature/handler.cs.hbs +23 -24
  34. package/framework/templates/code/dotnet/jobs/job.cs +15 -15
  35. package/framework/templates/code/dotnet/test.cs +72 -101
  36. package/package.json +1 -1
  37. package/src/commands/project/doctor.js +4 -1
  38. package/src/commands/project/update.js +2 -0
  39. package/src/scripts/setup-infra.js +4 -1
  40. package/src/utils/banner.js +53 -37
  41. package/src/utils/file-copier.js +6 -1
  42. 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.7.2
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.7.2 by [Polymorphism Tech](https://polymorphism.tech)*
368
+ *morph-spec v7.8.0 by [Polymorphism Tech](https://polymorphism.tech)*
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "morph-spec",
3
- "version": "7.7.2",
3
+ "version": "7.8.0",
4
4
  "displayName": "MORPH-SPEC Framework",
5
5
  "description": "Spec-driven development with 38 agents and 8-phase workflow for .NET/Blazor/Next.js/Azure",
6
6
  "publisher": "polymorphism-tech",
@@ -196,4 +196,4 @@ These files are never edited directly. Use CLI commands or `morph-spec update` i
196
196
 
197
197
  ---
198
198
 
199
- *morph-spec v7.7.2 by Polymorphism Tech*
199
+ *morph-spec v7.8.0 by Polymorphism Tech*
@@ -197,4 +197,4 @@ morph-spec doctor
197
197
 
198
198
  ---
199
199
 
200
- *morph-spec v7.7.2 by Polymorphism Tech*
200
+ *morph-spec v7.8.0 by Polymorphism Tech*
@@ -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.
@@ -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 `IRepository<T>` ou `DbContext`, nunca com um service
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 é híbrido: `IRepository<T>` + `IUnitOfWork` para CRUD simples; `ApplicationDbContext` injetado direto no slice quando a query for específica (joins, projeção, paginação, filtros compostos).
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
 
@@ -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 híbrido: IRepository<T> + IUnitOfWork para CRUD, DbContext direto no slice para queries específicas; nunca uma camada de Service/Manager. Não usa exceptions como fluxo. Consulta o standard antes de escrever cada slice. Nomeia tudo em PascalCase para tipos, camelCase para parâmetros."
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, Repository/UnitOfWork"
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.2"
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.2"
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.2"
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.2"
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 é híbrido: `IRepository<T>` + `IUnitOfWork` para CRUD; `DbContext` direto no slice para queries específicas.
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.2"
7
+ cliVersion: "7.8.0"
8
8
  ---
9
9
 
10
10
  # morph-eval
@@ -4,7 +4,7 @@ description: Guia a execução de cada tarefa de implementação com standard e
4
4
  version: core
5
5
  when-to-invoke: ao iniciar cada task de implementação listada em tasks.json
6
6
  user-invokable: false
7
- cliVersion: "7.7.2"
7
+ cliVersion: "7.8.0"
8
8
  ---
9
9
 
10
10
  # morph-implement
@@ -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.2"
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 `IRepository<T>` ou `DbContext` direto
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 como orientação, não proibição): `IRepository<T>` + `IUnitOfWork` para CRUD simples; `ApplicationDbContext` injetado direto no slice para queries específicas (joins, projeção, paginação).
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.2"
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.2"
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.2"
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 é híbrido: `IRepository<T>` + `IUnitOfWork` para CRUD; `DbContext` direto no slice para queries específicas.
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 é híbrido: `IRepository<T>` + `IUnitOfWork` para CRUD; `DbContext` direto no slice para queries específicas.
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.2"
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 calls Repository+UnitOfWork directly — no service layer. CancellationToken in every async call."
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
- IRepository<Book> _bookRepo,
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
- await _bookRepo.AddAsync(book, cancellationToken);
209
- await _unitOfWork.CommitAsync(cancellationToken);
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
- ## Repository Pattern
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
- // Repository/IRepository.cs
313
- public interface IRepository<T> where T : class
314
- {
315
- Task<IEnumerable<T>> GetAllAsync(CancellationToken cancellationToken = default);
316
- Task<T?> GetByIdAsync(object id, CancellationToken cancellationToken = default);
317
- Task<T> AddAsync(T entity, CancellationToken cancellationToken = default);
318
- Task UpdateAsync(T entity, CancellationToken cancellationToken = default);
319
- Task DeleteAsync(T entity, CancellationToken cancellationToken = default);
320
- IEnumerable<T> ExecuteQuery(string query, DbParameter[] dbParams);
321
- Task AddRangeAsync(IEnumerable<T> entities, CancellationToken cancellationToken = default);
322
- void RemoveRange(IEnumerable<T> entities);
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
- **Quando NÃO usar o repository:** o `IRepository<T>` genérico cobre CRUD por chave.
327
- Para queries com join, projeção, paginação ou filtros compostos, injete
328
- `ApplicationDbContext` direto no handler e escreva o LINQ no slice — não polua a
329
- interface genérica com métodos de um único slice, e não use `ExecuteQuery(string sql)`.
330
- Veja `backend/dotnet/vsa-handler-patterns.md` → "When to Use DbContext Directly".
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. **Acesso a dados híbrido**: `IRepository<T>` injetado no handler para CRUD por chave; `ApplicationDbContext` injetado direto no slice para queries específicas (joins, projeção, paginação). Nunca uma Application Service layer entre handler e dados.
389
- 8. **`IUnitOfWork.CommitAsync()`** em operações de escrita (Create, Update, Delete).
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 Repository diretamente |
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 _repository.GetByIdAsync(orderId, ct);
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 _repository.UpdateAsync(order, ct);
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 -> Repository -> UnitOfWork` with no service layer in between.
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
- IRepository<Book> bookRepo,
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
- await bookRepo.AddAsync(book, cancellationToken);
102
- await unitOfWork.CommitAsync(cancellationToken);
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** — `(IRepository<Book> bookRepo, IUnitOfWork unitOfWork)` replaces field + constructor boilerplate
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
- ## Repository + UnitOfWork (No Service Layer)
174
+ ## Data Access: DbContext in the Slice (No Service Layer, No Generic Repository)
176
175
 
177
- Handlers call repositories directly. There is no service/manager layer:
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 interface IRepository<T> where T : class
184
+ public sealed class DeleteBookHandler(
185
+ ApplicationDbContext db) : IHandler<DeleteBookRequest, Result>
181
186
  {
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
- }
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
- public interface IUnitOfWork
192
- {
193
- Task<int> CommitAsync(CancellationToken cancellationToken);
194
- Task RollbackAsync(CancellationToken cancellationToken = default);
194
+ db.Books.Remove(book);
195
+ await db.SaveChangesAsync(cancellationToken);
196
+ return Result.Success();
197
+ }
195
198
  }
196
199
  ```
197
200
 
198
- **Pattern:** `Repository.AddAsync()` -> `UnitOfWork.CommitAsync()` — always commit explicitly.
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
- **Rule of thumb:** CRUD by key → `IRepository<T>` + `IUnitOfWork`. Anything that needs a
226
- custom query shape → `ApplicationDbContext` in the slice. Never reach for raw SQL
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 bookRepo.GetByIdAsync(command.BookId, cancellationToken);
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 (Repository, UnitOfWork, DbContext)
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 calls Repository + UnitOfWork directly |
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
- | Force a slice-specific query through `IRepository<T>` | Inject `DbContext` and write LINQ in the slice |
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 IProductRepository _repository;
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 _repository.GetAsync(id, ct);
47
+ var product = await _db.Products.FindAsync([id], ct);
48
48
  if (product == null) return null;
49
49
 
50
50
  // Cache with TTL