- Create SeedDataService in Infrastructure/Seed with idempotent seeding - Seed 2 clubs: Sunrise Tennis Club, Valley Cycling Club - Seed 7 member records (5 unique Keycloak test users) - Seed 8 work items covering all status states - Seed 5 shifts with date variety (past, today, future) - Seed shift signups for realistic partial capacity - Register SeedDataService in Program.cs with development-only guard - Use deterministic GUID generation from club names - Ensure all tenant IDs match for RLS compliance - Track in learnings.md and evidence files for Task 22 QA
133 lines
4.2 KiB
Plaintext
133 lines
4.2 KiB
Plaintext
# Task 11: Seed Data Service Implementation Evidence
|
|
|
|
## Files Created
|
|
|
|
### 1. backend/src/WorkClub.Infrastructure/Seed/SeedDataService.cs
|
|
- **Purpose**: Provides idempotent seeding of development database
|
|
- **Key Features**:
|
|
- Deterministic GUID generation from entity names (MD5-based)
|
|
- Idempotent checks: Only seeds if no data exists
|
|
- IServiceScopeFactory injection for creating scoped DbContext
|
|
- Async SeedAsync() method for non-blocking seed operations
|
|
|
|
### 2. backend/src/WorkClub.Api/Program.cs (Modified)
|
|
- **Added Import**: `using WorkClub.Infrastructure.Seed;`
|
|
- **Service Registration**: `builder.Services.AddScoped<SeedDataService>();`
|
|
- **Development Startup**: Added seed execution in development environment only
|
|
```csharp
|
|
if (app.Environment.IsDevelopment())
|
|
{
|
|
using var scope = app.Services.CreateScope();
|
|
var seedService = scope.ServiceProvider.GetRequiredService<SeedDataService>();
|
|
await seedService.SeedAsync();
|
|
}
|
|
```
|
|
|
|
## Seed Data Structure
|
|
|
|
### Clubs (2 total)
|
|
1. **Sunrise Tennis Club** (Tennis)
|
|
- Tenant ID: Deterministic GUID from "Sunrise Tennis Club"
|
|
- Used by 3 members: admin@test.com, manager@test.com, member1@test.com, member2@test.com, viewer@test.com
|
|
|
|
2. **Valley Cycling Club** (Cycling)
|
|
- Tenant ID: Deterministic GUID from "Valley Cycling Club"
|
|
- Used by 2 members: admin@test.com, member1@test.com
|
|
|
|
### Members (7 total records, 5 unique users)
|
|
- **admin@test.com**: Admin in Tennis Club, Member in Cycling Club
|
|
- **manager@test.com**: Manager in Tennis Club
|
|
- **member1@test.com**: Member in Tennis Club, Member in Cycling Club
|
|
- **member2@test.com**: Member in Tennis Club
|
|
- **viewer@test.com**: Viewer in Tennis Club
|
|
|
|
### Work Items (8 total)
|
|
**Tennis Club (5 items)**
|
|
- Court renovation (Open, unassigned)
|
|
- Equipment order (Assigned, to manager)
|
|
- Tournament planning (InProgress, to member1)
|
|
- Member handbook review (Review, to member2)
|
|
- Website update (Done, to manager)
|
|
|
|
**Cycling Club (3 items)**
|
|
- Route mapping (Open, unassigned)
|
|
- Safety training (Assigned, to member1)
|
|
- Group ride coordination (InProgress, to admin)
|
|
|
|
### Shifts (5 total)
|
|
**Tennis Club (3 shifts)**
|
|
- Court Maintenance - Yesterday (past, capacity 2)
|
|
- Court Maintenance - Today (today, capacity 3)
|
|
- Tournament Setup - Next Week (future, capacity 5)
|
|
|
|
**Cycling Club (2 shifts)**
|
|
- Group Ride - Today (today, capacity 10)
|
|
- Maintenance Workshop - Next Week (future, capacity 4)
|
|
|
|
### Shift Signups (3-4 total)
|
|
- Tennis Court Maintenance (Yesterday): 2 signups
|
|
- Cycling Group Ride (Today): 1 signup
|
|
|
|
## Idempotency Implementation
|
|
|
|
Each entity type is seeded with idempotent checks:
|
|
```csharp
|
|
if (!context.Clubs.Any())
|
|
{
|
|
context.Clubs.AddRange(...);
|
|
await context.SaveChangesAsync();
|
|
}
|
|
```
|
|
|
|
This ensures:
|
|
- First run: All data inserted
|
|
- Subsequent runs: No duplicates (check passes on subsequent runs)
|
|
- Safe for multiple restarts during development
|
|
|
|
## Development-Only Guard
|
|
|
|
Seed execution is protected:
|
|
```csharp
|
|
if (app.Environment.IsDevelopment())
|
|
{
|
|
// Seed only runs in Development environment
|
|
}
|
|
```
|
|
|
|
This ensures:
|
|
- Production environment: No seed execution
|
|
- Staging/Testing: Controlled separately via environment variables
|
|
|
|
## Deterministic GUID Generation
|
|
|
|
Used MD5 hash to create consistent tenant IDs:
|
|
```csharp
|
|
private static string GenerateDeterministicGuid(string input)
|
|
{
|
|
var hash = MD5.HashData(Encoding.UTF8.GetBytes(input));
|
|
return new Guid(hash.Take(16).ToArray()).ToString();
|
|
}
|
|
```
|
|
|
|
Benefits:
|
|
- Same GUID generated for same club name (consistency across restarts)
|
|
- Predictable: Matches expected UUIDs in test users
|
|
- No external dependencies needed
|
|
|
|
## Usage During Development
|
|
|
|
1. Backend starts in Development environment
|
|
2. Program.cs development middleware runs
|
|
3. SeedDataService is resolved from DI container
|
|
4. SeedAsync() is called asynchronously
|
|
5. First run: All seed data inserted
|
|
6. Subsequent runs: Checks pass, no duplicates
|
|
|
|
## Notes
|
|
|
|
- Seed runs synchronously in middleware (blocking startup until complete)
|
|
- SeedDataService uses IServiceScopeFactory to create fresh DbContext
|
|
- All entities have CreatedAt/UpdatedAt timestamps set to UTC now
|
|
- ExternalUserId values are placeholder user IDs (can be updated when connected to Keycloak)
|
|
- Shift times use DateTimeOffset to handle timezone properly
|