Files
haucki 325a257153
SonarQube Scan / SonarQube Trigger (push) Failing after 1m56s
CLAUDE.md aktualisiert
2026-06-29 10:45:19 +02:00

13 KiB
Raw Permalink Blame History

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

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<T> + 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<PlayerRequestModelValidator>().

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

// "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:

// 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<ValidationStrategy<T>>) is a FluentValidation extension method. Internally it calls ValidateAsync(IValidationContext, CancellationToken). Moq must target the interface overload, not the generic one:

// ✅ Correct — matches the overload actually called at runtime
_validatorMock
    .Setup(v => v.ValidateAsync(It.IsAny<IValidationContext>(), It.IsAny<CancellationToken>()))
    .ReturnsAsync(new ValidationResult());

// ❌ Wrong — targets a different overload; mock is never hit → NullReferenceException
_validatorMock
    .Setup(v => v.ValidateAsync(It.IsAny<PlayerRequestModel>(), It.IsAny<CancellationToken>()))
    .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<T> 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

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.

# SQLite migration (default)
dotnet ef migrations add <Name> --project src/Dotnet.Samples.AspNetCore.WebApi
# PostgreSQL migration
DATABASE_PROVIDER=postgres DATABASE_URL="Host=localhost;..." \
  dotnet ef migrations add <Name> --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:

feat(scope): description (#issue)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

aha

Claude Code

  • Run /pre-commit to execute the full pre-commit checklist for this project.