Skip to content

Commit 7de7b87

Browse files
authored
Merge pull request #460 from nanotaboada/feat/ef-core-migrate-async-at-startup
feat(data): replace pre-seeded db with EF Core migrations and HasData() (#459)
2 parents 66eca79 + 55be5e6 commit 7de7b87

30 files changed

+1676
-685
lines changed

.github/copilot-instructions.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ src/Dotnet.Samples.AspNetCore.WebApi/
3434
├── Extensions/ — IServiceCollection extension methods (service registration)
3535
├── Configurations/ — Options classes bound from appsettings.json
3636
├── Middlewares/ — Custom ASP.NET Core middleware
37-
├── Data/ — DbContext + DbInitializer (seed data)
38-
└── Storage/ — SQLite database file (players.db)
37+
├── Data/ — DbContext; seed data via HasData() in OnModelCreating
38+
└── Storage/ — SQLite database file (created at runtime by MigrateAsync)
3939
4040
test/Dotnet.Samples.AspNetCore.WebApi.Tests/
4141
├── Unit/ — Unit tests (controllers, services, validators)
@@ -206,7 +206,7 @@ This project uses Spec-Driven Development (SDD): discuss in Plan mode first, cre
206206

207207
**Add an endpoint**: Add DTO in `Models/` → update `PlayerMappingProfile` in `Mappings/` → add repository method(s) in `Repositories/` → add service method in `Services/` → add controller action in `Controllers/` → add/update validator rule set in `Validators/` → add tests in `test/.../Unit/` → run pre-commit checks.
208208

209-
**Modify schema**: Update `Player` entity → update DTOs → update AutoMapper profile → reset `Storage/players.db` → update tests → run `dotnet test`.
209+
**Modify schema**: Update `Player` entity → update DTOs → update AutoMapper profile → update `HasData()` seed data in `OnModelCreating` if needed → run `dotnet ef migrations add <Name>` → update tests → run `dotnet test`.
210210

211211
## Architecture Decision Records (ADRs)
212212

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ logs/
22
bin/
33
obj/
44
TestResults/
5+
storage/*.db
56
.claude/settings.local.json

.sonarcloud.properties

Lines changed: 22 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,22 @@
77
# listed explicitly.
88
# =============================================================================
99

10-
sonar.sources=src/
11-
sonar.tests=test/
10+
# NOTE: sonar.sources and sonar.tests are NOT supported by the Scanner for .NET
11+
# and are silently ignored. The scanner auto-discovers sources and test projects
12+
# from .csproj files. Do not add them back.
1213
sonar.sourceEncoding=UTF-8
1314

1415
# =============================================================================
1516
# Global exclusions
1617
# =============================================================================
1718

1819
sonar.exclusions=\
19-
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20250414191223_InitialCreate.cs,\
20-
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20250414191223_InitialCreate.Designer.cs,\
21-
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20250414195445_SeedStarting11.cs,\
22-
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20250414195445_SeedStarting11.Designer.cs,\
23-
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20251221220614_SeedSubstitutes.cs,\
24-
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20251221220614_SeedSubstitutes.Designer.cs,\
25-
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260329000000_NormalizePlayerDataset.cs,\
26-
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260329000000_NormalizePlayerDataset.Designer.cs,\
20+
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141647_InitialCreate.cs,\
21+
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141647_InitialCreate.Designer.cs,\
22+
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141707_SeedStarting11.cs,\
23+
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141707_SeedStarting11.Designer.cs,\
24+
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141721_SeedSubstitutes.cs,\
25+
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141721_SeedSubstitutes.Designer.cs,\
2726
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/PlayerDbContextModelSnapshot.cs
2827

2928
# =============================================================================
@@ -41,14 +40,12 @@ sonar.coverage.exclusions=\
4140
test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerMocks.cs,\
4241
test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerStubs.cs,\
4342
test/Dotnet.Samples.AspNetCore.WebApi.Tests/Usings.cs,\
44-
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20250414191223_InitialCreate.cs,\
45-
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20250414191223_InitialCreate.Designer.cs,\
46-
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20250414195445_SeedStarting11.cs,\
47-
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20250414195445_SeedStarting11.Designer.cs,\
48-
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20251221220614_SeedSubstitutes.cs,\
49-
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20251221220614_SeedSubstitutes.Designer.cs,\
50-
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260329000000_NormalizePlayerDataset.cs,\
51-
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260329000000_NormalizePlayerDataset.Designer.cs,\
43+
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141647_InitialCreate.cs,\
44+
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141647_InitialCreate.Designer.cs,\
45+
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141707_SeedStarting11.cs,\
46+
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141707_SeedStarting11.Designer.cs,\
47+
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141721_SeedSubstitutes.cs,\
48+
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141721_SeedSubstitutes.Designer.cs,\
5249
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/PlayerDbContextModelSnapshot.cs,\
5350
src/Dotnet.Samples.AspNetCore.WebApi/Data/PlayerDbContext.cs,\
5451
src/Dotnet.Samples.AspNetCore.WebApi/Program.cs
@@ -59,7 +56,7 @@ sonar.coverage.exclusions=\
5956
# listed explicitly.
6057
#
6158
# Migrations — EF Core migration files are intentionally repetitive
62-
# (sequential InsertData/UpdateData/Sql calls).
59+
# (sequential InsertData calls generated by EF Core tooling).
6360
#
6461
# PlayerDbContext.cs — scaffolded EF Core infrastructure.
6562
#
@@ -73,14 +70,12 @@ sonar.coverage.exclusions=\
7370
# =============================================================================
7471

7572
sonar.cpd.exclusions=\
76-
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20250414191223_InitialCreate.cs,\
77-
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20250414191223_InitialCreate.Designer.cs,\
78-
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20250414195445_SeedStarting11.cs,\
79-
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20250414195445_SeedStarting11.Designer.cs,\
80-
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20251221220614_SeedSubstitutes.cs,\
81-
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20251221220614_SeedSubstitutes.Designer.cs,\
82-
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260329000000_NormalizePlayerDataset.cs,\
83-
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260329000000_NormalizePlayerDataset.Designer.cs,\
73+
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141647_InitialCreate.cs,\
74+
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141647_InitialCreate.Designer.cs,\
75+
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141707_SeedStarting11.cs,\
76+
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141707_SeedStarting11.Designer.cs,\
77+
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141721_SeedSubstitutes.cs,\
78+
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141721_SeedSubstitutes.Designer.cs,\
8479
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/PlayerDbContextModelSnapshot.cs,\
8580
src/Dotnet.Samples.AspNetCore.WebApi/Data/PlayerDbContext.cs,\
8681
src/Dotnet.Samples.AspNetCore.WebApi/Utilities/PlayerData.cs,\

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ This project uses famous football stadiums (A-Z) that hosted FIFA World Cup matc
5454

5555
### Changed
5656

57+
- Replace pre-seeded `storage/players-sqlite3.db` binary blob with EF Core `MigrateAsync()` at startup: schema and seed data are now applied automatically before the first request is served; `STORAGE_PATH` env var controls the database file path (Docker volume path in production, `AppContext.BaseDirectory/storage/` locally); the committed database file, `Dockerfile` db copy step, and `scripts/run-migrations-and-copy-database.sh` have been removed (#459)
58+
- Recreate EF Core migrations using `HasData()` in `OnModelCreating`: three self-contained migrations (`InitialCreate` DDL, `SeedStarting11` DML, `SeedSubstitutes` DML) generated by EF Core with literal `InsertData` values — no migration calls application methods; `NormalizePlayerDataset` patch migration eliminated by folding corrections into seed data from the start (#459)
59+
- Replace `DatabaseFakes.CreateTable()` (placeholder schema) and `DatabaseFakes.Seed()` (manual insert bypassing migrations) with `DatabaseFakes.MigrateAsync()`, which applies the full EF Core migration chain on in-memory SQLite (#459)
5760
- Switch runtime base image from `mcr.microsoft.com/dotnet/aspnet:10.0` (Debian)
5861
to `mcr.microsoft.com/dotnet/aspnet:10.0-alpine` (before: 113.4 MB →
5962
after: 73.9 MB compressed; measured via `docker manifest inspect` and

Dockerfile

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ WORKDIR /src
1010
COPY src/Dotnet.Samples.AspNetCore.WebApi/*.csproj ./Dotnet.Samples.AspNetCore.WebApi/
1111
RUN dotnet restore ./Dotnet.Samples.AspNetCore.WebApi
1212

13-
# Copy source code and pre-seeded SQLite database
1413
COPY src/Dotnet.Samples.AspNetCore.WebApi/ ./Dotnet.Samples.AspNetCore.WebApi/
1514

1615
WORKDIR /src/Dotnet.Samples.AspNetCore.WebApi
@@ -53,10 +52,6 @@ COPY --chmod=444 README.md ./
5352
# Copy entrypoint and healthcheck scripts
5453
COPY --chmod=555 scripts/entrypoint.sh ./entrypoint.sh
5554
COPY --chmod=555 scripts/healthcheck.sh ./healthcheck.sh
56-
# The 'hold' is our storage compartment within the image. Here, we copy a
57-
# pre-seeded SQLite database file, which Compose will mount as a persistent
58-
# 'storage' volume when the container starts up.
59-
COPY --from=builder /src/Dotnet.Samples.AspNetCore.WebApi/storage/players-sqlite3.db ./hold/players-sqlite3.db
6055

6156
# Add non-root user and make volume mount point writable
6257
RUN addgroup -S aspnetcore && \

README.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ Before you begin, ensure you have the following installed:
161161

162162
- .NET 10 SDK (LTS) or higher
163163
- Docker Desktop (optional, for containerized deployment)
164-
- dotnet-ef CLI tool (for database migrations)
164+
- dotnet-ef CLI tool (optional, for creating new migrations)
165165

166166
```bash
167167
dotnet tool install --global dotnet-ef
@@ -206,7 +206,7 @@ docker compose build
206206
docker compose up
207207
```
208208

209-
> 💡 On first run, the container copies a pre-seeded SQLite database into a persistent volume. On subsequent runs, that volume is reused and the data is preserved.
209+
> 💡 On first run, the app applies EF Core migrations and seeds the database automatically into a persistent volume. On subsequent runs, that volume is reused and the data is preserved.
210210
211211
### Stop the application
212212

@@ -216,7 +216,7 @@ docker compose down
216216

217217
### Reset the database
218218

219-
To remove the volume and reinitialize the database from the built-in seed file:
219+
To remove the volume and let the app re-create and re-seed the database on next startup:
220220

221221
```bash
222222
docker compose down -v
@@ -306,8 +306,7 @@ dotnet test --results-directory "coverage" --collect:"XPlat Code Coverage" --set
306306
| `dotnet test --collect:"XPlat Code Coverage"` | Run tests with coverage report |
307307
| `dotnet csharpier .` | Format source code |
308308
| `dotnet ef migrations add <Name>` | Create a new migration |
309-
| `dotnet ef database update` | Apply migrations |
310-
| `./scripts/run-migrations-and-copy-database.sh` | Regenerate database with seed data |
309+
| `dotnet ef database update` | Apply migrations manually |
311310
| `docker compose build` | Build Docker image |
312311
| `docker compose up` | Start Docker container |
313312
| `docker compose down` | Stop Docker container |

adr/0003-use-sqlite-for-data-storage.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@ The cross-language comparison set (Go/Gin, Java/Spring Boot, Python/FastAPI, Rus
1414

1515
## Decision
1616

17-
We will use SQLite as the database engine, accessed through Entity Framework Core. The database file is stored at `storage/players-sqlite3.db` and is pre-seeded with sample data. Docker deployments mount the file into a named volume so data survives container restarts.
17+
We will use SQLite as the database engine, accessed through Entity Framework Core. The database file is created at `storage/players-sqlite3.db` at runtime: EF Core applies pending migrations (schema + seed data via `HasData()`) automatically at startup via `MigrateAsync()` before the first request is served. Docker deployments mount the file into a named volume so data survives container restarts.
1818

1919
## Consequences
2020

2121
### Positive
22+
2223
- Zero-config: no server process, no connection string credentials, no Docker service dependency for local development.
23-
- The database file can be committed to the repository as seed data, making onboarding instant.
2424
- EF Core abstracts the SQL dialect, so migrating to another database requires changing only the provider registration.
25+
- `MigrateAsync()` at startup ensures the schema is always up to date, making onboarding instant without committing binary database files.
2526

2627
### Negative
2728
- SQLite does not support concurrent writes, making it unsuitable for multi-instance deployments or high-throughput scenarios.

scripts/entrypoint.sh

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,15 @@ log() {
88
return 0
99
}
1010

11-
IMAGE_STORAGE_PATH="/app/hold/players-sqlite3.db"
12-
VOLUME_STORAGE_PATH="/storage/players-sqlite3.db"
13-
1411
log "✔ Starting container..."
1512

13+
VOLUME_STORAGE_PATH="${STORAGE_PATH:-/storage/players-sqlite3.db}"
14+
1615
if [ ! -f "$VOLUME_STORAGE_PATH" ]; then
1716
log "⚠️ No existing database file found in volume."
18-
if [ -f "$IMAGE_STORAGE_PATH" ]; then
19-
log "🔄 Copying database file to writable volume..."
20-
cp "$IMAGE_STORAGE_PATH" "$VOLUME_STORAGE_PATH"
21-
log "✔ Database initialized at $VOLUME_STORAGE_PATH"
22-
else
23-
log "⚠️ Database file missing at $IMAGE_STORAGE_PATH"
24-
exit 1
25-
fi
17+
log "🗄️ EF Core migrations will initialize the database on first start."
2618
else
27-
log "✔ Existing database file found. Skipping seed copy."
19+
log "✔ Existing database file found at $VOLUME_STORAGE_PATH."
2820
fi
2921

3022
log "✔ Ready!"

scripts/run-migrations-and-copy-database.sh

Lines changed: 0 additions & 66 deletions
This file was deleted.

src/Dotnet.Samples.AspNetCore.WebApi/Data/PlayerDbContext.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Dotnet.Samples.AspNetCore.WebApi.Models;
2+
using Dotnet.Samples.AspNetCore.WebApi.Utilities;
23
using Microsoft.EntityFrameworkCore;
34

45
namespace Dotnet.Samples.AspNetCore.WebApi.Data;
@@ -33,6 +34,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
3334
entity.HasKey(player => player.Id);
3435
entity.Property(player => player.Id).ValueGeneratedOnAdd();
3536
entity.HasIndex(player => player.SquadNumber).IsUnique();
37+
entity.HasData(PlayerData.MakeStarting11WithId());
38+
entity.HasData(PlayerData.GetSubstitutesWithId());
3639
});
3740
}
3841
}

0 commit comments

Comments
 (0)