@polymorphism-tech/morph-spec 4.3.1 → 4.3.2
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/bin/morph-spec.js +1 -1
- package/package.json +2 -1
- package/src/commands/project/doctor.js +24 -18
- package/src/commands/project/init.js +1 -1
- package/src/commands/state/state.js +24 -2
- package/src/core/templates/template-registry.js +1 -1
- package/src/core/workflows/workflow-detector.js +2 -2
- package/stacks/blazor-azure/.claude/commands/morph-apply.md +221 -0
- package/stacks/blazor-azure/.claude/commands/morph-archive.md +79 -0
- package/stacks/blazor-azure/.claude/commands/morph-deploy.md +529 -0
- package/stacks/blazor-azure/.claude/commands/morph-infra.md +209 -0
- package/stacks/blazor-azure/.claude/commands/morph-preflight.md +227 -0
- package/stacks/blazor-azure/.claude/commands/morph-proposal.md +122 -0
- package/stacks/blazor-azure/.claude/commands/morph-status.md +86 -0
- package/stacks/blazor-azure/.claude/commands/morph-troubleshoot.md +122 -0
- package/stacks/blazor-azure/.morph/.morphversion +5 -5
- package/stacks/blazor-azure/.morph/archive/.gitkeep +25 -0
- package/stacks/blazor-azure/.morph/config/config.json +9 -0
- package/stacks/blazor-azure/.morph/features/.gitkeep +25 -0
- package/stacks/blazor-azure/.morph/project/context/README.md +17 -0
- package/stacks/blazor-azure/.morph/schemas/agent.schema.json +296 -0
- package/stacks/blazor-azure/.morph/schemas/tasks.schema.json +220 -0
- package/stacks/blazor-azure/.morph/specs/.gitkeep +20 -0
- package/stacks/blazor-azure/.morph/standards/ai-agents/blazor-ui.md +364 -0
- package/stacks/blazor-azure/.morph/standards/ai-agents/production.md +415 -0
- package/stacks/blazor-azure/.morph/standards/ai-agents/setup.md +418 -0
- package/stacks/blazor-azure/.morph/standards/ai-agents/team-orchestration.md +479 -0
- package/stacks/blazor-azure/.morph/standards/ai-agents/workflows.md +354 -0
- package/stacks/blazor-azure/.morph/standards/architecture/ddd/aggregates.md +120 -0
- package/stacks/blazor-azure/.morph/standards/architecture/ddd/entities.md +99 -0
- package/stacks/blazor-azure/.morph/standards/architecture/ddd/value-objects.md +124 -0
- package/stacks/blazor-azure/.morph/standards/backend/api/minimal-api.md +494 -0
- package/stacks/blazor-azure/.morph/standards/backend/api/rest.md +492 -0
- package/stacks/blazor-azure/.morph/standards/backend/api/validation.md +88 -0
- package/stacks/blazor-azure/.morph/standards/backend/authentication/passkeys.md +428 -0
- package/stacks/blazor-azure/.morph/standards/backend/database/ef-core.md +199 -0
- package/stacks/blazor-azure/.morph/standards/backend/database/migrations.md +393 -0
- package/stacks/blazor-azure/.morph/standards/backend/database/postgresql/database.md +352 -0
- package/stacks/blazor-azure/.morph/standards/backend/database/repository-patterns.md +528 -0
- package/stacks/blazor-azure/.morph/standards/backend/database/vector-search-rag.md +541 -0
- package/stacks/blazor-azure/.morph/standards/backend/dotnet/async.md +366 -0
- package/stacks/blazor-azure/.morph/standards/backend/dotnet/core.md +117 -0
- package/stacks/blazor-azure/.morph/standards/backend/dotnet/di.md +439 -0
- package/stacks/blazor-azure/.morph/standards/backend/dotnet/program-cs-checklist.md +92 -0
- package/stacks/blazor-azure/.morph/standards/backend/integrations/asaas/asaas-api.md +216 -0
- package/stacks/blazor-azure/.morph/standards/backend/integrations/clerk/clerk-auth.md +290 -0
- package/stacks/blazor-azure/.morph/standards/backend/integrations/hangfire/hangfire-jobs.md +350 -0
- package/stacks/blazor-azure/.morph/standards/backend/integrations/resend/resend-email.md +385 -0
- package/stacks/blazor-azure/.morph/standards/context/analytics.md +96 -0
- package/stacks/blazor-azure/.morph/standards/context/bundles.md +110 -0
- package/stacks/blazor-azure/.morph/standards/context/priming.md +78 -0
- package/stacks/blazor-azure/.morph/standards/core/architecture.md +185 -0
- package/stacks/blazor-azure/.morph/standards/core/coding.md +214 -0
- package/stacks/blazor-azure/.morph/standards/core/git-branching-strategy.md +403 -0
- package/stacks/blazor-azure/.morph/standards/core/git.md +185 -0
- package/stacks/blazor-azure/.morph/standards/core/testing.md +295 -0
- package/stacks/blazor-azure/.morph/standards/data/nosql/blob-storage.md +102 -0
- package/stacks/blazor-azure/.morph/standards/data/nosql/cache/redis.md +97 -0
- package/stacks/blazor-azure/.morph/standards/data/nosql/cosmos-db.md +118 -0
- package/stacks/blazor-azure/.morph/standards/data/vector-search/azure-ai-search.md +121 -0
- package/stacks/blazor-azure/.morph/standards/data/vector-search/rag-chunking.md +104 -0
- package/stacks/blazor-azure/.morph/standards/frontend/blazor/design-checklist.md +222 -0
- package/stacks/blazor-azure/.morph/standards/frontend/blazor/fluent-ui-setup.md +595 -0
- package/stacks/blazor-azure/.morph/standards/frontend/blazor/fluent-ui.md +137 -0
- package/stacks/blazor-azure/.morph/standards/frontend/blazor/html-conversion.md +184 -0
- package/stacks/blazor-azure/.morph/standards/frontend/blazor/lifecycle.md +195 -0
- package/stacks/blazor-azure/.morph/standards/frontend/blazor/pitfalls.md +198 -0
- package/stacks/blazor-azure/.morph/standards/frontend/blazor/state.md +191 -0
- package/stacks/blazor-azure/.morph/standards/frontend/design-system/animations.md +151 -0
- package/stacks/blazor-azure/.morph/standards/frontend/design-system/naming.md +64 -0
- package/stacks/blazor-azure/.morph/standards/frontend/nextjs/nextjs-patterns.md +198 -0
- package/stacks/blazor-azure/.morph/standards/infrastructure/azure/azure.md +624 -0
- package/stacks/blazor-azure/.morph/standards/infrastructure/azure/bicep/bicep-patterns.md +422 -0
- package/stacks/blazor-azure/.morph/standards/infrastructure/azure/devops/azure-devops-setup.md +516 -0
- package/stacks/blazor-azure/.morph/standards/infrastructure/azure/devops/local-development.md +520 -0
- package/stacks/blazor-azure/.morph/standards/infrastructure/azure/services/functions.md +486 -0
- package/stacks/blazor-azure/.morph/standards/infrastructure/azure/services/service-bus.md +459 -0
- package/stacks/blazor-azure/.morph/standards/infrastructure/azure/services/storage.md +407 -0
- package/stacks/blazor-azure/.morph/standards/infrastructure/docker/easypanel-deploy.md +196 -0
- package/stacks/blazor-azure/.morph/standards/infrastructure/supabase/mcp-setup.md +252 -0
- package/stacks/blazor-azure/.morph/standards/infrastructure/supabase/supabase-auth.md +176 -0
- package/stacks/blazor-azure/.morph/standards/infrastructure/supabase/supabase-pgvector.md +169 -0
- package/stacks/blazor-azure/.morph/standards/infrastructure/supabase/supabase-rls.md +184 -0
- package/stacks/blazor-azure/.morph/standards/infrastructure/supabase/supabase-storage.md +153 -0
- package/stacks/blazor-azure/.morph/standards/integration/api/graphql.md +91 -0
- package/stacks/blazor-azure/.morph/standards/integration/api/grpc.md +114 -0
- package/stacks/blazor-azure/.morph/standards/integration/api/rest-design.md +95 -0
- package/stacks/blazor-azure/.morph/standards/integration/event-driven/cqrs.md +101 -0
- package/stacks/blazor-azure/.morph/standards/integration/event-driven/event-sourcing.md +124 -0
- package/stacks/blazor-azure/.morph/standards/integration/event-driven/service-bus.md +95 -0
- package/stacks/blazor-azure/.morph/standards/observability/logging.md +131 -0
- package/stacks/blazor-azure/.morph/standards/observability/metrics.md +121 -0
- package/stacks/blazor-azure/.morph/standards/observability/monitoring.md +114 -0
- package/stacks/blazor-azure/.morph/standards/observability/tracing.md +132 -0
- package/stacks/blazor-azure/.morph/standards/workflows/parallel-execution.md +112 -0
- package/stacks/blazor-azure/.morph/standards/workflows/thread-management.md +113 -0
- package/stacks/blazor-azure/.morph/test-infra/example.bicep +59 -0
- package/stacks/blazor-azure/CLAUDE.md +106 -101
- package/stacks/nextjs-supabase/.claude/commands/morph-apply.md +221 -0
- package/stacks/nextjs-supabase/.claude/commands/morph-archive.md +79 -0
- package/stacks/nextjs-supabase/.claude/commands/morph-deploy.md +529 -0
- package/stacks/nextjs-supabase/.claude/commands/morph-infra.md +209 -0
- package/stacks/nextjs-supabase/.claude/commands/morph-preflight.md +227 -0
- package/stacks/nextjs-supabase/.claude/commands/morph-proposal.md +122 -0
- package/stacks/nextjs-supabase/.claude/commands/morph-status.md +86 -0
- package/stacks/nextjs-supabase/.claude/commands/morph-troubleshoot.md +122 -0
- package/stacks/nextjs-supabase/.morph/.morphversion +5 -0
- package/stacks/nextjs-supabase/.morph/config/agents.json +730 -127
- package/stacks/nextjs-supabase/.morph/config/config.json +9 -0
- package/stacks/nextjs-supabase/.morph/project/context/README.md +17 -0
- package/stacks/nextjs-supabase/.morph/standards/ai-agents/blazor-ui.md +364 -0
- package/stacks/nextjs-supabase/.morph/standards/ai-agents/production.md +415 -0
- package/stacks/nextjs-supabase/.morph/standards/ai-agents/setup.md +418 -0
- package/stacks/nextjs-supabase/.morph/standards/ai-agents/team-orchestration.md +479 -0
- package/stacks/nextjs-supabase/.morph/standards/ai-agents/workflows.md +354 -0
- package/stacks/nextjs-supabase/.morph/standards/architecture/ddd/aggregates.md +120 -0
- package/stacks/nextjs-supabase/.morph/standards/architecture/ddd/entities.md +99 -0
- package/stacks/nextjs-supabase/.morph/standards/architecture/ddd/value-objects.md +124 -0
- package/stacks/nextjs-supabase/.morph/standards/backend/api/minimal-api.md +494 -0
- package/stacks/nextjs-supabase/.morph/standards/backend/api/rest.md +492 -0
- package/stacks/nextjs-supabase/.morph/standards/backend/api/validation.md +88 -0
- package/stacks/nextjs-supabase/.morph/standards/backend/authentication/passkeys.md +428 -0
- package/stacks/nextjs-supabase/.morph/standards/backend/database/ef-core.md +199 -0
- package/stacks/nextjs-supabase/.morph/standards/backend/database/migrations.md +393 -0
- package/stacks/nextjs-supabase/.morph/standards/backend/database/postgresql/database.md +352 -0
- package/stacks/nextjs-supabase/.morph/standards/backend/database/repository-patterns.md +528 -0
- package/stacks/nextjs-supabase/.morph/standards/backend/database/vector-search-rag.md +541 -0
- package/stacks/nextjs-supabase/.morph/standards/backend/dotnet/async.md +366 -0
- package/stacks/nextjs-supabase/.morph/standards/backend/dotnet/core.md +117 -0
- package/stacks/nextjs-supabase/.morph/standards/backend/dotnet/di.md +439 -0
- package/stacks/nextjs-supabase/.morph/standards/backend/dotnet/program-cs-checklist.md +92 -0
- package/stacks/nextjs-supabase/.morph/standards/backend/integrations/asaas/asaas-api.md +216 -0
- package/stacks/nextjs-supabase/.morph/standards/backend/integrations/clerk/clerk-auth.md +290 -0
- package/stacks/nextjs-supabase/.morph/standards/backend/integrations/hangfire/hangfire-jobs.md +350 -0
- package/stacks/nextjs-supabase/.morph/standards/backend/integrations/resend/resend-email.md +385 -0
- package/stacks/nextjs-supabase/.morph/standards/context/analytics.md +96 -0
- package/stacks/nextjs-supabase/.morph/standards/context/bundles.md +110 -0
- package/stacks/nextjs-supabase/.morph/standards/context/priming.md +78 -0
- package/stacks/nextjs-supabase/.morph/standards/core/architecture.md +185 -0
- package/stacks/nextjs-supabase/.morph/standards/core/coding.md +214 -0
- package/stacks/nextjs-supabase/.morph/standards/core/git-branching-strategy.md +403 -0
- package/stacks/nextjs-supabase/.morph/standards/core/git.md +185 -0
- package/stacks/nextjs-supabase/.morph/standards/core/testing.md +295 -0
- package/stacks/nextjs-supabase/.morph/standards/data/nosql/blob-storage.md +102 -0
- package/stacks/nextjs-supabase/.morph/standards/data/nosql/cache/redis.md +97 -0
- package/stacks/nextjs-supabase/.morph/standards/data/nosql/cosmos-db.md +118 -0
- package/stacks/nextjs-supabase/.morph/standards/data/vector-search/azure-ai-search.md +121 -0
- package/stacks/nextjs-supabase/.morph/standards/data/vector-search/rag-chunking.md +104 -0
- package/stacks/nextjs-supabase/.morph/standards/frontend/blazor/design-checklist.md +222 -0
- package/stacks/nextjs-supabase/.morph/standards/frontend/blazor/fluent-ui-setup.md +595 -0
- package/stacks/nextjs-supabase/.morph/standards/frontend/blazor/fluent-ui.md +137 -0
- package/stacks/nextjs-supabase/.morph/standards/frontend/blazor/html-conversion.md +184 -0
- package/stacks/nextjs-supabase/.morph/standards/frontend/blazor/lifecycle.md +195 -0
- package/stacks/nextjs-supabase/.morph/standards/frontend/blazor/pitfalls.md +198 -0
- package/stacks/nextjs-supabase/.morph/standards/frontend/blazor/state.md +191 -0
- package/stacks/nextjs-supabase/.morph/standards/frontend/design-system/animations.md +151 -0
- package/stacks/nextjs-supabase/.morph/standards/frontend/design-system/naming.md +64 -0
- package/stacks/nextjs-supabase/.morph/standards/frontend/nextjs/nextjs-patterns.md +198 -0
- package/stacks/nextjs-supabase/.morph/standards/infrastructure/azure/azure.md +624 -0
- package/stacks/nextjs-supabase/.morph/standards/infrastructure/azure/bicep/bicep-patterns.md +422 -0
- package/stacks/nextjs-supabase/.morph/standards/infrastructure/azure/devops/azure-devops-setup.md +516 -0
- package/stacks/nextjs-supabase/.morph/standards/infrastructure/azure/devops/local-development.md +520 -0
- package/stacks/nextjs-supabase/.morph/standards/infrastructure/azure/services/functions.md +486 -0
- package/stacks/nextjs-supabase/.morph/standards/infrastructure/azure/services/service-bus.md +459 -0
- package/stacks/nextjs-supabase/.morph/standards/infrastructure/azure/services/storage.md +407 -0
- package/stacks/nextjs-supabase/.morph/standards/infrastructure/docker/easypanel-deploy.md +196 -0
- package/stacks/nextjs-supabase/.morph/standards/infrastructure/supabase/mcp-setup.md +252 -0
- package/stacks/nextjs-supabase/.morph/standards/infrastructure/supabase/supabase-auth.md +176 -0
- package/stacks/nextjs-supabase/.morph/standards/infrastructure/supabase/supabase-pgvector.md +169 -0
- package/stacks/nextjs-supabase/.morph/standards/infrastructure/supabase/supabase-rls.md +184 -0
- package/stacks/nextjs-supabase/.morph/standards/infrastructure/supabase/supabase-storage.md +153 -0
- package/stacks/nextjs-supabase/.morph/standards/integration/api/graphql.md +91 -0
- package/stacks/nextjs-supabase/.morph/standards/integration/api/grpc.md +114 -0
- package/stacks/nextjs-supabase/.morph/standards/integration/api/rest-design.md +95 -0
- package/stacks/nextjs-supabase/.morph/standards/integration/event-driven/cqrs.md +101 -0
- package/stacks/nextjs-supabase/.morph/standards/integration/event-driven/event-sourcing.md +124 -0
- package/stacks/nextjs-supabase/.morph/standards/integration/event-driven/service-bus.md +95 -0
- package/stacks/nextjs-supabase/.morph/standards/observability/logging.md +131 -0
- package/stacks/nextjs-supabase/.morph/standards/observability/metrics.md +121 -0
- package/stacks/nextjs-supabase/.morph/standards/observability/monitoring.md +114 -0
- package/stacks/nextjs-supabase/.morph/standards/observability/tracing.md +132 -0
- package/stacks/nextjs-supabase/.morph/standards/workflows/parallel-execution.md +112 -0
- package/stacks/nextjs-supabase/.morph/standards/workflows/thread-management.md +113 -0
- package/stacks/nextjs-supabase/CLAUDE.md +69 -63
- package/stacks/blazor-azure/.morph/templates/.gitkeep +0 -0
- package/stacks/blazor-azure/.morph/templates/infrastructure/github/workflows/cd-prod.yml.hbs +0 -41
- package/stacks/blazor-azure/.morph/templates/infrastructure/github/workflows/cd-staging.yml.hbs +0 -24
- package/stacks/blazor-azure/.morph/templates/infrastructure/github/workflows/ci-build.yml.hbs +0 -23
- package/stacks/nextjs-supabase/.morph/templates/.gitkeep +0 -0
- package/stacks/nextjs-supabase/.morph/templates/infrastructure/github/workflows/cd-prod.yml.hbs +0 -22
- package/stacks/nextjs-supabase/.morph/templates/infrastructure/github/workflows/cd-staging.yml.hbs +0 -22
- package/stacks/nextjs-supabase/.morph/templates/infrastructure/github/workflows/ci-build.yml.hbs +0 -35
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
# Passkeys/WebAuthn - Autenticação Sem Senhas (.NET 10)
|
|
2
|
+
|
|
3
|
+
> **Scope:** blazor-azure
|
|
4
|
+
> **Layer:** 2 (on keyword)
|
|
5
|
+
> **Keywords:** passkeys, azure entra, fido2, webauthn, authentication
|
|
6
|
+
> **Load When:** passkeys or authentication keywords detected
|
|
7
|
+
|
|
8
|
+
**Novidade .NET 10:** Suporte nativo a Passkeys/WebAuthn integrado no ASP.NET Core Identity.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 🎯 O Que São Passkeys?
|
|
13
|
+
|
|
14
|
+
**Passkeys** são credenciais criptográficas que substituem senhas tradicionais.
|
|
15
|
+
|
|
16
|
+
### Características
|
|
17
|
+
|
|
18
|
+
| Aspecto | Descrição |
|
|
19
|
+
|---------|-----------|
|
|
20
|
+
| **Segurança** | Resistentes a phishing, não podem ser roubadas |
|
|
21
|
+
| **Usabilidade** | Login com biometria ou PIN do dispositivo |
|
|
22
|
+
| **Privacidade** | Chave privada nunca sai do dispositivo |
|
|
23
|
+
| **Padrão** | WebAuthn (W3C) + FIDO2 |
|
|
24
|
+
|
|
25
|
+
### Como Funciona
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
1. Usuário solicita criação de passkey
|
|
29
|
+
2. Sistema gera par de chaves (pública/privada)
|
|
30
|
+
3. Chave pública → Servidor
|
|
31
|
+
4. Chave privada → Authenticator (Windows Hello, smartphone, security key)
|
|
32
|
+
5. Login: Servidor desafia → Authenticator assina → Servidor verifica
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Authenticators suportados:**
|
|
36
|
+
- Windows Hello
|
|
37
|
+
- Face ID / Touch ID (Apple)
|
|
38
|
+
- Biometria Android
|
|
39
|
+
- Security keys (YubiKey, etc.)
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 🚀 Setup em Blazor Web App
|
|
44
|
+
|
|
45
|
+
### 1. Criar Projeto com Identity
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
dotnet new blazor --auth Individual -o MyApp
|
|
49
|
+
cd MyApp
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Importante:** Escolher "Individual User Accounts" habilita passkeys automaticamente.
|
|
53
|
+
|
|
54
|
+
### 2. Estrutura Gerada
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
MyApp/
|
|
58
|
+
├── Components/
|
|
59
|
+
│ └── Account/
|
|
60
|
+
│ ├── Pages/
|
|
61
|
+
│ │ ├── Manage/
|
|
62
|
+
│ │ │ └── Passkeys.razor ← Gerenciamento de passkeys
|
|
63
|
+
│ │ ├── Login.razor ← Login com passkey
|
|
64
|
+
│ │ └── Register.razor
|
|
65
|
+
│ └── IdentityUserAccessor.cs
|
|
66
|
+
├── Data/
|
|
67
|
+
│ ├── ApplicationDbContext.cs
|
|
68
|
+
│ └── ApplicationUser.cs
|
|
69
|
+
└── Program.cs
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 3. Configuração Automática no Program.cs
|
|
73
|
+
|
|
74
|
+
```csharp
|
|
75
|
+
// Já vem configurado no template
|
|
76
|
+
builder.Services.AddIdentityCore<ApplicationUser>(options =>
|
|
77
|
+
{
|
|
78
|
+
options.SignIn.RequireConfirmedAccount = true;
|
|
79
|
+
|
|
80
|
+
// Passkeys habilitados por padrão
|
|
81
|
+
options.Stores.MaxLengthForKeys = 128;
|
|
82
|
+
})
|
|
83
|
+
.AddEntityFrameworkStores<ApplicationDbContext>()
|
|
84
|
+
.AddSignInManager()
|
|
85
|
+
.AddDefaultTokenProviders();
|
|
86
|
+
|
|
87
|
+
// WebAuthn/Passkeys configurado automaticamente
|
|
88
|
+
builder.Services.AddAuthentication(options =>
|
|
89
|
+
{
|
|
90
|
+
options.DefaultScheme = IdentityConstants.ApplicationScheme;
|
|
91
|
+
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
|
|
92
|
+
})
|
|
93
|
+
.AddIdentityCookies();
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## 📱 Fluxo de Uso
|
|
99
|
+
|
|
100
|
+
### Criar Passkey
|
|
101
|
+
|
|
102
|
+
1. Usuário loga com email/senha (primeira vez)
|
|
103
|
+
2. Acessa perfil → Aba "Passkeys"
|
|
104
|
+
3. Clica "Add Passkey"
|
|
105
|
+
4. Sistema solicita authenticator (Windows Hello, celular, etc.)
|
|
106
|
+
5. Usuário confirma identidade (biometria/PIN)
|
|
107
|
+
6. Passkey é criada e nomeada
|
|
108
|
+
7. Salva no banco de dados
|
|
109
|
+
|
|
110
|
+
### Login com Passkey
|
|
111
|
+
|
|
112
|
+
1. Usuário acessa página de login
|
|
113
|
+
2. Clica "Login with Passkey"
|
|
114
|
+
3. Sistema envia desafio
|
|
115
|
+
4. Authenticator assina desafio
|
|
116
|
+
5. Servidor verifica assinatura
|
|
117
|
+
6. Usuário autenticado
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## 💻 Implementação Customizada
|
|
122
|
+
|
|
123
|
+
### Adicionar Passkeys a Projeto Existente
|
|
124
|
+
|
|
125
|
+
Se você tem um projeto Blazor sem passkeys, use o **scaffolder**:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
dotnet tool install -g dotnet-aspnet-codegenerator
|
|
129
|
+
|
|
130
|
+
dotnet aspnet-codegenerator identity \
|
|
131
|
+
--useDefaultUI \
|
|
132
|
+
--dbContext ApplicationDbContext \
|
|
133
|
+
--files "Account.Manage.Passkeys;Account.Login"
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Isso adiciona:
|
|
137
|
+
- `Components/Account/Pages/Manage/Passkeys.razor`
|
|
138
|
+
- `Components/Account/Pages/Login.razor` (atualizado)
|
|
139
|
+
|
|
140
|
+
### Verificar Suporte a Passkeys
|
|
141
|
+
|
|
142
|
+
```csharp
|
|
143
|
+
@inject IPasskeyService PasskeyService
|
|
144
|
+
|
|
145
|
+
@code {
|
|
146
|
+
private bool _supportsPasskeys;
|
|
147
|
+
|
|
148
|
+
protected override async Task OnInitializedAsync()
|
|
149
|
+
{
|
|
150
|
+
_supportsPasskeys = await PasskeyService.IsSupportedAsync();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Listar Passkeys do Usuário
|
|
156
|
+
|
|
157
|
+
```csharp
|
|
158
|
+
@page "/manage/passkeys"
|
|
159
|
+
@inject IPasskeyService PasskeyService
|
|
160
|
+
@inject UserManager<ApplicationUser> UserManager
|
|
161
|
+
|
|
162
|
+
<h3>Minhas Passkeys</h3>
|
|
163
|
+
|
|
164
|
+
@if (_passkeys is null)
|
|
165
|
+
{
|
|
166
|
+
<p>Carregando...</p>
|
|
167
|
+
}
|
|
168
|
+
else if (!_passkeys.Any())
|
|
169
|
+
{
|
|
170
|
+
<p>Você não tem passkeys configuradas.</p>
|
|
171
|
+
}
|
|
172
|
+
else
|
|
173
|
+
{
|
|
174
|
+
<ul>
|
|
175
|
+
@foreach (var passkey in _passkeys)
|
|
176
|
+
{
|
|
177
|
+
<li>
|
|
178
|
+
@passkey.Name (@passkey.CreatedAt.ToString("dd/MM/yyyy"))
|
|
179
|
+
<button @onclick="() => RemovePasskey(passkey.Id)">Remover</button>
|
|
180
|
+
</li>
|
|
181
|
+
}
|
|
182
|
+
</ul>
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
<button @onclick="AddPasskey">Adicionar Passkey</button>
|
|
186
|
+
|
|
187
|
+
@code {
|
|
188
|
+
private List<Passkey>? _passkeys;
|
|
189
|
+
|
|
190
|
+
protected override async Task OnInitializedAsync()
|
|
191
|
+
{
|
|
192
|
+
var user = await UserManager.GetUserAsync(User);
|
|
193
|
+
_passkeys = await PasskeyService.GetUserPasskeysAsync(user!.Id);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private async Task AddPasskey()
|
|
197
|
+
{
|
|
198
|
+
var user = await UserManager.GetUserAsync(User);
|
|
199
|
+
await PasskeyService.CreatePasskeyAsync(user!.Id);
|
|
200
|
+
await OnInitializedAsync(); // Recarregar lista
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private async Task RemovePasskey(string passkeyId)
|
|
204
|
+
{
|
|
205
|
+
await PasskeyService.RemovePasskeyAsync(passkeyId);
|
|
206
|
+
await OnInitializedAsync(); // Recarregar lista
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## 🗄️ Modelo de Dados
|
|
214
|
+
|
|
215
|
+
### Entidade Passkey
|
|
216
|
+
|
|
217
|
+
```csharp
|
|
218
|
+
public class Passkey
|
|
219
|
+
{
|
|
220
|
+
public string Id { get; set; } = Guid.NewGuid().ToString();
|
|
221
|
+
public string UserId { get; set; } = null!;
|
|
222
|
+
public string Name { get; set; } = "Passkey";
|
|
223
|
+
public byte[] CredentialId { get; set; } = Array.Empty<byte>();
|
|
224
|
+
public byte[] PublicKey { get; set; } = Array.Empty<byte>();
|
|
225
|
+
public int SignatureCounter { get; set; }
|
|
226
|
+
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
|
227
|
+
public DateTime? LastUsedAt { get; set; }
|
|
228
|
+
|
|
229
|
+
// Relacionamento
|
|
230
|
+
public ApplicationUser User { get; set; } = null!;
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### DbContext
|
|
235
|
+
|
|
236
|
+
```csharp
|
|
237
|
+
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
|
|
238
|
+
{
|
|
239
|
+
public DbSet<Passkey> Passkeys { get; set; }
|
|
240
|
+
|
|
241
|
+
protected override void OnModelCreating(ModelBuilder builder)
|
|
242
|
+
{
|
|
243
|
+
base.OnModelCreating(builder);
|
|
244
|
+
|
|
245
|
+
builder.Entity<Passkey>(entity =>
|
|
246
|
+
{
|
|
247
|
+
entity.HasKey(p => p.Id);
|
|
248
|
+
entity.HasIndex(p => p.UserId);
|
|
249
|
+
entity.HasIndex(p => p.CredentialId).IsUnique();
|
|
250
|
+
|
|
251
|
+
entity.HasOne(p => p.User)
|
|
252
|
+
.WithMany()
|
|
253
|
+
.HasForeignKey(p => p.UserId)
|
|
254
|
+
.OnDelete(DeleteBehavior.Cascade);
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Migration
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
dotnet ef migrations add AddPasskeys
|
|
264
|
+
dotnet ef database update
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## 🔒 Segurança
|
|
270
|
+
|
|
271
|
+
### Configurações Recomendadas
|
|
272
|
+
|
|
273
|
+
```csharp
|
|
274
|
+
builder.Services.AddAuthentication()
|
|
275
|
+
.AddWebAuthn(options =>
|
|
276
|
+
{
|
|
277
|
+
// Nome do site (aparece no authenticator)
|
|
278
|
+
options.RelyingPartyId = "myapp.com";
|
|
279
|
+
options.RelyingPartyName = "My Application";
|
|
280
|
+
|
|
281
|
+
// Origem permitida
|
|
282
|
+
options.Origins = new[] { "https://myapp.com" };
|
|
283
|
+
|
|
284
|
+
// Timeout de autenticação
|
|
285
|
+
options.Timeout = TimeSpan.FromSeconds(60);
|
|
286
|
+
|
|
287
|
+
// Tipo de authenticator
|
|
288
|
+
options.AuthenticatorSelection = new()
|
|
289
|
+
{
|
|
290
|
+
RequireResidentKey = false,
|
|
291
|
+
UserVerification = UserVerificationRequirement.Preferred
|
|
292
|
+
};
|
|
293
|
+
});
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### User Verification
|
|
297
|
+
|
|
298
|
+
| Modo | Descrição | Quando Usar |
|
|
299
|
+
|------|-----------|-------------|
|
|
300
|
+
| `Required` | Exige biometria/PIN sempre | Alto risco |
|
|
301
|
+
| `Preferred` | Prefere biometria mas aceita alternativa | Padrão recomendado |
|
|
302
|
+
| `Discouraged` | Não solicita verificação | Não use |
|
|
303
|
+
|
|
304
|
+
### Attestation
|
|
305
|
+
|
|
306
|
+
```csharp
|
|
307
|
+
options.Attestation = AttestationConveyancePreference.None;
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
**Valores:**
|
|
311
|
+
- `None`: Não requer attestation (mais compatível)
|
|
312
|
+
- `Indirect`: Valida authenticator via CA
|
|
313
|
+
- `Direct`: Requer attestation direta (mais restritivo)
|
|
314
|
+
|
|
315
|
+
**Recomendação:** Use `None` para máxima compatibilidade.
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## 🧪 Testando Passkeys
|
|
320
|
+
|
|
321
|
+
### Ambiente de Desenvolvimento
|
|
322
|
+
|
|
323
|
+
1. **HTTPS obrigatório:** WebAuthn só funciona com HTTPS (ou localhost)
|
|
324
|
+
2. **Windows Hello:** Habilite no Windows
|
|
325
|
+
3. **Chrome DevTools:** Use Virtual Authenticator para testes
|
|
326
|
+
|
|
327
|
+
### Virtual Authenticator (Chrome)
|
|
328
|
+
|
|
329
|
+
1. F12 → More Tools → WebAuthn
|
|
330
|
+
2. Enable virtual authenticator environment
|
|
331
|
+
3. Add authenticator (escolha tipo: USB, NFC, Internal)
|
|
332
|
+
4. Teste criação e login
|
|
333
|
+
|
|
334
|
+
### Smartphone como Authenticator
|
|
335
|
+
|
|
336
|
+
1. Use HTTPS (não localhost)
|
|
337
|
+
2. Escaneie QR code gerado
|
|
338
|
+
3. Confirme com biometria do celular
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
## 🐛 Troubleshooting
|
|
343
|
+
|
|
344
|
+
### Erro: "WebAuthn not supported"
|
|
345
|
+
|
|
346
|
+
**Causa:** Navegador antigo ou HTTP (não HTTPS)
|
|
347
|
+
|
|
348
|
+
**Solução:**
|
|
349
|
+
- Use HTTPS em produção
|
|
350
|
+
- Localhost funciona com HTTP (somente dev)
|
|
351
|
+
- Atualize navegador
|
|
352
|
+
|
|
353
|
+
### Erro: "User verification failed"
|
|
354
|
+
|
|
355
|
+
**Causa:** Authenticator não configurado ou cancelado
|
|
356
|
+
|
|
357
|
+
**Solução:**
|
|
358
|
+
- Configure Windows Hello / Touch ID
|
|
359
|
+
- Tente outro authenticator
|
|
360
|
+
- Verifique `UserVerification = Preferred`
|
|
361
|
+
|
|
362
|
+
### Passkey não aparece em outro dispositivo
|
|
363
|
+
|
|
364
|
+
**Causa:** Passkeys não sincronizam automaticamente (depende do authenticator)
|
|
365
|
+
|
|
366
|
+
**Solução:**
|
|
367
|
+
- Use authenticator com sync (ex: Google Password Manager)
|
|
368
|
+
- Ou crie passkey separada por dispositivo
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## 📊 Migração Gradual
|
|
373
|
+
|
|
374
|
+
### Permitir Email/Senha + Passkeys
|
|
375
|
+
|
|
376
|
+
```csharp
|
|
377
|
+
// Login.razor
|
|
378
|
+
<EditForm Model="Input" OnValidSubmit="LoginAsync">
|
|
379
|
+
<InputText @bind-Value="Input.Email" />
|
|
380
|
+
<InputText @bind-Value="Input.Password" type="password" />
|
|
381
|
+
<button type="submit">Login with Password</button>
|
|
382
|
+
</EditForm>
|
|
383
|
+
|
|
384
|
+
<hr />
|
|
385
|
+
|
|
386
|
+
<button @onclick="LoginWithPasskey">Login with Passkey</button>
|
|
387
|
+
|
|
388
|
+
@code {
|
|
389
|
+
private async Task LoginAsync()
|
|
390
|
+
{
|
|
391
|
+
// Login tradicional com senha
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private async Task LoginWithPasskey()
|
|
395
|
+
{
|
|
396
|
+
// Login com WebAuthn
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
**Estratégia:** Mantenha senha como fallback, incentive passkeys.
|
|
402
|
+
|
|
403
|
+
---
|
|
404
|
+
|
|
405
|
+
## ✅ Checklist de Implementação
|
|
406
|
+
|
|
407
|
+
- [ ] Projeto criado com `--auth Individual` ou scaffolder usado
|
|
408
|
+
- [ ] DbContext inclui tabela `Passkeys`
|
|
409
|
+
- [ ] Migration executada
|
|
410
|
+
- [ ] HTTPS habilitado (dev e prod)
|
|
411
|
+
- [ ] Página de gerenciamento de passkeys funcional
|
|
412
|
+
- [ ] Login com passkey funcional
|
|
413
|
+
- [ ] Fallback para email/senha disponível
|
|
414
|
+
- [ ] Testado com Windows Hello ou smartphone
|
|
415
|
+
- [ ] `RelyingPartyId` configurado corretamente
|
|
416
|
+
|
|
417
|
+
---
|
|
418
|
+
|
|
419
|
+
## 📚 Referências
|
|
420
|
+
|
|
421
|
+
- [WebAuthn Specification (W3C)](https://w3c.github.io/webauthn/)
|
|
422
|
+
- [FIDO Alliance](https://fidoalliance.org/)
|
|
423
|
+
- [ASP.NET Core Identity - Passkeys](https://learn.microsoft.com/aspnet/core/security/authentication/identity/passkeys)
|
|
424
|
+
- [Chrome WebAuthn DevTools](https://developer.chrome.com/docs/devtools/webauthn/)
|
|
425
|
+
|
|
426
|
+
---
|
|
427
|
+
|
|
428
|
+
*MORPH-SPEC by Polymorphism Tech*
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Blazor Server + EF Core
|
|
2
|
+
|
|
3
|
+
> **Scope:** blazor-azure,nextjs-supabase
|
|
4
|
+
> **Layer:** 1 (on domain)
|
|
5
|
+
> **Keywords:** efcore, entity framework, dbcontext, dbfactory, concurrency
|
|
6
|
+
> **Load When:** database work
|
|
7
|
+
|
|
8
|
+
Patterns for Blazor Server + EF Core concurrency and background operations.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Core Problem: DbContext Concurrency
|
|
13
|
+
|
|
14
|
+
Blazor Server scoped services = circuit lifecycle (SignalR), NOT HTTP requests.
|
|
15
|
+
`Task.Run` or fire-and-forget share the same scoped DbContext → concurrent access violations.
|
|
16
|
+
|
|
17
|
+
**Rule:** Separate thread from HTTP request = isolated DbContext via `IDbContextFactory`.
|
|
18
|
+
|
|
19
|
+
| Scenario | Pattern |
|
|
20
|
+
|----------|---------|
|
|
21
|
+
| Regular component ops | Scoped DbContext/Repository |
|
|
22
|
+
| `Task.Run` / fire-and-forget | IDbContextFactory |
|
|
23
|
+
| Hangfire / background jobs | IDbContextFactory |
|
|
24
|
+
| SignalR hub handlers | IDbContextFactory |
|
|
25
|
+
| Hosted services | IDbContextFactory |
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Pattern: Repository Factory
|
|
30
|
+
|
|
31
|
+
```csharp
|
|
32
|
+
// Interfaces
|
|
33
|
+
public interface IOrderRepositoryBase
|
|
34
|
+
{
|
|
35
|
+
Task<Order?> GetByIdAsync(Guid id, CancellationToken ct = default);
|
|
36
|
+
Task UpdateAsync(Order order, CancellationToken ct = default);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public interface IOrderRepository : IOrderRepositoryBase
|
|
40
|
+
{
|
|
41
|
+
Task<List<Order>> GetPendingAsync(CancellationToken ct = default);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public interface IScopedOrderRepository : IOrderRepositoryBase, IAsyncDisposable { }
|
|
45
|
+
|
|
46
|
+
public interface IOrderRepositoryFactory
|
|
47
|
+
{
|
|
48
|
+
IScopedOrderRepository CreateScoped();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Implementation
|
|
52
|
+
public abstract class OrderRepositoryBase(AppDbContext context)
|
|
53
|
+
{
|
|
54
|
+
protected readonly AppDbContext _context = context;
|
|
55
|
+
|
|
56
|
+
public async Task<Order?> GetByIdAsync(Guid id, CancellationToken ct = default)
|
|
57
|
+
=> await _context.Orders.Include(o => o.Items).FirstOrDefaultAsync(o => o.Id == id, ct);
|
|
58
|
+
|
|
59
|
+
public async Task UpdateAsync(Order order, CancellationToken ct = default)
|
|
60
|
+
{
|
|
61
|
+
_context.Orders.Update(order);
|
|
62
|
+
await _context.SaveChangesAsync(ct);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public class ScopedOrderRepository(AppDbContext context) : OrderRepositoryBase(context), IScopedOrderRepository
|
|
67
|
+
{
|
|
68
|
+
public async ValueTask DisposeAsync() => await _context.DisposeAsync();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public class OrderRepositoryFactory(IDbContextFactory<AppDbContext> contextFactory) : IOrderRepositoryFactory
|
|
72
|
+
{
|
|
73
|
+
public IScopedOrderRepository CreateScoped() => new ScopedOrderRepository(contextFactory.CreateDbContext());
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// DI
|
|
77
|
+
builder.Services.AddDbContextFactory<AppDbContext>(o => o.UseSqlServer(conn));
|
|
78
|
+
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
|
|
79
|
+
builder.Services.AddSingleton<IOrderRepositoryFactory, OrderRepositoryFactory>();
|
|
80
|
+
|
|
81
|
+
// Usage
|
|
82
|
+
_ = Task.Run(async () =>
|
|
83
|
+
{
|
|
84
|
+
await using var repo = _repoFactory.CreateScoped();
|
|
85
|
+
var order = await repo.GetByIdAsync(orderId);
|
|
86
|
+
await repo.UpdateAsync(order);
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Background Operations Decision Matrix
|
|
93
|
+
|
|
94
|
+
| Option | When | Cost | Complexity | Resilience |
|
|
95
|
+
|--------|------|------|------------|------------|
|
|
96
|
+
| Task.Run + RepositoryFactory | < 5min, low volume, non-critical | $0 | Low | Low |
|
|
97
|
+
| Background Service + Queue | Long, needs retry, medium volume | $0-16/mo | Medium | Medium |
|
|
98
|
+
| Hangfire | Scheduled/recurring, needs dashboard | $0 (OSS) | Medium | High |
|
|
99
|
+
| Azure Functions Durable | High volume, spikes, orchestration | Pay-per-use | Med-High | High |
|
|
100
|
+
|
|
101
|
+
### Hangfire Quick Setup
|
|
102
|
+
|
|
103
|
+
```csharp
|
|
104
|
+
builder.Services.AddHangfire(c => c.UseSqlServerStorage(conn));
|
|
105
|
+
builder.Services.AddHangfireServer();
|
|
106
|
+
|
|
107
|
+
public class OrderJob(IOrderRepositoryFactory repoFactory)
|
|
108
|
+
{
|
|
109
|
+
[AutomaticRetry(Attempts = 3)]
|
|
110
|
+
public async Task ProcessAsync(Guid id, CancellationToken ct)
|
|
111
|
+
{
|
|
112
|
+
await using var repo = repoFactory.CreateScoped();
|
|
113
|
+
var order = await repo.GetByIdAsync(id, ct);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
BackgroundJob.Enqueue<OrderJob>(x => x.ProcessAsync(orderId, CancellationToken.None));
|
|
118
|
+
RecurringJob.AddOrUpdate<ReportJob>("daily", x => x.GenerateAsync(), Cron.Daily);
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## EF Core Configuration
|
|
124
|
+
|
|
125
|
+
### Navigation Properties
|
|
126
|
+
|
|
127
|
+
```csharp
|
|
128
|
+
// WRONG - Include() silently fails
|
|
129
|
+
builder.HasMany<GeneratedArt>().WithOne(x => x.Order).HasForeignKey(x => x.OrderId);
|
|
130
|
+
|
|
131
|
+
// CORRECT - Specify navigation property
|
|
132
|
+
builder.HasMany(x => x.GeneratedArts).WithOne(x => x.Order).HasForeignKey(x => x.OrderId);
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Rule:** `ICollection<T>` in entity → MUST appear in `HasMany(x => x.Property)`.
|
|
136
|
+
|
|
137
|
+
### Migrations (.NET 10)
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
dotnet ef migrations add <Name> --project src/Infrastructure --startup-project src/Web
|
|
141
|
+
dotnet ef database update --project src/Infrastructure --startup-project src/Web
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
| Environment | Auto-Migration? | Why |
|
|
145
|
+
|-------------|----------------|-----|
|
|
146
|
+
| Development | OK | Fast iteration |
|
|
147
|
+
| Staging | Caution | Test before prod |
|
|
148
|
+
| Production | NO | Use CI/CD pipeline |
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Anti-Patterns
|
|
153
|
+
|
|
154
|
+
| Anti-Pattern | Fix |
|
|
155
|
+
|-------------|-----|
|
|
156
|
+
| Scoped DbContext in `Task.Run` | Use `IDbContextFactory` |
|
|
157
|
+
| Scoped service in singleton | Use factory pattern |
|
|
158
|
+
| Missing `await using` | Always use with scoped repo |
|
|
159
|
+
| `IScopedRepo` inherits full `IRepo` | Inherit only `IBase` |
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Checklist
|
|
164
|
+
|
|
165
|
+
- [ ] Background ops use `IDbContextFactory` / Repository Factory?
|
|
166
|
+
- [ ] Duration/volume assessed against decision matrix?
|
|
167
|
+
- [ ] `HasMany` uses navigation property lambda?
|
|
168
|
+
- [ ] Migration created before `database update`?
|
|
169
|
+
- [ ] `await using` for scoped repositories?
|
|
170
|
+
- [ ] `AsNoTracking()` for read-only queries?
|
|
171
|
+
- [ ] CancellationToken propagated?
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
*MORPH-SPEC by Polymorphism Tech*
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Code Review Checklist
|
|
180
|
+
|
|
181
|
+
Use this checklist during code review for EF Core in Blazor:
|
|
182
|
+
|
|
183
|
+
### DbContext and Concurrency
|
|
184
|
+
|
|
185
|
+
- [ ] **No scoped DbContext used in `Task.Run` or background operations?**
|
|
186
|
+
- [ ] **`IDbContextFactory<T>` used for background/parallel operations?**
|
|
187
|
+
- [ ] **Factory-created contexts are properly disposed (`await using`)?**
|
|
188
|
+
- [ ] **No shared DbContext between concurrent operations?**
|
|
189
|
+
|
|
190
|
+
### File Upload (IBrowserFile)
|
|
191
|
+
|
|
192
|
+
- [ ] **`IBrowserFile` is read only ONCE and bytes stored?**
|
|
193
|
+
- [ ] **`maxAllowedSize` specified in `OpenReadStream()`?**
|
|
194
|
+
- [ ] **File size validated BEFORE opening stream?**
|
|
195
|
+
- [ ] **File extension/type validated?**
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
*MORPH-SPEC by Polymorphism Tech*
|