@polymorphism-tech/morph-spec 4.3.1 → 4.3.3
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 +15 -5
- package/src/commands/project/update.js +7 -3
- 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/config/config.json +9 -0
- package/stacks/blazor-azure/.morph/project/context/README.md +17 -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/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 +345 -345
- 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/config/config.template.json +0 -122
- package/stacks/blazor-azure/.morph/hooks/pre-commit/tests-csharp.sh +0 -61
- package/stacks/blazor-azure/.morph/project.md +0 -160
- package/stacks/blazor-azure/.morph/state.json +0 -18
- 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/config/config.template.json +0 -92
- package/stacks/nextjs-supabase/.morph/hooks/pre-commit/tests-typescript.sh +0 -61
- package/stacks/nextjs-supabase/.morph/project.md +0 -168
- 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,528 @@
|
|
|
1
|
+
# Repository Patterns
|
|
2
|
+
|
|
3
|
+
> **Scope:** blazor-azure,nextjs-supabase
|
|
4
|
+
> **Layer:** 1 (on domain)
|
|
5
|
+
> **Keywords:** repository, pattern, data access, dal, generic repository
|
|
6
|
+
> **Load When:** database work
|
|
7
|
+
|
|
8
|
+
Repository implementation patterns for data access layer
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Basic Repository Pattern
|
|
13
|
+
|
|
14
|
+
### Interface Definition
|
|
15
|
+
|
|
16
|
+
```csharp
|
|
17
|
+
// Domain layer
|
|
18
|
+
public interface IUserRepository
|
|
19
|
+
{
|
|
20
|
+
Task<User?> GetByIdAsync(int id, CancellationToken ct = default);
|
|
21
|
+
Task<List<User>> GetAllAsync(CancellationToken ct = default);
|
|
22
|
+
Task<User> CreateAsync(User user, CancellationToken ct = default);
|
|
23
|
+
Task UpdateAsync(User user, CancellationToken ct = default);
|
|
24
|
+
Task DeleteAsync(int id, CancellationToken ct = default);
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Implementation
|
|
29
|
+
|
|
30
|
+
```csharp
|
|
31
|
+
// Infrastructure layer
|
|
32
|
+
public class UserRepository : IUserRepository
|
|
33
|
+
{
|
|
34
|
+
private readonly AppDbContext _context;
|
|
35
|
+
|
|
36
|
+
public UserRepository(AppDbContext context)
|
|
37
|
+
{
|
|
38
|
+
_context = context;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public async Task<User?> GetByIdAsync(int id, CancellationToken ct = default)
|
|
42
|
+
{
|
|
43
|
+
return await _context.Users
|
|
44
|
+
.AsNoTracking()
|
|
45
|
+
.FirstOrDefaultAsync(u => u.Id == id, ct);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public async Task<List<User>> GetAllAsync(CancellationToken ct = default)
|
|
49
|
+
{
|
|
50
|
+
return await _context.Users
|
|
51
|
+
.AsNoTracking()
|
|
52
|
+
.ToListAsync(ct);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public async Task<User> CreateAsync(User user, CancellationToken ct = default)
|
|
56
|
+
{
|
|
57
|
+
_context.Users.Add(user);
|
|
58
|
+
await _context.SaveChangesAsync(ct);
|
|
59
|
+
return user;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public async Task UpdateAsync(User user, CancellationToken ct = default)
|
|
63
|
+
{
|
|
64
|
+
_context.Users.Update(user);
|
|
65
|
+
await _context.SaveChangesAsync(ct);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public async Task DeleteAsync(int id, CancellationToken ct = default)
|
|
69
|
+
{
|
|
70
|
+
var user = await GetByIdAsync(id, ct);
|
|
71
|
+
if (user != null)
|
|
72
|
+
{
|
|
73
|
+
_context.Users.Remove(user);
|
|
74
|
+
await _context.SaveChangesAsync(ct);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Factory Pattern for Background Operations
|
|
83
|
+
|
|
84
|
+
**Problem:** DbContext injected in scoped services may be disposed before background tasks complete.
|
|
85
|
+
|
|
86
|
+
**Solution:** Use Repository Factory pattern to create isolated DbContext instances.
|
|
87
|
+
|
|
88
|
+
### Interface Base for Segregation
|
|
89
|
+
|
|
90
|
+
```csharp
|
|
91
|
+
// Domain layer - Base interface with common methods
|
|
92
|
+
public interface IOrderRepositoryBase
|
|
93
|
+
{
|
|
94
|
+
Task<Order?> GetByIdAsync(Guid id, CancellationToken ct = default);
|
|
95
|
+
Task UpdateAsync(Order order, CancellationToken ct = default);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Full repository for normal operations
|
|
99
|
+
public interface IOrderRepository : IOrderRepositoryBase
|
|
100
|
+
{
|
|
101
|
+
Task<List<Order>> GetAllAsync(CancellationToken ct = default);
|
|
102
|
+
Task<Order> CreateAsync(Order order, CancellationToken ct = default);
|
|
103
|
+
Task DeleteAsync(Guid id, CancellationToken ct = default);
|
|
104
|
+
// ... more methods
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Scoped repository for background operations (limited methods)
|
|
108
|
+
public interface IScopedOrderRepository : IOrderRepositoryBase, IAsyncDisposable
|
|
109
|
+
{
|
|
110
|
+
// Inherits only GetByIdAsync and UpdateAsync from IOrderRepositoryBase
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Why this works:** Background operations only need limited methods, violating ISP to inherit all methods.
|
|
115
|
+
|
|
116
|
+
### Factory Interface
|
|
117
|
+
|
|
118
|
+
```csharp
|
|
119
|
+
// Domain layer
|
|
120
|
+
public interface IOrderRepositoryFactory
|
|
121
|
+
{
|
|
122
|
+
IScopedOrderRepository CreateScoped();
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Factory Implementation
|
|
127
|
+
|
|
128
|
+
```csharp
|
|
129
|
+
// Infrastructure layer
|
|
130
|
+
public class OrderRepositoryFactory : IOrderRepositoryFactory
|
|
131
|
+
{
|
|
132
|
+
private readonly IDbContextFactory<AppDbContext> _contextFactory;
|
|
133
|
+
|
|
134
|
+
public OrderRepositoryFactory(IDbContextFactory<AppDbContext> contextFactory)
|
|
135
|
+
{
|
|
136
|
+
_contextFactory = contextFactory;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
public IScopedOrderRepository CreateScoped()
|
|
140
|
+
{
|
|
141
|
+
var context = _contextFactory.CreateDbContext();
|
|
142
|
+
return new ScopedOrderRepository(context);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Scoped repository implementation
|
|
147
|
+
public class ScopedOrderRepository : IScopedOrderRepository
|
|
148
|
+
{
|
|
149
|
+
private readonly AppDbContext _context;
|
|
150
|
+
|
|
151
|
+
public ScopedOrderRepository(AppDbContext context)
|
|
152
|
+
{
|
|
153
|
+
_context = context;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
public async Task<Order?> GetByIdAsync(Guid id, CancellationToken ct = default)
|
|
157
|
+
{
|
|
158
|
+
return await _context.Orders.FindAsync(new object[] { id }, ct);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
public async Task UpdateAsync(Order order, CancellationToken ct = default)
|
|
162
|
+
{
|
|
163
|
+
_context.Orders.Update(order);
|
|
164
|
+
await _context.SaveChangesAsync(ct);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
public async ValueTask DisposeAsync()
|
|
168
|
+
{
|
|
169
|
+
await _context.DisposeAsync();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Registration (Program.cs)
|
|
175
|
+
|
|
176
|
+
```csharp
|
|
177
|
+
builder.Services.AddDbContextFactory<AppDbContext>(options =>
|
|
178
|
+
options.UseSqlServer(connectionString));
|
|
179
|
+
|
|
180
|
+
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
|
|
181
|
+
builder.Services.AddScoped<IOrderRepositoryFactory, OrderRepositoryFactory>();
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Usage in Background Jobs
|
|
185
|
+
|
|
186
|
+
```csharp
|
|
187
|
+
public class ProcessOrderJob
|
|
188
|
+
{
|
|
189
|
+
private readonly IOrderRepositoryFactory _repositoryFactory;
|
|
190
|
+
private readonly ILogger<ProcessOrderJob> _logger;
|
|
191
|
+
|
|
192
|
+
public ProcessOrderJob(
|
|
193
|
+
IOrderRepositoryFactory repositoryFactory,
|
|
194
|
+
ILogger<ProcessOrderJob> logger)
|
|
195
|
+
{
|
|
196
|
+
_repositoryFactory = repositoryFactory;
|
|
197
|
+
_logger = logger;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
public async Task ExecuteAsync(Guid orderId)
|
|
201
|
+
{
|
|
202
|
+
await using var repository = _repositoryFactory.CreateScoped();
|
|
203
|
+
|
|
204
|
+
var order = await repository.GetByIdAsync(orderId);
|
|
205
|
+
if (order == null)
|
|
206
|
+
{
|
|
207
|
+
_logger.LogWarning("Order {OrderId} not found", orderId);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Process order
|
|
212
|
+
order.Status = OrderStatus.Completed;
|
|
213
|
+
await repository.UpdateAsync(order);
|
|
214
|
+
|
|
215
|
+
// repository.DisposeAsync() called automatically (await using)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**Benefits:**
|
|
221
|
+
- Encapsulates DbContext creation for background operations
|
|
222
|
+
- Easy to mock in tests
|
|
223
|
+
- No DbContext disposal issues
|
|
224
|
+
- Follows Interface Segregation Principle (ISP)
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Generic Repository Pattern
|
|
229
|
+
|
|
230
|
+
### Generic Interface
|
|
231
|
+
|
|
232
|
+
```csharp
|
|
233
|
+
public interface IRepository<T> where T : class
|
|
234
|
+
{
|
|
235
|
+
Task<T?> GetByIdAsync(int id, CancellationToken ct = default);
|
|
236
|
+
Task<List<T>> GetAllAsync(CancellationToken ct = default);
|
|
237
|
+
Task<T> CreateAsync(T entity, CancellationToken ct = default);
|
|
238
|
+
Task UpdateAsync(T entity, CancellationToken ct = default);
|
|
239
|
+
Task DeleteAsync(int id, CancellationToken ct = default);
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Generic Implementation
|
|
244
|
+
|
|
245
|
+
```csharp
|
|
246
|
+
public class Repository<T> : IRepository<T> where T : class
|
|
247
|
+
{
|
|
248
|
+
private readonly AppDbContext _context;
|
|
249
|
+
private readonly DbSet<T> _dbSet;
|
|
250
|
+
|
|
251
|
+
public Repository(AppDbContext context)
|
|
252
|
+
{
|
|
253
|
+
_context = context;
|
|
254
|
+
_dbSet = context.Set<T>();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
public async Task<T?> GetByIdAsync(int id, CancellationToken ct = default)
|
|
258
|
+
{
|
|
259
|
+
return await _dbSet.FindAsync(new object[] { id }, ct);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
public async Task<List<T>> GetAllAsync(CancellationToken ct = default)
|
|
263
|
+
{
|
|
264
|
+
return await _dbSet.AsNoTracking().ToListAsync(ct);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
public async Task<T> CreateAsync(T entity, CancellationToken ct = default)
|
|
268
|
+
{
|
|
269
|
+
await _dbSet.AddAsync(entity, ct);
|
|
270
|
+
await _context.SaveChangesAsync(ct);
|
|
271
|
+
return entity;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
public async Task UpdateAsync(T entity, CancellationToken ct = default)
|
|
275
|
+
{
|
|
276
|
+
_dbSet.Update(entity);
|
|
277
|
+
await _context.SaveChangesAsync(ct);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
public async Task DeleteAsync(int id, CancellationToken ct = default)
|
|
281
|
+
{
|
|
282
|
+
var entity = await GetByIdAsync(id, ct);
|
|
283
|
+
if (entity != null)
|
|
284
|
+
{
|
|
285
|
+
_dbSet.Remove(entity);
|
|
286
|
+
await _context.SaveChangesAsync(ct);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Specific Repository Extending Generic
|
|
293
|
+
|
|
294
|
+
```csharp
|
|
295
|
+
public interface IUserRepository : IRepository<User>
|
|
296
|
+
{
|
|
297
|
+
Task<User?> GetByEmailAsync(string email, CancellationToken ct = default);
|
|
298
|
+
Task<List<User>> GetActiveUsersAsync(CancellationToken ct = default);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
public class UserRepository : Repository<User>, IUserRepository
|
|
302
|
+
{
|
|
303
|
+
private readonly AppDbContext _context;
|
|
304
|
+
|
|
305
|
+
public UserRepository(AppDbContext context) : base(context)
|
|
306
|
+
{
|
|
307
|
+
_context = context;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
public async Task<User?> GetByEmailAsync(string email, CancellationToken ct = default)
|
|
311
|
+
{
|
|
312
|
+
return await _context.Users
|
|
313
|
+
.AsNoTracking()
|
|
314
|
+
.FirstOrDefaultAsync(u => u.Email == email, ct);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
public async Task<List<User>> GetActiveUsersAsync(CancellationToken ct = default)
|
|
318
|
+
{
|
|
319
|
+
return await _context.Users
|
|
320
|
+
.AsNoTracking()
|
|
321
|
+
.Where(u => u.IsActive)
|
|
322
|
+
.ToListAsync(ct);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## Unit of Work Pattern
|
|
330
|
+
|
|
331
|
+
### Interface
|
|
332
|
+
|
|
333
|
+
```csharp
|
|
334
|
+
public interface IUnitOfWork : IDisposable
|
|
335
|
+
{
|
|
336
|
+
IUserRepository Users { get; }
|
|
337
|
+
IOrderRepository Orders { get; }
|
|
338
|
+
Task<int> SaveChangesAsync(CancellationToken ct = default);
|
|
339
|
+
Task BeginTransactionAsync(CancellationToken ct = default);
|
|
340
|
+
Task CommitAsync(CancellationToken ct = default);
|
|
341
|
+
Task RollbackAsync(CancellationToken ct = default);
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### Implementation
|
|
346
|
+
|
|
347
|
+
```csharp
|
|
348
|
+
public class UnitOfWork : IUnitOfWork
|
|
349
|
+
{
|
|
350
|
+
private readonly AppDbContext _context;
|
|
351
|
+
private IDbContextTransaction? _transaction;
|
|
352
|
+
|
|
353
|
+
public IUserRepository Users { get; }
|
|
354
|
+
public IOrderRepository Orders { get; }
|
|
355
|
+
|
|
356
|
+
public UnitOfWork(
|
|
357
|
+
AppDbContext context,
|
|
358
|
+
IUserRepository userRepository,
|
|
359
|
+
IOrderRepository orderRepository)
|
|
360
|
+
{
|
|
361
|
+
_context = context;
|
|
362
|
+
Users = userRepository;
|
|
363
|
+
Orders = orderRepository;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
public async Task<int> SaveChangesAsync(CancellationToken ct = default)
|
|
367
|
+
{
|
|
368
|
+
return await _context.SaveChangesAsync(ct);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
public async Task BeginTransactionAsync(CancellationToken ct = default)
|
|
372
|
+
{
|
|
373
|
+
_transaction = await _context.Database.BeginTransactionAsync(ct);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
public async Task CommitAsync(CancellationToken ct = default)
|
|
377
|
+
{
|
|
378
|
+
if (_transaction != null)
|
|
379
|
+
{
|
|
380
|
+
await _transaction.CommitAsync(ct);
|
|
381
|
+
await _transaction.DisposeAsync();
|
|
382
|
+
_transaction = null;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
public async Task RollbackAsync(CancellationToken ct = default)
|
|
387
|
+
{
|
|
388
|
+
if (_transaction != null)
|
|
389
|
+
{
|
|
390
|
+
await _transaction.RollbackAsync(ct);
|
|
391
|
+
await _transaction.DisposeAsync();
|
|
392
|
+
_transaction = null;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
public void Dispose()
|
|
397
|
+
{
|
|
398
|
+
_transaction?.Dispose();
|
|
399
|
+
_context.Dispose();
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Usage
|
|
405
|
+
|
|
406
|
+
```csharp
|
|
407
|
+
public class OrderService
|
|
408
|
+
{
|
|
409
|
+
private readonly IUnitOfWork _uow;
|
|
410
|
+
|
|
411
|
+
public OrderService(IUnitOfWork uow)
|
|
412
|
+
{
|
|
413
|
+
_uow = uow;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
public async Task CreateOrderWithUserAsync(Order order, User user)
|
|
417
|
+
{
|
|
418
|
+
await _uow.BeginTransactionAsync();
|
|
419
|
+
|
|
420
|
+
try
|
|
421
|
+
{
|
|
422
|
+
await _uow.Users.CreateAsync(user);
|
|
423
|
+
await _uow.Orders.CreateAsync(order);
|
|
424
|
+
await _uow.SaveChangesAsync();
|
|
425
|
+
await _uow.CommitAsync();
|
|
426
|
+
}
|
|
427
|
+
catch
|
|
428
|
+
{
|
|
429
|
+
await _uow.RollbackAsync();
|
|
430
|
+
throw;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
## Best Practices
|
|
439
|
+
|
|
440
|
+
### DO
|
|
441
|
+
|
|
442
|
+
✅ **Use AsNoTracking for read-only queries**
|
|
443
|
+
|
|
444
|
+
```csharp
|
|
445
|
+
public async Task<List<User>> GetAllAsync()
|
|
446
|
+
{
|
|
447
|
+
return await _context.Users
|
|
448
|
+
.AsNoTracking() // ✅ Faster, no tracking overhead
|
|
449
|
+
.ToListAsync();
|
|
450
|
+
}
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
✅ **Include related entities explicitly**
|
|
454
|
+
|
|
455
|
+
```csharp
|
|
456
|
+
public async Task<Order?> GetOrderWithItemsAsync(Guid id)
|
|
457
|
+
{
|
|
458
|
+
return await _context.Orders
|
|
459
|
+
.Include(o => o.Items) // ✅ Explicit include
|
|
460
|
+
.FirstOrDefaultAsync(o => o.Id == id);
|
|
461
|
+
}
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
✅ **Use CancellationToken**
|
|
465
|
+
|
|
466
|
+
```csharp
|
|
467
|
+
public async Task<User?> GetByIdAsync(int id, CancellationToken ct = default)
|
|
468
|
+
{
|
|
469
|
+
return await _context.Users.FindAsync(new object[] { id }, ct);
|
|
470
|
+
}
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
### DON'T
|
|
474
|
+
|
|
475
|
+
❌ **Don't return IQueryable from repository**
|
|
476
|
+
|
|
477
|
+
```csharp
|
|
478
|
+
// ❌ WRONG (exposes EF Core details)
|
|
479
|
+
public IQueryable<User> GetUsers()
|
|
480
|
+
{
|
|
481
|
+
return _context.Users;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// ✅ CORRECT (return materialized data)
|
|
485
|
+
public async Task<List<User>> GetUsersAsync()
|
|
486
|
+
{
|
|
487
|
+
return await _context.Users.ToListAsync();
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
❌ **Don't use repository in background without factory**
|
|
492
|
+
|
|
493
|
+
```csharp
|
|
494
|
+
// ❌ WRONG
|
|
495
|
+
public class BackgroundJob
|
|
496
|
+
{
|
|
497
|
+
private readonly IOrderRepository _repository; // Scoped, may be disposed!
|
|
498
|
+
|
|
499
|
+
public async Task ExecuteAsync(Guid orderId)
|
|
500
|
+
{
|
|
501
|
+
var order = await _repository.GetByIdAsync(orderId); // May fail!
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// ✅ CORRECT
|
|
506
|
+
public class BackgroundJob
|
|
507
|
+
{
|
|
508
|
+
private readonly IOrderRepositoryFactory _factory;
|
|
509
|
+
|
|
510
|
+
public async Task ExecuteAsync(Guid orderId)
|
|
511
|
+
{
|
|
512
|
+
await using var repository = _factory.CreateScoped();
|
|
513
|
+
var order = await repository.GetByIdAsync(orderId);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
---
|
|
519
|
+
|
|
520
|
+
## Related Standards
|
|
521
|
+
|
|
522
|
+
- [EF Core Standards](./ef-core.md)
|
|
523
|
+
- [DI Standards](../dotnet/di.md)
|
|
524
|
+
- [Async Standards](../dotnet/async.md)
|
|
525
|
+
|
|
526
|
+
---
|
|
527
|
+
|
|
528
|
+
*MORPH-SPEC by Polymorphism Tech*
|