@eltonssouza/development-utility-kit 0.10.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/.claude/agents/README.md +24 -0
- package/.claude/agents/analyst.md +198 -0
- package/.claude/agents/backend-developer.md +126 -0
- package/.claude/agents/brain-keeper.md +229 -0
- package/.claude/agents/code-reviewer.md +181 -0
- package/.claude/agents/database-engineer.md +94 -0
- package/.claude/agents/devops-engineer.md +141 -0
- package/.claude/agents/frontend-developer.md +97 -0
- package/.claude/agents/gate-keeper.md +118 -0
- package/.claude/agents/migrator.md +291 -0
- package/.claude/agents/mobile-developer.md +80 -0
- package/.claude/agents/n8n-specialist.md +94 -0
- package/.claude/agents/product-owner.md +115 -0
- package/.claude/agents/qa-engineer.md +232 -0
- package/.claude/agents/release-engineer.md +204 -0
- package/.claude/agents/scaffold.md +87 -0
- package/.claude/agents/security-engineer.md +199 -0
- package/.claude/agents/sprint-runner.md +46 -0
- package/.claude/agents/stack-resolver.md +104 -0
- package/.claude/agents/tech-lead.md +182 -0
- package/.claude/agents/update-template.md +54 -0
- package/.claude/agents/ux-designer.md +118 -0
- package/.claude/hooks/flow-guard.js +261 -0
- package/.claude/hooks/flow-state.js +197 -0
- package/.claude/local/CLAUDE.md +71 -0
- package/.claude/settings.json +55 -0
- package/.claude/skills/README.md +331 -0
- package/.claude/skills/active-project/SKILL.md +131 -0
- package/.claude/skills/api-integration-test/SKILL.md +84 -0
- package/.claude/skills/auto-test-guard/SKILL.md +239 -0
- package/.claude/skills/auto-test-guard/resources/backend-tests.md +20 -0
- package/.claude/skills/auto-test-guard/resources/e2e-tests.md +24 -0
- package/.claude/skills/auto-test-guard/resources/execution-report.md +49 -0
- package/.claude/skills/auto-test-guard/resources/frontend-tests.md +18 -0
- package/.claude/skills/auto-test-guard/resources/initial-setup.md +108 -0
- package/.claude/skills/auto-test-guard/resources/run-suite.md +48 -0
- package/.claude/skills/auto-test-guard/resources/senior-gate.md +19 -0
- package/.claude/skills/brain-keeper/SKILL.md +62 -0
- package/.claude/skills/brain-keeper/obsidian/app.json +9 -0
- package/.claude/skills/brain-keeper/obsidian/appearance.json +4 -0
- package/.claude/skills/brain-keeper/obsidian/core-plugins.json +20 -0
- package/.claude/skills/brain-keeper/obsidian/daily-notes.json +5 -0
- package/.claude/skills/brain-keeper/obsidian/graph.json +32 -0
- package/.claude/skills/brain-keeper/obsidian/snippets/folder-colors.css +90 -0
- package/.claude/skills/brain-keeper/obsidian/templates.json +5 -0
- package/.claude/skills/brain-keeper/templates/README.md +51 -0
- package/.claude/skills/brain-keeper/templates/adr.md +40 -0
- package/.claude/skills/brain-keeper/templates/bug.md +35 -0
- package/.claude/skills/brain-keeper/templates/daily.md +38 -0
- package/.claude/skills/brain-keeper/templates/feature.md +62 -0
- package/.claude/skills/brain-keeper/templates/meeting.md +34 -0
- package/.claude/skills/brain-keeper/templates/tech-debt.md +21 -0
- package/.claude/skills/caveman/SKILL.md +189 -0
- package/.claude/skills/create-stack-pack/SKILL.md +281 -0
- package/.claude/skills/grill-me/SKILL.md +80 -0
- package/.claude/skills/pair-debug/SKILL.md +288 -0
- package/.claude/skills/prd-ready-check/SKILL.md +86 -0
- package/.claude/skills/project-manager/SKILL.md +334 -0
- package/.claude/skills/quality-standards/SKILL.md +203 -0
- package/.claude/skills/quick-feature/SKILL.md +266 -0
- package/.claude/skills/run-sprint/SKILL.md +41 -0
- package/.claude/skills/scaffold/SKILL.md +60 -0
- package/.claude/skills/stack-discovery/SKILL.md +161 -0
- package/.claude/skills/test-coverage-auditor/SKILL.md +87 -0
- package/.claude/skills/to-issues/SKILL.md +163 -0
- package/.claude/skills/to-prd/SKILL.md +130 -0
- package/.claude/skills/update-template/SKILL.md +256 -0
- package/.claude/stacks/CODEOWNERS +30 -0
- package/.claude/stacks/README.md +97 -0
- package/.claude/stacks/_template.md +116 -0
- package/.claude/stacks/dotnet/aspire-9.md +528 -0
- package/.claude/stacks/go/gin-1.10.md +570 -0
- package/.claude/stacks/java/spring-boot-3.md +376 -0
- package/.claude/stacks/java/spring-boot-4.md +438 -0
- package/.claude/stacks/node/express-5.md +538 -0
- package/.claude/stacks/python/django-5.md +483 -0
- package/.claude/stacks/python/fastapi-0.115.md +522 -0
- package/.claude/stacks/typescript/angular-18.md +420 -0
- package/.claude/stacks/typescript/angular-19.md +397 -0
- package/.claude/stacks/typescript/angular-21.md +494 -0
- package/CLAUDE.md +472 -0
- package/README.md +412 -0
- package/bin/cli.js +848 -0
- package/bin/lib/adr.js +146 -0
- package/bin/lib/backup.js +62 -0
- package/bin/lib/detect-stack.js +476 -0
- package/bin/lib/doctor.js +527 -0
- package/bin/lib/help.js +328 -0
- package/bin/lib/identity.js +108 -0
- package/bin/lib/lint-allowlist.json +15 -0
- package/bin/lib/lint.js +798 -0
- package/bin/lib/local-dir.js +68 -0
- package/bin/lib/manifest.js +236 -0
- package/bin/lib/sync-all.js +394 -0
- package/bin/lib/version-check.js +398 -0
- package/dashboard/db.js +321 -0
- package/dashboard/package.json +22 -0
- package/dashboard/public/app.js +853 -0
- package/dashboard/public/content/docs/agents-reference.en.md +911 -0
- package/dashboard/public/content/docs/architecture-overview.en.md +252 -0
- package/dashboard/public/content/docs/autonomy-matrix.en.md +186 -0
- package/dashboard/public/content/docs/cli-reference.en.md +538 -0
- package/dashboard/public/content/docs/git-flow.en.md +525 -0
- package/dashboard/public/content/docs/honcho-memory.en.md +394 -0
- package/dashboard/public/content/docs/hooks-reference.en.md +404 -0
- package/dashboard/public/content/docs/pipeline.en.md +414 -0
- package/dashboard/public/content/docs/plugins.en.md +289 -0
- package/dashboard/public/content/docs/quality-gate.en.md +315 -0
- package/dashboard/public/content/docs/skills-reference.en.md +484 -0
- package/dashboard/public/content/docs/stack-rules.en.md +362 -0
- package/dashboard/public/content/docs/troubleshooting.en.md +565 -0
- package/dashboard/public/content/manifest.json +114 -0
- package/dashboard/public/content/manual/backend.en.md +1053 -0
- package/dashboard/public/content/manual/existing-project.en.md +848 -0
- package/dashboard/public/content/manual/frontend.en.md +1008 -0
- package/dashboard/public/content/manual/fullstack.en.md +1459 -0
- package/dashboard/public/content/manual/mobile.en.md +837 -0
- package/dashboard/public/content/manual/quickstart.en.md +169 -0
- package/dashboard/public/index.html +217 -0
- package/dashboard/public/style.css +857 -0
- package/dashboard/public/vendor/marked.min.js +69 -0
- package/dashboard/rtk.js +143 -0
- package/dashboard/server-app.js +421 -0
- package/dashboard/server.js +104 -0
- package/dashboard/test/sprint1.test.js +406 -0
- package/dashboard/test/sprint2.test.js +571 -0
- package/dashboard/test/sprint3.test.js +560 -0
- package/package.json +33 -0
- package/scripts/hooks/subagent-telemetry.sh +14 -0
- package/scripts/hooks/telemetry-writer.js +250 -0
- package/scripts/latest-versions.json +56 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
---
|
|
2
|
+
stack: <lang>/<framework> # e.g. java/spring-boot-3, python/django, go/gin
|
|
3
|
+
versions_covered: "<range>" # e.g. "3.0.x — 3.4.x", "5.0.x — 5.1.x"
|
|
4
|
+
last_validated: <YYYY-MM-DD> # date of last validation in real project
|
|
5
|
+
validated_against: "<description>" # e.g. "projeto-x v1.2 (Java 21 + SB 3.2.5)"
|
|
6
|
+
status: active # active | deprecated | archived
|
|
7
|
+
pack_owner: "@<github-handle>" # e.g. "@elton"
|
|
8
|
+
security_review: <YYYY-MM-DD> # date of last security-focused review
|
|
9
|
+
next_review_due: <YYYY-MM-DD> # security_review + 12 months
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# <Stack name + version>
|
|
13
|
+
|
|
14
|
+
<Intro 1-3 lines: what this pack covers, when to use it vs alternative pack/version.>
|
|
15
|
+
|
|
16
|
+
## 1. When to use this pack
|
|
17
|
+
|
|
18
|
+
- Project declares `Primary stack: <X>` in `## Project Identity`.
|
|
19
|
+
- Build manifest (`pom.xml` / `package.json` / `pyproject.toml` / `go.mod` / etc.) declares versions within `versions_covered`.
|
|
20
|
+
- (Optional) Greenfield/legacy guidance — when to prefer next-major pack instead.
|
|
21
|
+
|
|
22
|
+
## 2. Stack baseline (what this pack assumes)
|
|
23
|
+
|
|
24
|
+
| Component | Version range | Notes |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| <language> | <range> | <notes — LTS, EOL dates> |
|
|
27
|
+
| <framework> | <range> | <key feature for this major> |
|
|
28
|
+
| <build tool> | <range> | <Maven Wrapper, npm, etc.> |
|
|
29
|
+
| <test framework> | <range> | <what to avoid — e.g. never H2 as Postgres> |
|
|
30
|
+
| <observability> | <stack> | <Micrometer, OpenTelemetry, etc.> |
|
|
31
|
+
|
|
32
|
+
## 3. Project structure
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
<tree of canonical folder layout for this stack>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 4. Code patterns
|
|
39
|
+
|
|
40
|
+
### <Pattern name 1 — e.g. DTOs>
|
|
41
|
+
<Real compilable snippet showing the canonical pattern + 1-line rule.>
|
|
42
|
+
|
|
43
|
+
### <Pattern name 2 — e.g. Error handling>
|
|
44
|
+
<Snippet + rule.>
|
|
45
|
+
|
|
46
|
+
### <Pattern name 3, 4, 5...>
|
|
47
|
+
|
|
48
|
+
## 5. Testing
|
|
49
|
+
|
|
50
|
+
### Unit
|
|
51
|
+
<Snippet + framework versions + rules.>
|
|
52
|
+
|
|
53
|
+
### Integration
|
|
54
|
+
<Snippet — REAL test pattern, not pseudocode.>
|
|
55
|
+
|
|
56
|
+
### <Other test types if relevant — slice, contract, mutation, e2e>
|
|
57
|
+
|
|
58
|
+
## 6. Build & run commands
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
<exact commands a dev would run — build, test, run, lint, coverage>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## 7. Security (per ADR-007 + ADR-027 — MANDATORY section)
|
|
65
|
+
|
|
66
|
+
### 7.1 Authentication & Authorization
|
|
67
|
+
<Stack-specific auth: framework class names, hashing, MFA hooks>
|
|
68
|
+
|
|
69
|
+
### 7.2 CORS
|
|
70
|
+
<Snippet — never `*` in prod>
|
|
71
|
+
|
|
72
|
+
### 7.3 Validation & input sanitization
|
|
73
|
+
<SQL injection, XSS, path traversal — stack-specific mitigation>
|
|
74
|
+
|
|
75
|
+
### 7.4 Secrets management
|
|
76
|
+
<Env vars, vault, never in committed files>
|
|
77
|
+
|
|
78
|
+
### 7.5 Rate limiting
|
|
79
|
+
<Library + config snippet for public endpoints>
|
|
80
|
+
|
|
81
|
+
### 7.6 OWASP Top 10 mapping
|
|
82
|
+
|
|
83
|
+
| OWASP | Mitigation in this stack |
|
|
84
|
+
|---|---|
|
|
85
|
+
| A01 Broken Access Control | <stack-specific> |
|
|
86
|
+
| A02 Cryptographic Failures | <stack-specific> |
|
|
87
|
+
| A03 Injection | <stack-specific> |
|
|
88
|
+
| A04 Insecure Design | <stack-specific> |
|
|
89
|
+
| A05 Security Misconfiguration | <stack-specific> |
|
|
90
|
+
| A06 Vulnerable Components | <CVE scanner + Renovate/Dependabot> |
|
|
91
|
+
| A07 Auth Failures | <rate limit + account lock + MFA> |
|
|
92
|
+
| A08 Data Integrity | <signature verification, supply chain, SBOM> |
|
|
93
|
+
| A09 Logging Failures | <structured logs + correlation ID + no PII> |
|
|
94
|
+
| A10 SSRF | <allowlist for outbound, validate URLs> |
|
|
95
|
+
|
|
96
|
+
### 7.7 LGPD / GDPR / compliance specifics (if applicable)
|
|
97
|
+
<PII tagging, soft-delete, data subject access, encryption at rest>
|
|
98
|
+
|
|
99
|
+
## 8. Anti-patterns (block in code-review)
|
|
100
|
+
|
|
101
|
+
| ❌ Bad | ✅ Good | Why |
|
|
102
|
+
|---|---|---|
|
|
103
|
+
| <bad pattern> | <good replacement> | <reason> |
|
|
104
|
+
| ... | ... | ... |
|
|
105
|
+
|
|
106
|
+
## 9. Migration hints — <this version> → <next major>
|
|
107
|
+
|
|
108
|
+
<Short list of breaking changes when migrating. Points to `migrator` agent.>
|
|
109
|
+
|
|
110
|
+
## 10. References
|
|
111
|
+
|
|
112
|
+
- ADR-007 (Senior+ gate thresholds)
|
|
113
|
+
- ADR-026 (Generic agents + packs architecture)
|
|
114
|
+
- ADR-027 (Pack governance)
|
|
115
|
+
- <Official framework migration guide URLs>
|
|
116
|
+
- <OWASP / compliance references>
|
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
---
|
|
2
|
+
stack: dotnet/aspire-9
|
|
3
|
+
versions_covered: "9.0.x — 9.2.x"
|
|
4
|
+
last_validated: 2026-05-28
|
|
5
|
+
validated_against: "reference pack — .NET 9 + Aspire 9.0 + EF Core 9 + xUnit 2.9"
|
|
6
|
+
status: active
|
|
7
|
+
pack_owner: "@elton"
|
|
8
|
+
security_review: 2026-05-28
|
|
9
|
+
next_review_due: 2027-05-28
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# .NET 9 + Aspire 9.x
|
|
13
|
+
|
|
14
|
+
Canonical knowledge pack for .NET cloud-native projects using **.NET Aspire 9.x** as the orchestration framework on top of .NET 9 + ASP.NET Core 9 + EF Core 9. Aspire is Microsoft's opinionated stack for distributed apps: it ships AppHost orchestration, service discovery, telemetry defaults, resilience defaults, and Dev Dashboard out of the box. For traditional ASP.NET Core projects without orchestration needs, this pack still applies — Aspire is additive.
|
|
15
|
+
|
|
16
|
+
## 1. When to use this pack
|
|
17
|
+
|
|
18
|
+
- Project declares `Primary stack: .NET 9 + Aspire 9.x + ASP.NET Core` in `## Project Identity`.
|
|
19
|
+
- Solution contains a `<Solution>.AppHost` project referencing `Aspire.Hosting` and at least one ASP.NET Core service project.
|
|
20
|
+
- Project follows the Aspire convention of `*.AppHost` (orchestration) + `*.ServiceDefaults` (shared telemetry/resilience) + one or more service projects.
|
|
21
|
+
- Greenfield .NET on cloud-native infra (Kubernetes, Azure Container Apps, AWS ECS) — Aspire's manifest export makes deployment cleaner.
|
|
22
|
+
- For Windows desktop apps, MAUI, or library-only solutions: Aspire is not applicable. This pack covers cloud-native services only.
|
|
23
|
+
|
|
24
|
+
## 2. Stack baseline (what this pack assumes)
|
|
25
|
+
|
|
26
|
+
| Component | Version range | Notes |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| .NET SDK | 9.0.x (min) / 10.0 preview (latest) | Native AOT supported for many service patterns; minimal APIs maduro |
|
|
29
|
+
| Aspire workload | 9.0.x — 9.2.x | `dotnet workload install aspire`; AppHost + ServiceDefaults convention |
|
|
30
|
+
| ASP.NET Core | 9.0.x | Minimal APIs are the default; `OpenAPI.NET` package replaces Swashbuckle for OpenAPI 3.1 |
|
|
31
|
+
| EF Core | 9.0.x | Bulk insert/update first-class; `IExecutionStrategy` mandatory for retry-aware transactions |
|
|
32
|
+
| Database providers | Npgsql 9.x (Postgres) / Microsoft.EntityFrameworkCore.SqlServer 9.x | NEVER InMemory provider for tests if prod is real DB |
|
|
33
|
+
| Validation | FluentValidation 11.10+ (integrated via `SharpGrip.FluentValidation.AutoValidation`) OR DataAnnotations | FluentValidation preferred for complex rules |
|
|
34
|
+
| Build | `dotnet build` + `dotnet publish` (Native AOT optional) | SLN format ok; new `slnx` (preview) acceptable |
|
|
35
|
+
| Tests | xUnit 2.9+ + FluentAssertions 6.x + Moq 4.20 or NSubstitute 5.x | Testcontainers .NET 4.0+ for integration |
|
|
36
|
+
| Mutation | Stryker.NET 4.4+ | Target ≥70% on `Domain` + `Application` |
|
|
37
|
+
| Coverage | `coverlet.collector` + `dotnet test --collect:"XPlat Code Coverage"` | Target ≥85% lines |
|
|
38
|
+
| Static analysis | Roslyn analyzers (`Microsoft.CodeAnalysis.NetAnalyzers`, `StyleCop`, `SonarAnalyzer.CSharp`) | TreatWarningsAsErrors = true in CI |
|
|
39
|
+
| Security scan | `dotnet list package --vulnerable --include-transitive` + `dotnet-security-scan` | 0 CVE with CVSS ≥7.0 |
|
|
40
|
+
| Observability | Aspire ServiceDefaults pre-wires OpenTelemetry SDK + Health Checks | W3C Trace Context; auto-instruments HTTP, EF Core, http.Client |
|
|
41
|
+
| Service discovery | Aspire built-in (DNS-based in dev, K8s in prod) | No Consul/Eureka needed |
|
|
42
|
+
| Background jobs | `Hangfire` 1.8+ OR `Quartz.NET` 3.x | Or Azure Functions / AWS Lambda for FaaS pattern |
|
|
43
|
+
| Resilience | Polly 8.x (integrated via `Microsoft.Extensions.Http.Resilience`) | Standard pipelines: retry + circuit breaker + timeout |
|
|
44
|
+
|
|
45
|
+
## 3. Project structure (DDD / Clean Architecture)
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
solution/
|
|
49
|
+
├── solution.sln
|
|
50
|
+
├── Directory.Build.props
|
|
51
|
+
├── src/
|
|
52
|
+
│ ├── Solution.AppHost/ # Aspire orchestrator
|
|
53
|
+
│ │ ├── Program.cs # builder.AddProject<...>()
|
|
54
|
+
│ │ └── Solution.AppHost.csproj
|
|
55
|
+
│ ├── Solution.ServiceDefaults/ # shared OTel/HealthCheck/Resilience
|
|
56
|
+
│ │ ├── Extensions.cs # AddServiceDefaults()
|
|
57
|
+
│ │ └── Solution.ServiceDefaults.csproj
|
|
58
|
+
│ ├── Solution.Domain/ # pure C#; no EF, no ASP.NET
|
|
59
|
+
│ │ ├── Products/
|
|
60
|
+
│ │ │ ├── Product.cs # entity / value object
|
|
61
|
+
│ │ │ ├── IProductRepository.cs # interface
|
|
62
|
+
│ │ │ └── ProductService.cs
|
|
63
|
+
│ │ └── Solution.Domain.csproj
|
|
64
|
+
│ ├── Solution.Application/ # use cases (1 class each)
|
|
65
|
+
│ │ ├── Products/
|
|
66
|
+
│ │ │ ├── CreateProductHandler.cs
|
|
67
|
+
│ │ │ ├── ListProductsHandler.cs
|
|
68
|
+
│ │ │ └── Dto.cs
|
|
69
|
+
│ │ └── Solution.Application.csproj
|
|
70
|
+
│ ├── Solution.Infrastructure/ # EF Core, HttpClient, AWS SDK
|
|
71
|
+
│ │ ├── Persistence/
|
|
72
|
+
│ │ │ ├── AppDbContext.cs
|
|
73
|
+
│ │ │ ├── Migrations/
|
|
74
|
+
│ │ │ └── ProductRepository.cs
|
|
75
|
+
│ │ └── Solution.Infrastructure.csproj
|
|
76
|
+
│ └── Solution.Api/ # minimal APIs / controllers
|
|
77
|
+
│ ├── Endpoints/
|
|
78
|
+
│ │ ├── Products.cs
|
|
79
|
+
│ │ └── ErrorEndpoints.cs # RFC 9457 ProblemDetails
|
|
80
|
+
│ ├── Program.cs # bootstrap
|
|
81
|
+
│ └── Solution.Api.csproj
|
|
82
|
+
└── tests/
|
|
83
|
+
├── Solution.Domain.Tests/
|
|
84
|
+
├── Solution.Application.Tests/
|
|
85
|
+
└── Solution.Api.IntegrationTests/
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Rule**: `Domain` project references nothing from EF Core or ASP.NET. `Application` references only `Domain`. `Infrastructure` references EF Core. `Api` references `Application` + `Infrastructure` (for DI registration). Project references enforced by `Directory.Build.props` — compile fails on violation.
|
|
89
|
+
|
|
90
|
+
## 4. Code patterns
|
|
91
|
+
|
|
92
|
+
### Domain entity (no EF Core, no ASP.NET)
|
|
93
|
+
|
|
94
|
+
```csharp
|
|
95
|
+
// src/Solution.Domain/Products/Product.cs
|
|
96
|
+
namespace Solution.Domain.Products;
|
|
97
|
+
|
|
98
|
+
public sealed class Product
|
|
99
|
+
{
|
|
100
|
+
public Guid Id { get; }
|
|
101
|
+
public string Name { get; private set; }
|
|
102
|
+
public decimal Price { get; private set; }
|
|
103
|
+
public int Stock { get; private set; }
|
|
104
|
+
public DateTimeOffset CreatedAt { get; }
|
|
105
|
+
|
|
106
|
+
private Product(Guid id, string name, decimal price, int stock, DateTimeOffset createdAt)
|
|
107
|
+
=> (Id, Name, Price, Stock, CreatedAt) = (id, name, price, stock, createdAt);
|
|
108
|
+
|
|
109
|
+
public static Product Create(string name, decimal price, int stock)
|
|
110
|
+
{
|
|
111
|
+
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("name required");
|
|
112
|
+
if (price <= 0) throw new ArgumentException("price must be positive");
|
|
113
|
+
if (stock < 0) throw new ArgumentException("stock cannot be negative");
|
|
114
|
+
return new Product(Guid.NewGuid(), name, price, stock, DateTimeOffset.UtcNow);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public void Reserve(int qty)
|
|
118
|
+
{
|
|
119
|
+
if (qty <= 0) throw new InvalidOperationException("qty must be positive");
|
|
120
|
+
if (qty > Stock) throw new InvalidOperationException("insufficient stock");
|
|
121
|
+
Stock -= qty;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Rule**: `decimal` for money (NOT `double` / `float`). Entity constructor private; static `Create` factory enforces invariants. Business rules as methods on the entity.
|
|
127
|
+
|
|
128
|
+
### Repository port + EF Core adapter
|
|
129
|
+
|
|
130
|
+
```csharp
|
|
131
|
+
// src/Solution.Domain/Products/IProductRepository.cs
|
|
132
|
+
namespace Solution.Domain.Products;
|
|
133
|
+
|
|
134
|
+
public interface IProductRepository
|
|
135
|
+
{
|
|
136
|
+
Task<Product?> GetAsync(Guid id, CancellationToken ct);
|
|
137
|
+
Task SaveAsync(Product product, CancellationToken ct);
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
```csharp
|
|
142
|
+
// src/Solution.Infrastructure/Persistence/AppDbContext.cs
|
|
143
|
+
using Microsoft.EntityFrameworkCore;
|
|
144
|
+
using Solution.Domain.Products;
|
|
145
|
+
|
|
146
|
+
namespace Solution.Infrastructure.Persistence;
|
|
147
|
+
|
|
148
|
+
public sealed class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
|
|
149
|
+
{
|
|
150
|
+
public DbSet<Product> Products => Set<Product>();
|
|
151
|
+
|
|
152
|
+
protected override void OnModelCreating(ModelBuilder mb)
|
|
153
|
+
{
|
|
154
|
+
var product = mb.Entity<Product>();
|
|
155
|
+
product.ToTable("products");
|
|
156
|
+
product.HasKey(p => p.Id);
|
|
157
|
+
product.Property(p => p.Name).HasMaxLength(120).IsRequired();
|
|
158
|
+
product.Property(p => p.Price).HasPrecision(12, 2);
|
|
159
|
+
product.Property(p => p.CreatedAt);
|
|
160
|
+
product.HasIndex(p => p.Name);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
```csharp
|
|
166
|
+
// src/Solution.Infrastructure/Persistence/ProductRepository.cs
|
|
167
|
+
using Microsoft.EntityFrameworkCore;
|
|
168
|
+
using Solution.Domain.Products;
|
|
169
|
+
|
|
170
|
+
namespace Solution.Infrastructure.Persistence;
|
|
171
|
+
|
|
172
|
+
public sealed class ProductRepository(AppDbContext db) : IProductRepository
|
|
173
|
+
{
|
|
174
|
+
public Task<Product?> GetAsync(Guid id, CancellationToken ct)
|
|
175
|
+
=> db.Products.AsNoTracking().FirstOrDefaultAsync(p => p.Id == id, ct);
|
|
176
|
+
|
|
177
|
+
public async Task SaveAsync(Product product, CancellationToken ct)
|
|
178
|
+
{
|
|
179
|
+
if (await db.Products.AnyAsync(p => p.Id == product.Id, ct))
|
|
180
|
+
db.Products.Update(product);
|
|
181
|
+
else
|
|
182
|
+
db.Products.Add(product);
|
|
183
|
+
await db.SaveChangesAsync(ct);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Rule**: `AsNoTracking()` for read-only queries (perf). `SaveChangesAsync` always with CancellationToken. UUID primary key.
|
|
189
|
+
|
|
190
|
+
### Minimal API endpoint (ASP.NET Core 9 canonical)
|
|
191
|
+
|
|
192
|
+
```csharp
|
|
193
|
+
// src/Solution.Api/Endpoints/Products.cs
|
|
194
|
+
using Solution.Application.Products;
|
|
195
|
+
|
|
196
|
+
namespace Solution.Api.Endpoints;
|
|
197
|
+
|
|
198
|
+
public static class ProductEndpoints
|
|
199
|
+
{
|
|
200
|
+
public record CreateProductRequest(string Name, decimal Price, int Stock);
|
|
201
|
+
public record ProductResponse(Guid Id, string Name, decimal Price, int Stock, DateTimeOffset CreatedAt);
|
|
202
|
+
|
|
203
|
+
public static void MapProductEndpoints(this IEndpointRouteBuilder app)
|
|
204
|
+
{
|
|
205
|
+
var group = app.MapGroup("/api/v1/products").WithTags("Products");
|
|
206
|
+
|
|
207
|
+
group.MapPost("/", async (
|
|
208
|
+
CreateProductRequest req,
|
|
209
|
+
CreateProductHandler handler,
|
|
210
|
+
CancellationToken ct) =>
|
|
211
|
+
{
|
|
212
|
+
var product = await handler.HandleAsync(req.Name, req.Price, req.Stock, ct);
|
|
213
|
+
return Results.Created(
|
|
214
|
+
$"/api/v1/products/{product.Id}",
|
|
215
|
+
new ProductResponse(product.Id, product.Name, product.Price, product.Stock, product.CreatedAt));
|
|
216
|
+
})
|
|
217
|
+
.WithName("CreateProduct")
|
|
218
|
+
.Produces<ProductResponse>(StatusCodes.Status201Created)
|
|
219
|
+
.ProducesValidationProblem(StatusCodes.Status422UnprocessableEntity);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
```csharp
|
|
225
|
+
// src/Solution.Api/Program.cs
|
|
226
|
+
using Solution.Infrastructure.Persistence;
|
|
227
|
+
using Solution.Application.Products;
|
|
228
|
+
using Microsoft.EntityFrameworkCore;
|
|
229
|
+
|
|
230
|
+
var builder = WebApplication.CreateBuilder(args);
|
|
231
|
+
|
|
232
|
+
// Aspire ServiceDefaults: telemetry, health checks, service discovery, resilience
|
|
233
|
+
builder.AddServiceDefaults();
|
|
234
|
+
|
|
235
|
+
// EF Core + connection string via Aspire reference
|
|
236
|
+
builder.AddNpgsqlDbContext<AppDbContext>("postgresdb");
|
|
237
|
+
|
|
238
|
+
builder.Services.AddScoped<IProductRepository, ProductRepository>();
|
|
239
|
+
builder.Services.AddScoped<CreateProductHandler>();
|
|
240
|
+
|
|
241
|
+
builder.Services.AddProblemDetails();
|
|
242
|
+
builder.Services.AddOpenApi();
|
|
243
|
+
|
|
244
|
+
var app = builder.Build();
|
|
245
|
+
|
|
246
|
+
app.MapDefaultEndpoints(); // /health, /alive, /metrics from ServiceDefaults
|
|
247
|
+
app.UseExceptionHandler(); // converts unhandled to RFC 9457
|
|
248
|
+
app.MapOpenApi();
|
|
249
|
+
app.MapProductEndpoints();
|
|
250
|
+
|
|
251
|
+
app.Run();
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
**Rule**: minimal APIs are the default (no `Controllers/` folder unless team prefers MVC). `Results.Created/...` returns typed `IResult`. `Produces<>` for OpenAPI. ServiceDefaults pre-wires observability.
|
|
255
|
+
|
|
256
|
+
### Aspire AppHost orchestration
|
|
257
|
+
|
|
258
|
+
```csharp
|
|
259
|
+
// src/Solution.AppHost/Program.cs
|
|
260
|
+
var builder = DistributedApplication.CreateBuilder(args);
|
|
261
|
+
|
|
262
|
+
var postgres = builder.AddPostgres("postgres")
|
|
263
|
+
.WithDataVolume()
|
|
264
|
+
.AddDatabase("postgresdb");
|
|
265
|
+
|
|
266
|
+
var api = builder.AddProject<Projects.Solution_Api>("api")
|
|
267
|
+
.WithReference(postgres)
|
|
268
|
+
.WaitFor(postgres);
|
|
269
|
+
|
|
270
|
+
builder.Build().Run();
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
**Rule**: AppHost is the single source of truth for service topology in dev. `WithReference()` injects connection strings via env vars. `WaitFor()` ensures startup order.
|
|
274
|
+
|
|
275
|
+
## 5. Testing
|
|
276
|
+
|
|
277
|
+
### Unit (xUnit + FluentAssertions, no EF / no ASP.NET)
|
|
278
|
+
|
|
279
|
+
```csharp
|
|
280
|
+
// tests/Solution.Domain.Tests/Products/ProductTests.cs
|
|
281
|
+
using FluentAssertions;
|
|
282
|
+
using Solution.Domain.Products;
|
|
283
|
+
|
|
284
|
+
public class ProductTests
|
|
285
|
+
{
|
|
286
|
+
[Fact]
|
|
287
|
+
public void Reserve_DecreasesStock()
|
|
288
|
+
{
|
|
289
|
+
var p = Product.Create("widget", 9.90m, 10);
|
|
290
|
+
p.Reserve(3);
|
|
291
|
+
p.Stock.Should().Be(7);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
[Fact]
|
|
295
|
+
public void Reserve_ThrowsOnZero()
|
|
296
|
+
{
|
|
297
|
+
var p = Product.Create("widget", 9.90m, 10);
|
|
298
|
+
var act = () => p.Reserve(0);
|
|
299
|
+
act.Should().Throw<InvalidOperationException>();
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Integration (WebApplicationFactory + Testcontainers Postgres)
|
|
305
|
+
|
|
306
|
+
```csharp
|
|
307
|
+
// tests/Solution.Api.IntegrationTests/ProductEndpointsTests.cs
|
|
308
|
+
using FluentAssertions;
|
|
309
|
+
using Microsoft.AspNetCore.Mvc.Testing;
|
|
310
|
+
using Testcontainers.PostgreSql;
|
|
311
|
+
|
|
312
|
+
public class ProductEndpointsTests : IAsyncLifetime
|
|
313
|
+
{
|
|
314
|
+
private readonly PostgreSqlContainer _pg = new PostgreSqlBuilder()
|
|
315
|
+
.WithImage("postgres:16-alpine")
|
|
316
|
+
.Build();
|
|
317
|
+
|
|
318
|
+
private WebApplicationFactory<Program>? _factory;
|
|
319
|
+
|
|
320
|
+
public async Task InitializeAsync()
|
|
321
|
+
{
|
|
322
|
+
await _pg.StartAsync();
|
|
323
|
+
Environment.SetEnvironmentVariable("ConnectionStrings__postgresdb", _pg.GetConnectionString());
|
|
324
|
+
_factory = new WebApplicationFactory<Program>();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
public async Task DisposeAsync()
|
|
328
|
+
{
|
|
329
|
+
await _pg.DisposeAsync();
|
|
330
|
+
_factory?.Dispose();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
[Fact]
|
|
334
|
+
public async Task Post_CreatesProduct()
|
|
335
|
+
{
|
|
336
|
+
var client = _factory!.CreateClient();
|
|
337
|
+
var resp = await client.PostAsJsonAsync("/api/v1/products", new {
|
|
338
|
+
Name = "widget", Price = 9.90m, Stock = 100,
|
|
339
|
+
});
|
|
340
|
+
resp.StatusCode.Should().Be(HttpStatusCode.Created);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
**Rule**: NEVER `UseInMemoryDatabase()` if prod is Postgres. Testcontainers .NET 4.0+ is the standard.
|
|
346
|
+
|
|
347
|
+
### Mutation (Stryker.NET)
|
|
348
|
+
|
|
349
|
+
```bash
|
|
350
|
+
dotnet stryker --project Solution.Domain --threshold-high 80 --threshold-low 70 --threshold-break 60
|
|
351
|
+
# Target: mutation score >= 70% on Domain + Application
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## 6. Build & run commands
|
|
355
|
+
|
|
356
|
+
```bash
|
|
357
|
+
# Setup (install workload once per machine)
|
|
358
|
+
dotnet workload install aspire
|
|
359
|
+
|
|
360
|
+
# Restore
|
|
361
|
+
dotnet restore
|
|
362
|
+
|
|
363
|
+
# Build (with warnings as errors)
|
|
364
|
+
dotnet build -p:TreatWarningsAsErrors=true
|
|
365
|
+
|
|
366
|
+
# Run via Aspire AppHost (boots all services + Dev Dashboard)
|
|
367
|
+
dotnet run --project src/Solution.AppHost
|
|
368
|
+
|
|
369
|
+
# Run a single service directly (skips Aspire orchestration)
|
|
370
|
+
dotnet run --project src/Solution.Api
|
|
371
|
+
|
|
372
|
+
# Migrations (EF Core)
|
|
373
|
+
dotnet ef migrations add InitialCreate --project src/Solution.Infrastructure --startup-project src/Solution.Api
|
|
374
|
+
dotnet ef database update --project src/Solution.Infrastructure --startup-project src/Solution.Api
|
|
375
|
+
|
|
376
|
+
# Tests
|
|
377
|
+
dotnet test # all
|
|
378
|
+
dotnet test --collect:"XPlat Code Coverage" # with coverage
|
|
379
|
+
dotnet test --filter "Category!=Integration" # fast loop
|
|
380
|
+
|
|
381
|
+
# Mutation
|
|
382
|
+
dotnet stryker
|
|
383
|
+
|
|
384
|
+
# Lint (Roslyn analyzers built in; surface via build)
|
|
385
|
+
dotnet format --verify-no-changes # CI gate
|
|
386
|
+
|
|
387
|
+
# Security scan
|
|
388
|
+
dotnet list package --vulnerable --include-transitive
|
|
389
|
+
dotnet list package --outdated
|
|
390
|
+
|
|
391
|
+
# Native AOT publish (when applicable)
|
|
392
|
+
dotnet publish src/Solution.Api -c Release -r linux-x64 -p:PublishAot=true
|
|
393
|
+
|
|
394
|
+
# Aspire manifest export (for K8s/Azure deployment)
|
|
395
|
+
dotnet run --project src/Solution.AppHost --publisher manifest --output-path ./aspire-manifest.json
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
## 7. Security (per ADR-007 + ADR-027 — MANDATORY section)
|
|
399
|
+
|
|
400
|
+
### 7.1 Authentication & Authorization
|
|
401
|
+
|
|
402
|
+
- **JWT Bearer**: `Microsoft.AspNetCore.Authentication.JwtBearer`. Configure validation parameters (issuer, audience, signing key, lifetime). RS256 preferred.
|
|
403
|
+
- **Identity**: `Microsoft.AspNetCore.Identity` for user management; ASP.NET Core 9 ships `MapIdentityApi<T>()` for minimal-API friendly endpoints.
|
|
404
|
+
- **OAuth2 / OIDC**: `Microsoft.AspNetCore.Authentication.OpenIdConnect` for Auth0/Entra/Keycloak.
|
|
405
|
+
- **Password hashing**: ASP.NET Core Identity uses PBKDF2 by default. For new projects consider switching to Argon2 via `BCrypt.Net-Next` or `Konscious.Security.Cryptography.Argon2`.
|
|
406
|
+
- **Authorization**: `[Authorize(Policy = "...")]` attributes; minimal APIs use `.RequireAuthorization("policy")`. Object-level checks inside the handler.
|
|
407
|
+
|
|
408
|
+
### 7.2 CORS
|
|
409
|
+
|
|
410
|
+
```csharp
|
|
411
|
+
builder.Services.AddCors(opt => opt.AddPolicy("Default", p =>
|
|
412
|
+
p.WithOrigins("https://app.example.com") // NEVER WithAnyOrigin() in prod
|
|
413
|
+
.AllowCredentials()
|
|
414
|
+
.WithMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
|
415
|
+
.WithHeaders("Authorization", "Content-Type")));
|
|
416
|
+
|
|
417
|
+
app.UseCors("Default");
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### 7.3 Validation & input sanitization
|
|
421
|
+
|
|
422
|
+
- **SQL injection**: EF Core parameterizes by default. NEVER `FromSqlRaw($"... {input}")` — use `FromSqlInterpolated($"... {input}")` (parameterizes) OR `FromSqlRaw` with parameters separately.
|
|
423
|
+
- **DataAnnotations or FluentValidation**: required on every request DTO. `[Required]`, `[MaxLength]`, `[Range]`, etc.
|
|
424
|
+
- **Path traversal**: `Path.GetFullPath` + `StartsWith` check against an allowlisted base directory.
|
|
425
|
+
- **Antiforgery**: `app.UseAntiforgery()` if you serve form-based POSTs; not needed for stateless JWT APIs.
|
|
426
|
+
|
|
427
|
+
### 7.4 Secrets management
|
|
428
|
+
|
|
429
|
+
- **dev**: `dotnet user-secrets` (per-user, not in source).
|
|
430
|
+
- **prod**: Azure Key Vault / AWS Secrets Manager / HashiCorp Vault via `Microsoft.Extensions.Configuration.<Provider>`.
|
|
431
|
+
- **Aspire**: AppHost wires connection strings via `WithReference()` — secrets surface via configuration, not env vars in code.
|
|
432
|
+
- Never `appsettings.json` with real secrets. `appsettings.json` committed; `appsettings.Production.json` typically NOT committed.
|
|
433
|
+
|
|
434
|
+
### 7.5 Rate limiting
|
|
435
|
+
|
|
436
|
+
ASP.NET Core 9 has built-in rate limiting:
|
|
437
|
+
|
|
438
|
+
```csharp
|
|
439
|
+
builder.Services.AddRateLimiter(opt =>
|
|
440
|
+
{
|
|
441
|
+
opt.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(ctx =>
|
|
442
|
+
RateLimitPartition.GetFixedWindowLimiter(
|
|
443
|
+
partitionKey: ctx.Connection.RemoteIpAddress?.ToString() ?? "anon",
|
|
444
|
+
factory: _ => new FixedWindowRateLimiterOptions {
|
|
445
|
+
PermitLimit = 100, Window = TimeSpan.FromMinutes(1) }));
|
|
446
|
+
|
|
447
|
+
opt.AddPolicy("auth", ctx => RateLimitPartition.GetFixedWindowLimiter(
|
|
448
|
+
ctx.Connection.RemoteIpAddress?.ToString() ?? "anon",
|
|
449
|
+
_ => new FixedWindowRateLimiterOptions {
|
|
450
|
+
PermitLimit = 5, Window = TimeSpan.FromMinutes(1) }));
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
app.UseRateLimiter();
|
|
454
|
+
// Per-endpoint: .RequireRateLimiting("auth")
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
### 7.6 OWASP Top 10 mapping
|
|
458
|
+
|
|
459
|
+
| OWASP | Mitigation in .NET 9 + ASP.NET Core |
|
|
460
|
+
|---|---|
|
|
461
|
+
| A01 Broken Access Control | `[Authorize]` + policy-based auth; object-level check in handler; default-deny via `[FallbackAuth]` |
|
|
462
|
+
| A02 Cryptographic Failures | PBKDF2 (Identity default) or Argon2 password hashing; TLS at reverse proxy; HSTS via `app.UseHsts()`; JWT RS256 with key rotation |
|
|
463
|
+
| A03 Injection | EF Core parameterization; `FromSqlInterpolated` (safe) vs `FromSqlRaw` (review); DataAnnotations/FluentValidation on every DTO |
|
|
464
|
+
| A04 Insecure Design | DDD layering enforced (`Domain` no EF/ASP.NET); use case handler = single async method; ADRs document deviations |
|
|
465
|
+
| A05 Security Misconfiguration | `app.UseHsts()`; `Server` header removed; CORS explicit allowlist; `ASPNETCORE_ENVIRONMENT=Production` |
|
|
466
|
+
| A06 Vulnerable Components | `dotnet list package --vulnerable` in CI; Dependabot for `.csproj` bumps; `Directory.Packages.props` for central version mgmt |
|
|
467
|
+
| A07 Auth Failures | Built-in rate limiter on auth endpoints; account lockout via Identity options; MFA via `Microsoft.AspNetCore.Identity.Extensions` |
|
|
468
|
+
| A08 Data Integrity | `nuget.config` with verified package sources; SBOM via `dotnet-sbom-tool`; signed assemblies for internal libs |
|
|
469
|
+
| A09 Logging Failures | Serilog or built-in `Microsoft.Extensions.Logging` with JSON formatter; correlation ID via `AddOpenTelemetry()`; **NEVER** log request body raw (PII) |
|
|
470
|
+
| A10 SSRF | Centralized `IHttpClientFactory` with named clients + delegating handler that allowlists outbound hosts; reject private CIDRs by default |
|
|
471
|
+
|
|
472
|
+
### 7.7 LGPD / GDPR / compliance specifics
|
|
473
|
+
|
|
474
|
+
- **PII tagging**: attribute `[PersonalData]` (from ASP.NET Identity) on entity properties; custom Roslyn analyzer flags missing tags on string properties named Email/Document/CPF.
|
|
475
|
+
- **Soft delete**: `IsDeleted` + `DeletedAt` columns; global query filter in `OnModelCreating` excludes deleted rows. NEVER hard delete user-owned data.
|
|
476
|
+
- **Data subject access (Art 15)**: `GET /api/v1/me/export` returns user data as ZIP (use ASP.NET Identity's built-in personal data export).
|
|
477
|
+
- **Erasure (Art 17)**: `DELETE /api/v1/me` redacts PII fields while preserving FKs.
|
|
478
|
+
- **Encryption at rest**: SQL Server TDE / PostgreSQL TDE (cloud-managed) OR column-level via `Microsoft.AspNetCore.DataProtection` for sensitive fields.
|
|
479
|
+
|
|
480
|
+
## 8. Anti-patterns (block in code-review)
|
|
481
|
+
|
|
482
|
+
| ❌ Bad | ✅ Good | Why |
|
|
483
|
+
|---|---|---|
|
|
484
|
+
| `double` or `float` for money | `decimal` | IEEE 754 precision loss is real |
|
|
485
|
+
| `DbContext` injected as singleton | `AddDbContext<>` (scoped — default) | `DbContext` is not thread-safe |
|
|
486
|
+
| `.Result` or `.Wait()` on Tasks | `await` everywhere | Deadlocks under ASP.NET sync context; lost exceptions |
|
|
487
|
+
| `FromSqlRaw($"... {input}")` (interpolated string) | `FromSqlInterpolated($"... {input}")` | The Raw + interpolated combo is SQL injection; Interpolated parameterizes |
|
|
488
|
+
| Public setters on domain entity properties | Private setters + business methods | Encapsulation breaks otherwise |
|
|
489
|
+
| `UseInMemoryDatabase()` for integration tests | Testcontainers Postgres / SQL Server | InMemory has DIFFERENT semantics (no FK constraints, etc.) |
|
|
490
|
+
| `Controller` everywhere when minimal APIs fit | Minimal APIs for simple CRUD; controllers only when MVC features needed | Less ceremony, less code |
|
|
491
|
+
| `Task<ActionResult<T>>` from minimal API | `Results.Ok(...)` / `Results.Created(...)` | Minimal APIs use `IResult`, not `ActionResult` |
|
|
492
|
+
| Ignoring `CancellationToken` in async signatures | Accept + pass `CancellationToken` everywhere | Caller cancellation must propagate to DB / HTTP |
|
|
493
|
+
| `JsonSerializer.Serialize(...)` without options | Configure `JsonOptions` once (camelCase, ignore nulls, etc.) | Inconsistent serialization across endpoints |
|
|
494
|
+
| Catching `Exception` in handler | Let global exception handler do it; catch specific domain exceptions | Hides bugs, breaks 500 telemetry |
|
|
495
|
+
| `appsettings.json` with secrets | User secrets in dev, Key Vault in prod | Secrets in repo = secrets in git history |
|
|
496
|
+
| Native AOT for solutions using reflection-heavy libs | Trim warnings audited; switch lib OR keep JIT | Runtime crash discovered late |
|
|
497
|
+
|
|
498
|
+
## 9. Migration hints — .NET 8 / Aspire preview → 9
|
|
499
|
+
|
|
500
|
+
Breaking changes worth flagging when `migrator` agent runs .NET 8 → 9 + Aspire 1.x → 9:
|
|
501
|
+
|
|
502
|
+
- **.NET 9 SDK required**; downgrading targetFramework to net8.0 still works but new APIs unavailable.
|
|
503
|
+
- **Aspire workload re-install**: `dotnet workload update` after .NET 9 SDK install.
|
|
504
|
+
- **`Aspire.Hosting.AppHost` package renamed/reorganized** — review AppHost `using` directives.
|
|
505
|
+
- **OpenAPI**: ASP.NET 9 ships `Microsoft.AspNetCore.OpenApi` natively. Remove Swashbuckle if you only used basic Swagger UI; keep Swashbuckle if you used its extensibility heavily.
|
|
506
|
+
- **EF Core 9 bulk operations**: `ExecuteUpdate` / `ExecuteDelete` work with complex predicates now. Audit places where you wrote manual batching loops.
|
|
507
|
+
- **`IExceptionHandler`** interface (NET 8+) is the canonical exception hook — replaces `UseExceptionHandler(builder => ...)` patterns from earlier.
|
|
508
|
+
- **Rate limiting** moved out of preview (built-in since .NET 7); remove `Microsoft.AspNetCore.RateLimiting` package if you added it manually.
|
|
509
|
+
- **Native AOT** broader compat: many libs that didn't AOT in .NET 8 now do. Retry compilation if you backed off before.
|
|
510
|
+
- **`PublishAot`** sets multiple sub-properties; review project file for redundant manual settings.
|
|
511
|
+
|
|
512
|
+
Hand off to `migrator` with: current .NET version, Aspire version, list of AppHost dependencies, list of services using deprecated APIs.
|
|
513
|
+
|
|
514
|
+
## 10. References
|
|
515
|
+
|
|
516
|
+
- [.NET Aspire documentation](https://learn.microsoft.com/en-us/dotnet/aspire/)
|
|
517
|
+
- [ASP.NET Core 9 docs](https://learn.microsoft.com/en-us/aspnet/core/)
|
|
518
|
+
- [EF Core 9 docs](https://learn.microsoft.com/en-us/ef/core/)
|
|
519
|
+
- [Minimal APIs guide](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis)
|
|
520
|
+
- [Polly resilience library](https://www.thepollyproject.org/)
|
|
521
|
+
- [Testcontainers .NET](https://dotnet.testcontainers.org/)
|
|
522
|
+
- [Stryker.NET mutation testing](https://stryker-mutator.io/docs/stryker-net/introduction/)
|
|
523
|
+
- [OWASP .NET Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/DotNet_Security_Cheat_Sheet.html)
|
|
524
|
+
- [ASP.NET Core security overview](https://learn.microsoft.com/en-us/aspnet/core/security/)
|
|
525
|
+
- ADR-007 (Senior+ gate thresholds — coverage ≥85%, mutation ≥70%)
|
|
526
|
+
- ADR-026 (Generic agents + stack packs architecture)
|
|
527
|
+
- ADR-027 (Pack governance — frontmatter + security mandatory + CODEOWNERS + annual review)
|
|
528
|
+
- ADR-029 (Canonical pack format — this document follows it)
|