Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ src/Dotnet.Samples.AspNetCore.WebApi/
├── Extensions/ — IServiceCollection extension methods (service registration)
├── Configurations/ — Options classes bound from appsettings.json
├── Middlewares/ — Custom ASP.NET Core middleware
├── Data/ — DbContext + DbInitializer (seed data)
└── Storage/ — SQLite database file (players.db)
├── Data/ — DbContext; seed data via HasData() in OnModelCreating
└── Storage/ — SQLite database file (created at runtime by MigrateAsync)

test/Dotnet.Samples.AspNetCore.WebApi.Tests/
├── Unit/ — Unit tests (controllers, services, validators)
Expand Down Expand Up @@ -206,7 +206,7 @@ This project uses Spec-Driven Development (SDD): discuss in Plan mode first, cre

**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.

**Modify schema**: Update `Player` entity → update DTOs → update AutoMapper profile → reset `Storage/players.db` → update tests → run `dotnet test`.
**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`.

## Architecture Decision Records (ADRs)

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ logs/
bin/
obj/
TestResults/
storage/*.db
.claude/settings.local.json
49 changes: 22 additions & 27 deletions .sonarcloud.properties
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,22 @@
# listed explicitly.
# =============================================================================

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

# =============================================================================
# Global exclusions
# =============================================================================

sonar.exclusions=\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20250414191223_InitialCreate.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20250414191223_InitialCreate.Designer.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20250414195445_SeedStarting11.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20250414195445_SeedStarting11.Designer.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20251221220614_SeedSubstitutes.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20251221220614_SeedSubstitutes.Designer.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260329000000_NormalizePlayerDataset.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260329000000_NormalizePlayerDataset.Designer.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141647_InitialCreate.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141647_InitialCreate.Designer.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141707_SeedStarting11.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141707_SeedStarting11.Designer.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141721_SeedSubstitutes.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141721_SeedSubstitutes.Designer.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/PlayerDbContextModelSnapshot.cs

# =============================================================================
Expand All @@ -41,14 +40,12 @@ sonar.coverage.exclusions=\
test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerMocks.cs,\
test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerStubs.cs,\
test/Dotnet.Samples.AspNetCore.WebApi.Tests/Usings.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20250414191223_InitialCreate.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20250414191223_InitialCreate.Designer.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20250414195445_SeedStarting11.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20250414195445_SeedStarting11.Designer.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20251221220614_SeedSubstitutes.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20251221220614_SeedSubstitutes.Designer.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260329000000_NormalizePlayerDataset.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260329000000_NormalizePlayerDataset.Designer.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141647_InitialCreate.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141647_InitialCreate.Designer.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141707_SeedStarting11.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141707_SeedStarting11.Designer.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141721_SeedSubstitutes.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141721_SeedSubstitutes.Designer.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/PlayerDbContextModelSnapshot.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Data/PlayerDbContext.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Program.cs
Expand All @@ -59,7 +56,7 @@ sonar.coverage.exclusions=\
# listed explicitly.
#
# Migrations — EF Core migration files are intentionally repetitive
# (sequential InsertData/UpdateData/Sql calls).
# (sequential InsertData calls generated by EF Core tooling).
#
# PlayerDbContext.cs — scaffolded EF Core infrastructure.
#
Expand All @@ -73,14 +70,12 @@ sonar.coverage.exclusions=\
# =============================================================================

sonar.cpd.exclusions=\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20250414191223_InitialCreate.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20250414191223_InitialCreate.Designer.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20250414195445_SeedStarting11.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20250414195445_SeedStarting11.Designer.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20251221220614_SeedSubstitutes.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20251221220614_SeedSubstitutes.Designer.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260329000000_NormalizePlayerDataset.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260329000000_NormalizePlayerDataset.Designer.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141647_InitialCreate.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141647_InitialCreate.Designer.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141707_SeedStarting11.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141707_SeedStarting11.Designer.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141721_SeedSubstitutes.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141721_SeedSubstitutes.Designer.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Migrations/PlayerDbContextModelSnapshot.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Data/PlayerDbContext.cs,\
src/Dotnet.Samples.AspNetCore.WebApi/Utilities/PlayerData.cs,\
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ This project uses famous football stadiums (A-Z) that hosted FIFA World Cup matc

### Changed

- 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)
- 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)
- 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)
- Switch runtime base image from `mcr.microsoft.com/dotnet/aspnet:10.0` (Debian)
to `mcr.microsoft.com/dotnet/aspnet:10.0-alpine` (before: 113.4 MB →
after: 73.9 MB compressed; measured via `docker manifest inspect` and
Expand Down
5 changes: 0 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ WORKDIR /src
COPY src/Dotnet.Samples.AspNetCore.WebApi/*.csproj ./Dotnet.Samples.AspNetCore.WebApi/
RUN dotnet restore ./Dotnet.Samples.AspNetCore.WebApi

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

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

# Add non-root user and make volume mount point writable
RUN addgroup -S aspnetcore && \
Expand Down
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ Before you begin, ensure you have the following installed:

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

```bash
dotnet tool install --global dotnet-ef
Expand Down Expand Up @@ -206,7 +206,7 @@ docker compose build
docker compose up
```

> 💡 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.
> 💡 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.

### Stop the application

Expand All @@ -216,7 +216,7 @@ docker compose down

### Reset the database

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

```bash
docker compose down -v
Expand Down Expand Up @@ -306,8 +306,7 @@ dotnet test --results-directory "coverage" --collect:"XPlat Code Coverage" --set
| `dotnet test --collect:"XPlat Code Coverage"` | Run tests with coverage report |
| `dotnet csharpier .` | Format source code |
| `dotnet ef migrations add <Name>` | Create a new migration |
| `dotnet ef database update` | Apply migrations |
| `./scripts/run-migrations-and-copy-database.sh` | Regenerate database with seed data |
| `dotnet ef database update` | Apply migrations manually |
| `docker compose build` | Build Docker image |
| `docker compose up` | Start Docker container |
| `docker compose down` | Stop Docker container |
Expand Down
5 changes: 3 additions & 2 deletions adr/0003-use-sqlite-for-data-storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ The cross-language comparison set (Go/Gin, Java/Spring Boot, Python/FastAPI, Rus

## Decision

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.
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.

## Consequences

### Positive

- Zero-config: no server process, no connection string credentials, no Docker service dependency for local development.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- The database file can be committed to the repository as seed data, making onboarding instant.
- EF Core abstracts the SQL dialect, so migrating to another database requires changing only the provider registration.
- `MigrateAsync()` at startup ensures the schema is always up to date, making onboarding instant without committing binary database files.

### Negative
- SQLite does not support concurrent writes, making it unsuitable for multi-instance deployments or high-throughput scenarios.
Expand Down
16 changes: 4 additions & 12 deletions scripts/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,15 @@ log() {
return 0
}

IMAGE_STORAGE_PATH="/app/hold/players-sqlite3.db"
VOLUME_STORAGE_PATH="/storage/players-sqlite3.db"

log "✔ Starting container..."

VOLUME_STORAGE_PATH="${STORAGE_PATH:-/storage/players-sqlite3.db}"

if [ ! -f "$VOLUME_STORAGE_PATH" ]; then
log "⚠️ No existing database file found in volume."
if [ -f "$IMAGE_STORAGE_PATH" ]; then
log "🔄 Copying database file to writable volume..."
cp "$IMAGE_STORAGE_PATH" "$VOLUME_STORAGE_PATH"
log "✔ Database initialized at $VOLUME_STORAGE_PATH"
else
log "⚠️ Database file missing at $IMAGE_STORAGE_PATH"
exit 1
fi
log "🗄️ EF Core migrations will initialize the database on first start."
else
log "✔ Existing database file found. Skipping seed copy."
log "✔ Existing database file found at $VOLUME_STORAGE_PATH."
fi

log "✔ Ready!"
Expand Down
66 changes: 0 additions & 66 deletions scripts/run-migrations-and-copy-database.sh

This file was deleted.

3 changes: 3 additions & 0 deletions src/Dotnet.Samples.AspNetCore.WebApi/Data/PlayerDbContext.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Dotnet.Samples.AspNetCore.WebApi.Models;
using Dotnet.Samples.AspNetCore.WebApi.Utilities;
using Microsoft.EntityFrameworkCore;

namespace Dotnet.Samples.AspNetCore.WebApi.Data;
Expand Down Expand Up @@ -33,6 +34,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
entity.HasKey(player => player.Id);
entity.Property(player => player.Id).ValueGeneratedOnAdd();
entity.HasIndex(player => player.SquadNumber).IsUnique();
entity.HasData(PlayerData.MakeStarting11WithId());
entity.HasData(PlayerData.GetSubstitutesWithId());
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,6 @@
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.7" />
</ItemGroup>

<ItemGroup>
<Content Include="storage/players-sqlite3.db">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<PackageCopyToOutput>true</PackageCopyToOutput>
</Content>
</ItemGroup>

<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,16 @@ IWebHostEnvironment environment
{
services.AddDbContextPool<PlayerDbContext>(options =>
{
var dataSource = Path.Combine(
AppContext.BaseDirectory,
"storage",
"players-sqlite3.db"
);
var storagePath = Environment.GetEnvironmentVariable("STORAGE_PATH");
var dataSource = !string.IsNullOrWhiteSpace(storagePath)
? storagePath
: Path.Combine(AppContext.BaseDirectory, "storage", "players-sqlite3.db");

var storageDir = Path.GetDirectoryName(dataSource);
if (!string.IsNullOrWhiteSpace(storageDir))
{
Directory.CreateDirectory(storageDir);
}
options.UseSqlite($"Data Source={dataSource}");

if (environment.IsDevelopment())
Expand Down
Loading
Loading