Development Guide

August 7, 2026 · View on GitHub

Local development uses .NET Aspire to orchestrate all services — API, PostgreSQL, and PgAdmin — with a single F5 press in VS Code.

Prerequisites

ToolVersionPurpose
.NET 10 SDK10.0+Build and run the API
Aspire CLIAnyOrchestrate local dev stack from CLI
Docker DesktopAnyPostgreSQL container (Aspire manages it)
Node.js18+OpenAPI spec linting and drift detection
VS CodeAnyRecommended IDE
C# Dev KitAnyVS Code C# support

Docker is required — Aspire provisions PostgreSQL as a container automatically. You don't need to manage the database manually.

Initial Setup

# Clone the repository
git clone https://github.com/mggarofalo/Receipts.git
cd Receipts

# Install Aspire CLI (if not already installed)
dotnet tool install --global Aspire.Cli

# Restore .NET packages and install tools (also configures native Git hooks)
dotnet restore Receipts.slnx

# Install Node dependencies (OpenAPI linting tools)
npm install

# Pre-download the ONNX embedding model (~1.34 GB)
# Optional: the app fetches this itself on first start. Running it up front just means
# semantic features work immediately instead of a few minutes in.
dotnet run scripts/download-onnx-model.cs

The model is stored per-machine, not per-checkout — %LOCALAPPDATA%\Receipts\models on Windows, ~/.local/share/Receipts/models elsewhere — so every clone and worktree shares one copy. Override the location with Embeddings__ModelPath, or set Embeddings__AutoDownload=false to require it be staged in by hand.

  1. Open the repository root in VS Code
  2. Press F5 (or Run → Start Debugging)
  3. Select "Launch Aspire AppHost" if prompted
  4. Wait ~30 seconds for all services to start

VS Code will automatically open the Aspire Dashboard in your browser.

What Starts

ServiceDefault URLDescription
Aspire Dashboardhttp://localhost:15888Observability — logs, traces, metrics
Frontend (Vite)http://localhost:5173React client — pinned, see below
API (HTTP)http://localhost:5000REST API
API (HTTPS)https://localhost:5001REST API (HTTPS)
API Docs (Scalar)http://localhost:5000/scalarInteractive API documentation
PgAdminauto-assignedPostgreSQL admin UI

Note: Aspire assigns dynamic ports and the defaults above may differ. Check the Aspire Dashboard → Resources view for the actual URLs assigned to each service in your session.

The frontend is the exception: AppHost.cs pins its endpoint to 5173 so tooling that hardcodes the port (Playwright's webServer, the QA skills) keeps working. The endpoint is plain HTTP — there is no dev certificate on it, so https://localhost:5173 will fail the TLS handshake. Do not add a second endpoint to the frontend resource; AddViteApp already declares one, and a duplicate produces a proxy that accepts connections and then hangs forever (RECEIPTS-882, guarded by tests/Receipts.AppHost.Tests).

Debug Configurations

  • Launch Aspire AppHost — Starts the entire stack (API + DB + Dashboard)
  • Attach to API — Attach debugger to a running API process for breakpoints
  • Debug All (AppHost + API) — Launch AppHost and immediately attach the debugger to the API

Aspire Dashboard

The dashboard provides full observability without any external tooling:

ViewWhat You See
ResourcesAll running services with health status
TracesDistributed request traces across API → DB
MetricsRequest rates, response times, runtime stats
LogsStructured logs from all services, searchable
ConsoleRaw stdout/stderr from each service

Database Tracing

EF Core queries are automatically traced and visible in the Traces view. Each HTTP request shows the full span tree including SQL statements executed against PostgreSQL.

Running Without Aspire

If you prefer to run the API directly (without Docker/Aspire):

# Set database environment variables
export POSTGRES_HOST=localhost
export POSTGRES_PORT=5432
export POSTGRES_USER=postgres
export POSTGRES_PASSWORD=yourpassword
export POSTGRES_DB=receiptsdb

# Apply migrations and seed the database (required before first run)
dotnet run --project src/Tools/DbMigrator/DbMigrator.csproj
dotnet run --project src/Tools/DbSeeder/DbSeeder.csproj

# Run the API
dotnet run --project src/Presentation/API/API.csproj

The API does not self-migrate or self-seed. You must run DbMigrator and DbSeeder before starting the API. Re-run DbMigrator after pulling new migrations.

Admin User Seeding

The DbSeeder creates an initial admin user when AdminSeed__Email and AdminSeed__Password are set. Under Aspire, these are passed automatically via AppHost.cs. For standalone runs, set the environment variables manually:

export AdminSeed__Email=admin@receipts.local
export AdminSeed__Password="Admin123!@#"
export AdminSeed__FirstName=Admin
export AdminSeed__LastName=User
dotnet run --project src/Tools/DbSeeder/DbSeeder.csproj

If the variables are absent, the seeder logs a warning and seeds only roles (no admin user). The seed is not recorded in __SeedHistory when admin config is missing, so you can re-run the seeder with the correct variables later.

Tip: The src/Tools/DbSeeder/appsettings.Development.json file provides these defaults automatically when running with DOTNET_ENVIRONMENT=Development (the default for dotnet run).

Build and Test

# Build entire solution
dotnet build Receipts.slnx

# Run unit tests (same as CI)
dotnet test Receipts.slnx --filter "Category!=Integration"

# Run all tests including integration (requires Docker + ONNX model)
dotnet test Receipts.slnx

# Run integration tests only (requires Docker)
dotnet test tests/Infrastructure.IntegrationTests --filter "Category=Integration"

# Run tests for a specific project
dotnet test tests/Application.Tests/Application.Tests.csproj

Integration Tests (Testcontainers)

The Infrastructure.IntegrationTests project runs EF Core against a real PostgreSQL instance via Testcontainers. These tests catch bugs that InMemory unit tests cannot, such as:

  • DateTimeOffset UTC validation — Npgsql rejects non-UTC offsets for timestamptz columns
  • Column type mappingdecimal(18,2), uuid, text, date, enum-to-string, pgvector
  • Soft-delete cascades — parent deletion cascades DeletedAt to owned children via real SQL
  • Audit logging — full SaveChangesAsync pipeline with real database round-trips
  • Query filtersHasQueryFilter generates real SQL WHERE clauses

Requirements: Docker must be running. The tests automatically start and stop a PostgreSQL container — no manual database setup needed.

CI note: Integration tests are tagged [Trait("Category", "Integration")] and excluded from the CI unit test step (--filter "Category!=Integration"). They run locally or in CI environments with Docker available.

Git Hooks

Git hooks are installed automatically by dotnet restore (or bash .githooks/setup.sh). Two hooks run on every commit:

Commit Convention

All commits follow Conventional Commits format: <type>(<scope>): <description>

Typesfeat, fix, docs, refactor, test, chore
Scopesapi, client, domain, application, infrastructure, infra, common, shared, ci, hooks

Multiple scopes are allowed with a comma separator (e.g., feat(api,client): add pagination).

Examples:

  • feat(api): add pagination to receipts endpoint
  • fix(client): prevent infinite re-render in TransactionForm
  • chore: update dependencies

Enforcement:

  • Local: commit-msg hook runs commitlint on every commit (see .githooks/commit-msg)
  • CI: PR title validation via amannn/action-semantic-pull-request (squash-merge means the PR title becomes the commit on main)
  • Config: commitlint.config.mjs at the repo root defines allowed types, scopes, and header length (100 chars max)

pre-commit hook

Every git commit runs the full quality pipeline automatically:

  1. Prerequisitesdotnet run scripts/worktree-setup.cs -- --check
  2. OpenAPI spec lintnpx spectral lint openapi/spec.yaml
  3. Code format checkdotnet format --verify-no-changes
  4. Build with warnings-as-errors — also regenerates DTOs and openapi/generated/API.json
  5. Semantic drift check — compares spec vs generated output for structural differences
  6. Testsdotnet test --no-build --filter "Category!=Integration"
  7. TypeScript typesnpx tsc --noEmit
  8. ESLintnpx eslint src/client/src

For faster iteration, quick mode runs only prerequisites, format, tsc, and eslint:

PRECOMMIT_QUICK=1 git commit -m "message"

OpenAPI Spec-First Workflow

All API changes follow a spec-first workflow:

  1. Edit openapi/spec.yaml — this is the single source of truth
  2. npm run lint:spec — validate the spec
  3. dotnet build — regenerates DTOs and the built output
  4. npm run check:drift — verify spec and implementation stay in sync

See docs/api-guidelines.md for the full spec-first workflow details.

Troubleshooting

Port conflicts

If ports 5000/5001 are in use, Aspire will pick alternative ports. Check the Dashboard Resources view for the actual URLs.

Docker not running

Aspire requires Docker to provision the PostgreSQL container. Start Docker Desktop before pressing F5.

Database connection issues

The API waits for PostgreSQL to be healthy before starting (.WaitFor(db) in AppHost). If the API starts before the database is ready, Aspire restarts it automatically.

Pre-commit hook failures

  • Spec lint fails — fix the OpenAPI spec error reported by Spectral
  • Format fails — run dotnet format Receipts.slnx to auto-fix
  • Drift check fails — the spec and generated API are out of sync; update the spec or the implementation to match
  • Tests fail — fix the failing tests before committing

Releases

See docs/releases.md for the full release process. Releases are tag-driven: pushing a vX.Y.Z git tag on main publishes Docker images to GHCR and creates a GitHub Release with auto-generated notes. The .NET version is derived from the tag by MinVer.