# CLAUDE.md ## Overview REST API for managing football players built with ASP.NET Core 10. Implements CRUD operations with a layered architecture, EF Core persistence (SQLite by default, PostgreSQL opt-in via `DATABASE_PROVIDER`), FluentValidation, AutoMapper, and in-memory caching. Primarily a learning and reference project — clarity and educational value take precedence over brevity. ## Tech Stack | Category | Technology | |-----------------|-----------------------------------------------------------| | Language | C# (.NET 10 LTS) | | Framework | ASP.NET Core (MVC controllers) | | ORM | Entity Framework Core 10 | | Database | SQLite (default) · PostgreSQL 17 (opt-in) | | Mapping | AutoMapper | | Validation | FluentValidation | | Caching | `IMemoryCache` (10-min sliding + 1-hour absolute expiry) | | Logging | Serilog (structured, console + file) | | Testing | xUnit + Moq + FluentAssertions | | Formatting | CSharpier | | Containerization| Docker | ## Structure ```tree src/Dotnet.Samples.AspNetCore.WebApi/ ├── Controllers/ — HTTP handlers; minimal logic, delegate to services [HTTP layer] ├── Services/ — Business logic + IMemoryCache caching [business layer] ├── Repositories/ — Generic Repository + specific implementations [data layer] ├── Models/ — Player entity + request/response DTOs ├── Validators/ — FluentValidation validators (one per request model) ├── Mappings/ — AutoMapper profiles (PlayerMappingProfile) ├── Enums/ — Position abbreviations and other domain enumerations ├── Extensions/ — IServiceCollection extension methods (service registration) ├── Configurations/ — Options classes bound from appsettings.json ├── Middlewares/ — Custom ASP.NET Core middleware ├── Data/ — DbContext; seed data via HasData() in OnModelCreating ├── Migrations/ — EF Core migrations; Npgsql/ subdirectory for PostgreSQL provider ├── Utilities/ — Internal helpers: HttpContext, Swagger, PlayerData seed source └── Storage/ — SQLite database file (runtime-generated, gitignored) test/Dotnet.Samples.AspNetCore.WebApi.Tests/ ├── Integration/ — Repository and WebApplication integration tests ├── Unit/ — Unit tests (controllers, services, validators) └── Utilities/ — Shared test helpers: PlayerFakes, PlayerMocks, PlayerStubs ``` **Layer rule**: `Controller → Service → Repository → Database`. Controllers must not access repositories directly. Business logic must not live in controllers. **Cross-cutting**: `Program.cs` wires health checks (`GET /health`), rate limiting, CORS (dev only), and Swagger UI (dev only). Serilog is configured at host level. All validators are registered via `AddValidatorsFromAssemblyContaining()`. ## Coding Guidelines - **Naming**: PascalCase (public members), camelCase (private fields with `_` prefix) - **DI**: Primary constructors everywhere - **Async**: All I/O operations use `async`/`await`; no `ConfigureAwait(false)` (unnecessary in ASP.NET Core) - **Reads**: Use `AsNoTracking()` for all EF Core read queries - **Errors**: RFC 7807 Problem Details via `TypedResults.Problem(new HttpValidationProblemDetails(...))` for validation failures (422) and `TypedResults.Problem(statusCode: ...)` for other errors - **Logging**: Structured logging via `ILogger`; never `Console.Write` - **Avoid**: synchronous EF Core APIs, controller business logic, static service/repository classes ### Test naming conventions Two naming patterns, strictly by layer: | Layer | Location | Pattern | Example | |----------------------|-----------------------|---------------------------------------------------------------|------------------------------------------------------------------| | Controller (unit) | `test/.../Unit/` | `{HttpMethod}_{Resource}_{Condition}_Returns{Outcome}` | `Get_Players_Existing_ReturnsPlayers` | | Service / Validator | `test/.../Unit/` | `{MethodName}_{StateUnderTest}_{ExpectedBehavior}` | `RetrieveAsync_CacheMiss_QueriesRepositoryAndCachesResult` | | HTTP integration | `test/.../Integration/` | `{HttpMethod}_{Resource}_{Condition}_Returns{Outcome}` | `Get_Players_Existing_Returns200Ok` | Each pattern has exactly three underscore-delimited segments where `{HttpMethod}_{Resource}` counts as the first segment. Do not add a fourth segment. ### FluentValidation rule sets Validators use CRUD-named rule sets to make intent explicit. Use `RuleSet("Create", ...)` and `RuleSet("Update", ...)` — never anonymous / default rules. ```csharp // "Create" rule set — POST /players // Includes BeUniqueSquadNumber to prevent duplicate squad numbers on insert. RuleSet("Create", () => { RuleFor(p => p.SquadNumber) .MustAsync(BeUniqueSquadNumber).WithMessage("SquadNumber must be unique."); // ... other rules }); // "Update" rule set — PUT /players/squadNumber/{n} // BeUniqueSquadNumber intentionally omitted: the player already exists in DB. RuleSet("Update", () => { // ... same structural rules, no uniqueness check }); ``` Controllers must call the appropriate rule set explicitly: ```csharp // POST await validator.ValidateAsync(player, opts => opts.IncludeRuleSets("Create")); // PUT await validator.ValidateAsync(player, opts => opts.IncludeRuleSets("Update")); ``` ### Mocking validators in controller tests `ValidateAsync(T, Action>)` is a FluentValidation extension method. Internally it calls `ValidateAsync(IValidationContext, CancellationToken)`. Moq must target the **interface overload**, not the generic one: ```csharp // ✅ Correct — matches the overload actually called at runtime _validatorMock .Setup(v => v.ValidateAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(new ValidationResult()); // ❌ Wrong — targets a different overload; mock is never hit → NullReferenceException _validatorMock .Setup(v => v.ValidateAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(new ValidationResult()); ``` Add `using FluentValidation;` to any test file that calls the rule set overload. ### Test utilities `test/.../Utilities/` contains shared helpers used across all unit tests: | Class | Purpose | |-----------------|-------------------------------------------------------------------------| | `PlayerFakes` | Deterministic in-memory objects: `MakeNew()`, `MakeRequestModelForCreate()`, `MakeRequestModelForUpdate(n)`, `MakeFromStarting11(n)` | | `PlayerMocks` | Pre-configured `Mock` setups for common scenarios | | `PlayerStubs` | Simple stub implementations where Moq would be overkill | Always prefer `PlayerFakes` factory methods over constructing test data inline. ## Commands ### Quick Start ```bash dotnet restore dotnet build dotnet run --project src/Dotnet.Samples.AspNetCore.WebApi # https://localhost:9000 dotnet watch run --project src/Dotnet.Samples.AspNetCore.WebApi # hot reload dotnet test --settings .runsettings # with coverage docker compose up ``` ### Pre-commit Checks 1. Update `CHANGELOG.md` `[Unreleased]` section (Added / Changed / Fixed / Removed) 2. `dotnet build --configuration Release` — must succeed 3. `dotnet test --settings .runsettings` — all tests must pass 4. `dotnet csharpier .` — format; fix any reported issues 5. Commit message follows Conventional Commits format (enforced by commitlint) 6. If this commit introduces or changes an architectural decision, update CLAUDE.md and create or amend the relevant ADR in `docs/adr/`. ### Commits Format: `type(scope): description (#issue)` — max 80 chars Types: `feat` `fix` `chore` `docs` `test` `refactor` `ci` `perf` Example: `feat(api): add player search endpoint (#123)` ## Agent Mode ### Proceed freely - Route handlers and controllers - Service layer logic and caching - Repository implementations - Unit and integration tests - Documentation and CHANGELOG updates - Bug fixes and refactoring within existing patterns ### Ask before changing - Database schema (entity fields, migrations) - Dependencies (`*.csproj`, `global.json`) - CI/CD configuration (`.github/workflows/`) - Docker setup - Application configuration (`appsettings.json`) - API contracts (breaking DTO changes) - Caching strategy or TTL values - FluentValidation rule set structure (adding or removing rule sets affects controller callers and tests) ### Never modify - Production configurations or deployment secrets - `.runsettings` coverage thresholds - Port configuration (9000) - Migration namespace constants in `ProviderSpecificMigrationsAssembly` — renaming breaks runtime provider filtering for one or both providers - CD pipeline tag format (`vX.Y.Z-stadium`) or the stadium name sequence — names are assigned sequentially A→Z from the list in `CHANGELOG.md`; the next name is always the next unused letter ### Creating Issues This project uses Spec-Driven Development (SDD): discuss in Plan mode first, create a GitHub Issue as the spec artifact, then implement. Always offer to draft an issue before writing code. **Feature request** (`enhancement` label): Problem · Proposed Solution · Suggested Approach (optional) · Acceptance Criteria · References **Bug report** (`bug` label): Description · Steps to Reproduce · Expected/Actual Behavior · Environment · Additional Context · Possible Solution (optional) ### Key workflows **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 → update `HasData()` seed data in `OnModelCreating` if needed → add migrations for both providers → update tests → run `dotnet test`. ```bash # SQLite migration (default) dotnet ef migrations add --project src/Dotnet.Samples.AspNetCore.WebApi # PostgreSQL migration DATABASE_PROVIDER=postgres DATABASE_URL="Host=localhost;..." \ dotnet ef migrations add --project src/Dotnet.Samples.AspNetCore.WebApi --output-dir Migrations/Npgsql ``` **Switch database provider**: - Set `DATABASE_PROVIDER=postgres` to use PostgreSQL, or leave unset for SQLite (default). - `DATABASE_URL` (required for PostgreSQL) follows the Npgsql convention: `Host=...;Database=...;Username=...;Password=...`. - For SQLite, `STORAGE_PATH` overrides the default file path (`AppContext.BaseDirectory/storage/players-sqlite3.db`). - `ProviderSpecificMigrationsAssembly` filters migration discovery to the active provider's namespace at runtime — no code changes needed to switch. - Migrations run automatically at startup via `MigrateAsync()`; no manual `dotnet ef database update` is required. ## Invariants (never change without explicit discussion) - **Port**: 9000 — configured via Docker (`Dockerfile` ENV and `compose.yaml` ports) - **API contract**: endpoints, HTTP status codes, and response shapes are fixed; do not change them without explicit discussion - **Commit format**: `type(scope): description (#issue)` — max 80 chars - **Conventional Commits types**: `feat` `fix` `chore` `docs` `test` `refactor` `ci` `perf` - **CHANGELOG.md** `[Unreleased]` section must be updated before every commit ## Architecture Decision Records Significant architectural decisions are documented in `docs/adr/` (ADR-0001–0017). Load `#file:docs/adr/README.md` when: - The user asks about architectural choices or "why we use X" - Proposing changes to core architecture or dependencies - Historical context for past decisions is needed Each ADR is self-contained. When a proposal would change an accepted decision, create a new ADR rather than editing the existing one. **After completing work**: Suggest a branch name (e.g. `feat/add-player-search`) and a commit message following Conventional Commits including co-author line: ```text feat(scope): description (#issue) Co-authored-by: Claude Sonnet 4.6 ``` # aha ## Claude Code - Run `/pre-commit` to execute the full pre-commit checklist for this project.