From 5e5b875e37b5393469a6fcbe250ca8aa82d9eda7 Mon Sep 17 00:00:00 2001 From: Dieter Hauck Date: Mon, 29 Jun 2026 07:36:04 +0200 Subject: [PATCH] first commit --- .claude/commands/pre-commit.md | 15 + .claude/commands/pre-release.md | 121 ++ .claude/settings.json | 7 + .coderabbit.yaml | 406 ++++++ .dockerignore | 339 +++++ .env.example | 9 + .gitignore | 7 + .markdownlint.json | 6 + .runsettings | 22 + .sonarcloud.properties | 108 ++ .../DesignTimeBuild/.dtbcache.v2 | Bin 0 -> 277001 bytes ...f5bc1017-3a26-4825-bddc-3e3d9a72a097.vsidx | Bin 0 -> 280857 bytes .../v18/.futdcache.v2 | Bin 0 -> 390 bytes .vs/Dotnet.Samples.AspNetCore.WebApi/v18/.suo | Bin 0 -> 24064 bytes .../v18/DocumentLayout.json | 54 + ...samples.aspnetcore.webapi.metadata.v10.bin | Bin 0 -> 4422 bytes ...samples.aspnetcore.webapi.projects.v10.bin | Bin 0 -> 63590 bytes ....samples.aspnetcore.webapi.strings.v10.bin | Bin 0 -> 202397 bytes CHANGELOG.md | 257 ++++ CLAUDE.md | 250 ++++ CODE_OF_CONDUCT.md | 128 ++ CONTRIBUTING.md | 64 + Dockerfile | 70 + Dotnet.Samples.AspNetCore.WebApi.sln | 36 + LICENSE | 21 + README.md | 355 +++++ RELEASES.md | 73 + azure-pipelines.yml | 75 + codecov.yml | 68 + commitlint.config.mjs | 14 + compose.yaml | 45 + ...-adopt-traditional-layered-architecture.md | 32 + ...02-use-mvc-controllers-over-minimal-api.md | 33 + docs/adr/0003-use-sqlite-for-data-storage.md | 33 + .../0004-use-uuid-as-database-primary-key.md | 32 + ...05-use-squad-number-as-api-mutation-key.md | 34 + ...use-rfc-7807-problem-details-for-errors.md | 32 + ...-fluentvalidation-over-data-annotations.md | 35 + .../0008-use-automapper-for-dto-mapping.md | 32 + docs/adr/0009-implement-in-memory-caching.md | 34 + ...0010-use-serilog-for-structured-logging.md | 34 + .../0011-use-docker-for-containerization.md | 33 + ...-use-stadium-themed-semantic-versioning.md | 35 + docs/adr/0013-testing-strategy.md | 49 + .../0014-configurable-database-provider.md | 87 ++ .../adr/0015-use-full-replace-put-no-patch.md | 49 + .../0016-ai-assisted-development-workflow.md | 76 + docs/adr/0017-spec-driven-development.md | 61 + docs/adr/README.md | 67 + global.json | 6 + scripts/entrypoint.sh | 35 + scripts/healthcheck.sh | 5 + .../AuthorizeCheckOperationFilter.cs | 46 + .../RateLimiterConfiguration.cs | 25 + .../Controllers/PlayerController.cs | 295 ++++ .../Data/PlayerDbContext.cs | 41 + .../Dotnet.Samples.AspNetCore.WebApi.csproj | 38 + .../Enums/Enumeration.cs | 42 + .../Enums/Position.cs | 63 + .../Extensions/MiddlewareExtensions.cs | 21 + .../Extensions/ServiceCollectionExtensions.cs | 255 ++++ .../Mappings/PlayerMappingProfile.cs | 61 + .../Middlewares/ExceptionMiddleware.cs | 121 ++ .../20260409141647_InitialCreate.Designer.cs | 69 + .../20260409141647_InitialCreate.cs | 50 + .../20260409141707_SeedStarting11.Designer.cs | 222 +++ .../20260409141707_SeedStarting11.cs | 251 ++++ ...20260409141721_SeedSubstitutes.Designer.cs | 429 ++++++ .../20260409141721_SeedSubstitutes.cs | 327 +++++ .../20260409151000_InitialCreate.Designer.cs | 68 + .../Npgsql/20260409151000_InitialCreate.cs | 53 + .../20260409151100_SeedStarting11.Designer.cs | 68 + .../Npgsql/20260409151100_SeedStarting11.cs | 251 ++++ ...20260409151200_SeedSubstitutes.Designer.cs | 68 + .../Npgsql/20260409151200_SeedSubstitutes.cs | 327 +++++ .../PlayerDbContextModelSnapshot.cs | 426 ++++++ .../ProviderSpecificMigrationsAssembly.cs | 47 + .../Models/Player.cs | 67 + .../Models/PlayerRequestModel.cs | 54 + .../Models/PlayerResponseModel.cs | 47 + .../Program.cs | 129 ++ .../Properties/launchSettings.json | 12 + .../Repositories/IPlayerRepository.cs | 21 + .../Repositories/IRepository.cs | 47 + .../Repositories/PlayerRepository.cs | 18 + .../Repositories/Repository.cs | 35 + .../Services/IPlayerService.cs | 59 + .../Services/PlayerService.cs | 150 ++ .../Utilities/HttpContextUtilities.cs | 47 + .../Utilities/PlayerData.cs | 917 ++++++++++++ .../Utilities/SwaggerUtilities.cs | 64 + .../Validators/PlayerRequestModelValidator.cs | 135 ++ .../appsettings.Development.json | 47 + .../appsettings.json | 47 + .../packages.lock.json | 1289 +++++++++++++++++ ...net.Samples.AspNetCore.WebApi.Tests.csproj | 29 + .../Integration/PlayerRepositoryTests.cs | 186 +++ .../Integration/PlayerWebApplicationTests.cs | 345 +++++ .../Unit/PlayerControllerTests.cs | 544 +++++++ .../Unit/PlayerServiceTests.cs | 400 +++++ .../Unit/PlayerValidatorTests.cs | 381 +++++ .../Usings.cs | 1 + .../Utilities/DatabaseFakes.cs | 59 + .../Utilities/PlayerFakes.cs | 228 +++ .../Utilities/PlayerMocks.cs | 113 ++ .../Utilities/PlayerStubs.cs | 31 + .../Utilities/TestAuthHandler.cs | 30 + .../packages.lock.json | 767 ++++++++++ 108 files changed, 13347 insertions(+) create mode 100644 .claude/commands/pre-commit.md create mode 100644 .claude/commands/pre-release.md create mode 100644 .claude/settings.json create mode 100644 .coderabbit.yaml create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .markdownlint.json create mode 100644 .runsettings create mode 100644 .sonarcloud.properties create mode 100644 .vs/Dotnet.Samples.AspNetCore.WebApi/DesignTimeBuild/.dtbcache.v2 create mode 100644 .vs/Dotnet.Samples.AspNetCore.WebApi/FileContentIndex/f5bc1017-3a26-4825-bddc-3e3d9a72a097.vsidx create mode 100644 .vs/Dotnet.Samples.AspNetCore.WebApi/v18/.futdcache.v2 create mode 100644 .vs/Dotnet.Samples.AspNetCore.WebApi/v18/.suo create mode 100644 .vs/Dotnet.Samples.AspNetCore.WebApi/v18/DocumentLayout.json create mode 100644 .vs/ProjectEvaluation/dotnet.samples.aspnetcore.webapi.metadata.v10.bin create mode 100644 .vs/ProjectEvaluation/dotnet.samples.aspnetcore.webapi.projects.v10.bin create mode 100644 .vs/ProjectEvaluation/dotnet.samples.aspnetcore.webapi.strings.v10.bin create mode 100644 CHANGELOG.md create mode 100644 CLAUDE.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 Dockerfile create mode 100644 Dotnet.Samples.AspNetCore.WebApi.sln create mode 100644 LICENSE create mode 100644 README.md create mode 100644 RELEASES.md create mode 100644 azure-pipelines.yml create mode 100644 codecov.yml create mode 100644 commitlint.config.mjs create mode 100644 compose.yaml create mode 100644 docs/adr/0001-adopt-traditional-layered-architecture.md create mode 100644 docs/adr/0002-use-mvc-controllers-over-minimal-api.md create mode 100644 docs/adr/0003-use-sqlite-for-data-storage.md create mode 100644 docs/adr/0004-use-uuid-as-database-primary-key.md create mode 100644 docs/adr/0005-use-squad-number-as-api-mutation-key.md create mode 100644 docs/adr/0006-use-rfc-7807-problem-details-for-errors.md create mode 100644 docs/adr/0007-use-fluentvalidation-over-data-annotations.md create mode 100644 docs/adr/0008-use-automapper-for-dto-mapping.md create mode 100644 docs/adr/0009-implement-in-memory-caching.md create mode 100644 docs/adr/0010-use-serilog-for-structured-logging.md create mode 100644 docs/adr/0011-use-docker-for-containerization.md create mode 100644 docs/adr/0012-use-stadium-themed-semantic-versioning.md create mode 100644 docs/adr/0013-testing-strategy.md create mode 100644 docs/adr/0014-configurable-database-provider.md create mode 100644 docs/adr/0015-use-full-replace-put-no-patch.md create mode 100644 docs/adr/0016-ai-assisted-development-workflow.md create mode 100644 docs/adr/0017-spec-driven-development.md create mode 100644 docs/adr/README.md create mode 100644 global.json create mode 100644 scripts/entrypoint.sh create mode 100644 scripts/healthcheck.sh create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Configurations/AuthorizeCheckOperationFilter.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Configurations/RateLimiterConfiguration.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Data/PlayerDbContext.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Dotnet.Samples.AspNetCore.WebApi.csproj create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Enums/Enumeration.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Enums/Position.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Extensions/MiddlewareExtensions.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Extensions/ServiceCollectionExtensions.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Mappings/PlayerMappingProfile.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Middlewares/ExceptionMiddleware.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141647_InitialCreate.Designer.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141647_InitialCreate.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141707_SeedStarting11.Designer.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141707_SeedStarting11.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141721_SeedSubstitutes.Designer.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141721_SeedSubstitutes.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151000_InitialCreate.Designer.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151000_InitialCreate.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151100_SeedStarting11.Designer.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151100_SeedStarting11.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151200_SeedSubstitutes.Designer.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151200_SeedSubstitutes.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Migrations/PlayerDbContextModelSnapshot.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Migrations/ProviderSpecificMigrationsAssembly.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Models/Player.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Models/PlayerRequestModel.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Models/PlayerResponseModel.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Program.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Properties/launchSettings.json create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Repositories/IPlayerRepository.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Repositories/IRepository.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Repositories/PlayerRepository.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Repositories/Repository.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Services/IPlayerService.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Services/PlayerService.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Utilities/HttpContextUtilities.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Utilities/PlayerData.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Utilities/SwaggerUtilities.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/Validators/PlayerRequestModelValidator.cs create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/appsettings.Development.json create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/appsettings.json create mode 100644 src/Dotnet.Samples.AspNetCore.WebApi/packages.lock.json create mode 100644 test/Dotnet.Samples.AspNetCore.WebApi.Tests/Dotnet.Samples.AspNetCore.WebApi.Tests.csproj create mode 100644 test/Dotnet.Samples.AspNetCore.WebApi.Tests/Integration/PlayerRepositoryTests.cs create mode 100644 test/Dotnet.Samples.AspNetCore.WebApi.Tests/Integration/PlayerWebApplicationTests.cs create mode 100644 test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerControllerTests.cs create mode 100644 test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerServiceTests.cs create mode 100644 test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerValidatorTests.cs create mode 100644 test/Dotnet.Samples.AspNetCore.WebApi.Tests/Usings.cs create mode 100644 test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/DatabaseFakes.cs create mode 100644 test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerFakes.cs create mode 100644 test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerMocks.cs create mode 100644 test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerStubs.cs create mode 100644 test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/TestAuthHandler.cs create mode 100644 test/Dotnet.Samples.AspNetCore.WebApi.Tests/packages.lock.json diff --git a/.claude/commands/pre-commit.md b/.claude/commands/pre-commit.md new file mode 100644 index 0000000..94f1407 --- /dev/null +++ b/.claude/commands/pre-commit.md @@ -0,0 +1,15 @@ +Before running the checklist, run `git fetch origin`. If the current branch is behind `origin/master`, stop and rebase before proceeding. + +Run the pre-commit checklist for this project: + +1. Update `CHANGELOG.md` `[Unreleased]` section — add an entry under the appropriate subsection (Added / Changed / Fixed / Removed) describing the changes made, referencing the issue number. +2. Run `dotnet build --configuration Release` — must succeed. +3. Run `dotnet test --settings .runsettings` — all tests must pass. +4. If `dotnet csharpier` is available, run `dotnet csharpier --check .` — must pass + (run `dotnet csharpier .` to auto-fix). Skip this step with a note if not installed. +5. If `coderabbit` CLI is installed, run `coderabbit review --type uncommitted --prompt-only`: + - If actionable/serious findings are reported, stop and address them before proposing the commit. + - If only nitpick-level findings, report them and continue to the commit proposal. + - If `coderabbit` is not installed, skip this step with a note. + +Run steps 1–4, report the results clearly, then run step 5 (CodeRabbit review) if available, then propose a branch name and commit message for my approval using the format `type(scope): description (#issue)` (max 80 chars; types: `feat` `fix` `chore` `docs` `test` `refactor` `ci` `perf`). Do not create the branch or commit until I explicitly confirm. diff --git a/.claude/commands/pre-release.md b/.claude/commands/pre-release.md new file mode 100644 index 0000000..47727de --- /dev/null +++ b/.claude/commands/pre-release.md @@ -0,0 +1,121 @@ +Run the pre-release checklist for this project. Work through all three phases +in order, pausing for explicit confirmation at each decision point before +proceeding. Never create a branch, commit, tag, or push without approval. + +--- + +## Phase 1 — Determine next release + +1. Run `git status` and confirm the working tree is clean and on `master`. + If not, stop and report the problem. + +2. Run `git tag --sort=-v:refname` to list existing tags. Identify the most + recent tag matching `v*.*.*-*` and extract its stadium codename. + +3. Read the A–Z stadium table from `CHANGELOG.md` to find the next stadium: + - **No tags yet**: start at `A` (first stadium in the table). + - **Normal case**: use the stadium that follows the last used codename + alphabetically. If letters were skipped, pick the next after the + highest existing codename — do not backfill gaps. + - **Last codename is `Z`** (Zentralstadion): the list is finite. Stop and + refer to ADR 0012 for guidance on extending or revisiting the convention. + +4. Read the `[Unreleased]` section of `CHANGELOG.md` and infer the version + bump using these rules (applied in order — first match wins): + - Any entry contains the word **BREAKING** (case-insensitive), a + `BREAKING CHANGE:` token in a commit footer, or a `!` suffix after + the commit type/scope (e.g. `feat!:` or `feat(scope)!:`) → **major** bump + - Any `### Added` subsection has entries → **minor** bump + - Otherwise (only `### Changed`, `### Fixed`, `### Removed`) → **patch** bump + +5. Compute the next version by applying the bump to the current latest tag's + semver (e.g. `v2.1.0-dusseldorf` + minor → `2.2.0`). + +6. Present a summary for confirmation before continuing: + - Last tag and stadium + - Next version and stadium codename + - Bump type and the reasoning (what triggered it) + - Proposed tag: `vX.Y.Z-{stadium}` + - Proposed branch: `release/vX.Y.Z-{stadium}` + + **Wait for explicit approval before proceeding to Phase 2.** + +--- + +## Phase 2 — Prepare release branch + +1. Create branch `release/vX.Y.Z-{stadium}` from `master`. + +2. Edit `CHANGELOG.md`: + - Replace `## [Unreleased]` with `## [X.Y.Z - StadiumName] - YYYY-MM-DD` + (use today's date; use the stadium's display name from the table, e.g. + "Bernabeu", "Centenario"). + - Consolidate duplicate subsection headings (e.g. two `### Added` blocks + should be merged into one). + - Add a new empty `## [Unreleased]` section at the top (above the new + versioned heading) with the standard subsections. + - Update the compare links at the bottom of the file: + - `[unreleased]` → `.../compare/vX.Y.Z-{stadium}...HEAD` + - Add `[X.Y.Z - StadiumName]` → `.../compare/v{prev-tag}...vX.Y.Z-{stadium}` + +3. Show the full diff of `CHANGELOG.md`. + +4. If `coderabbit` CLI is installed, run `coderabbit review --type uncommitted --prompt-only` + on the uncommitted CHANGELOG changes: + - If actionable/serious findings are reported, stop and address them before proceeding. + - If only nitpick-level findings, report them and continue. + - If `coderabbit` is not installed, skip with a note. + +5. Propose this commit message: + + ```text + docs(changelog): prepare release notes for vX.Y.Z-{stadium} (#issue) + ``` + + **Wait for explicit approval before committing.** + +6. Run `dotnet build --configuration Release` — must succeed. + +7. Run `dotnet test --settings .runsettings` — all tests must pass. + +8. If `dotnet csharpier` is available, run `dotnet csharpier --check .` — must pass + (run `dotnet csharpier .` to auto-fix). Skip with a note if not installed. + +9. Stage `CHANGELOG.md` and commit using the approved message from step 5. + +10. Propose opening a PR from `release/vX.Y.Z-{stadium}` into `master`. + **Wait for explicit approval before opening.** + +11. Open the PR with: + - Title: `docs(changelog): prepare release notes for vX.Y.Z-{stadium}` + - Body summarising what is included in this release. + +--- + +## Phase 3 — Tag and release + +1. Wait — do not proceed until the user confirms: + - CI is green + - The PR has been merged into `master` + +2. Once confirmed, run: + ```bash + git checkout master && git pull origin master + ``` + and show the resulting `git log --oneline -3`. + +3. Propose the annotated tag: + ```bash + git tag -a vX.Y.Z-{stadium} -m "Release X.Y.Z - StadiumName" + ``` + + **Wait for explicit approval before creating the tag.** + +4. Create the tag, then propose: + ```bash + git push origin vX.Y.Z-{stadium} + ``` + + **Wait for explicit approval before pushing.** Remind the user that pushing + the tag triggers the CD workflow which will build, publish the Docker image, + and create the GitHub Release. diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..e221d1b --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(dotnet *)" + ] + } +} diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000..48f5461 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,406 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json +# https://docs.coderabbit.ai/getting-started/configure-coderabbit + +# CodeRabbit Configuration +# Optimized for .NET 10 / ASP.NET Core Web API project + +language: en-US +early_access: true + +reviews: + profile: chill + request_changes_workflow: false + high_level_summary: true + high_level_summary_placeholder: "@coderabbitai summary" + review_status: true + commit_status: true + fail_commit_status: false + collapse_walkthrough: false + changed_files_summary: true + sequence_diagrams: true + estimate_code_review_effort: true + assess_linked_issues: true + related_issues: true + related_prs: true + suggested_labels: true + auto_apply_labels: false + suggested_reviewers: false + poem: false + abort_on_close: true + + path_instructions: + - path: "**/*.cs" + instructions: | + - Follow C# naming conventions: PascalCase for classes/methods/properties, camelCase for local variables + - Ensure nullable reference types are handled properly + - Verify async/await patterns are used consistently + - Check for proper dependency injection usage + - Validate that var is used appropriately for obvious types + - Ensure ILogger is used for logging, not Console.WriteLine + - Code should follow CSharpier formatting standards + + - path: "src/**/Controllers/**/*.cs" + instructions: | + - Controllers should be thin - delegate to services + - Verify proper HTTP status codes (200, 201, 404, 409, 500); payload + validation failures (FluentValidation) must return 422 Unprocessable + Entity via TypedResults.Problem(new HttpValidationProblemDetails(...)) + — not 400 Bad Request. See ADR-0006 in docs/adr/. + - Check that [ApiController] and route attributes are present + - Ensure validation happens before processing + - Confirm async controller actions return appropriate Result types + + - path: "src/**/Services/**/*.cs" + instructions: | + - Services should contain business logic and orchestration + - Verify proper use of IMemoryCache for read operations + - Check that AutoMapper is used for object transformations + - Ensure structured logging with ILogger + - Validate cache invalidation on data modifications + + - path: "src/**/Repositories/**/*.cs" + instructions: | + - Repositories should abstract data access + - Verify AsNoTracking() is used for read-only queries + - Check that all database operations are async + - Ensure proper null handling for not-found scenarios + - Validate LINQ queries for performance + + - path: "src/**/Models/**/*.cs" + instructions: | + - Models should be clean POCOs + - Check for proper use of nullable reference types + - Verify navigation properties are virtual (if using lazy loading) + - Ensure DTOs are separate from domain entities + + - path: "src/**/Validators/**/*.cs" + instructions: | + - Validators should use FluentValidation + - Check validation rules are for data structure, not business logic + - Ensure error messages are descriptive + - Verify validators are registered in DI container + + - path: "src/**/Mappings/**/*.cs" + instructions: | + - Profiles should inherit from AutoMapper Profile + - Verify CreateMap() calls cover all DTOs in use + - Check reverse mappings are defined where bidirectional mapping is needed + - Ensure profiles are registered via AddAutoMapper() in the DI setup + - Flag any manual property mappings that could be replaced by convention + + - path: "src/**/Middlewares/**/*.cs" + instructions: | + - Middleware must implement InvokeAsync(HttpContext context) signature + - Verify _next(context) is called unless the middleware intentionally short-circuits + - Ensure exceptions are not swallowed — rethrow or translate to Problem Details + - Middleware should be stateless; dependencies requiring request scope belong in services + + - path: "src/**/Extensions/**/*.cs" + instructions: | + - Extension methods should target IServiceCollection and return IServiceCollection + - Each method should encapsulate a single registration concern + - Verify service lifetimes (Singleton/Scoped/Transient) match the dependency's usage + - Follow the Add{Feature} naming convention (e.g. AddPlayerServices, AddRateLimiting) + + - path: "test/**/*.cs" + instructions: | + - Tests should use xUnit, Moq, and FluentAssertions + - Verify test naming follows the two-pattern convention (exactly 3 underscore-delimited segments): + - Controller tests: {HttpMethod}_{Resource}_{Condition}_Returns{Outcome} (e.g. Get_Players_Existing_ReturnsPlayers) + - Service/Validator tests: {MethodName}_{StateUnderTest}_{ExpectedBehavior} (e.g. RetrieveAsync_CacheMiss_QueriesRepositoryAndCachesResult) + - Check that mocks are properly configured + - Ensure async tests use Task return type + - Validate test data uses PlayerFakes factory methods from test/Utilities/ + - Tests should use [Fact] and [Theory] attributes with [Trait("Category", "Unit")] + + - path: "**/Dockerfile" + instructions: | + - Verify multi-stage builds are used + - Check that .NET 10 SDK and runtime versions match + - Ensure non-root user is used for security + - Validate HEALTHCHECK instruction is present + + - path: "**/appsettings*.json" + instructions: | + - Verify no sensitive data is committed + - Check configuration structure matches usage in code + - Ensure Development and Production configs are appropriate + + - path: "**/*.csproj" + instructions: | + - Verify .NET 10 (net10.0) target framework + - Check that nullable reference types are enabled + - Ensure package versions are up to date + - Validate that ImplicitUsings is enabled + - DATABASE_PROVIDER selects the active DB engine at runtime: unset = + SQLite (default), DATABASE_PROVIDER=postgres = PostgreSQL. Do not + hardcode a provider. Both SQLite and PostgreSQL migration paths must + remain valid. See ADR-0014 in docs/adr/. + + - path: ".github/workflows/**" + instructions: | + - Any CI job that runs migrations or integration tests must set + DATABASE_PROVIDER explicitly. Unset defaults to SQLite. See ADR-0014 + in docs/adr/. + + path_filters: + - "!**/bin/**" + - "!**/obj/**" + - "!**/logs/**" + - "!**/storage/**" + - "!**/*.db" + - "!**/*.db-shm" + - "!**/*.db-wal" + - "!**/packages.lock.json" + - "!**/Migrations/**" + - "!**/*.Designer.cs" + - "!**/ModelSnapshot.cs" + + auto_review: + enabled: true + auto_incremental_review: true + ignore_title_keywords: + - "WIP" + - "DO NOT REVIEW" + - "wip" + drafts: false + base_branches: + - master + - main + + finishing_touches: + docstrings: + enabled: true + unit_tests: + enabled: true + custom: + - name: "sync documentation" + instructions: | + This is a PoC/learning project targeting developers unfamiliar with the stack. + Documentation is a first-class concern. Review the PR changes and perform the + following three checks: + + ## 1. Method/function docstrings + For every public function, method, or handler touched in the PR: + - If it lacks a docstring/doc comment, add one using the idiomatic format + for the language and framework in use. + - If it has one but no longer matches the current signature, parameters, + or behavior, update it. + - Docstrings should explain *why* and *what*, not just restate the signature. + Assume the reader is learning the language. + + ## 2. README.md + Check whether the PR introduces or removes endpoints, changes behavior, + adds dependencies, or modifies how to run the project. + If so, update the relevant sections of README.md to reflect the current state. + Do not rewrite sections unrelated to the changes. + + ## 3. CLAUDE.md + If the PR introduces patterns, conventions, or architectural decisions that + should guide future AI-assisted contributions, add or update the relevant + instructions in CLAUDE.md. + Focus on things a developer (or AI assistant) unfamiliar with this specific + stack implementation should know before writing code here. + + - name: "enforce http error handling" + instructions: | + Audit all HTTP handler functions in the changed files. + Ensure errors return appropriate HTTP status codes (400 for bad input, + 404 for not found, 500 for unexpected errors) and a consistent JSON error + body with at least a "message" field. + Flag handlers that return 200 on error or swallow errors silently. + Use idiomatic error handling patterns for the language and framework in use. + + - name: "idiomatic review" + instructions: | + Review the changed files for non-idiomatic patterns given the language and + framework in use. Flag code that looks like it was translated from another + language rather than written naturally for this stack. Suggest idiomatic + alternatives with brief explanations. This is a PoC comparison project, + so idiomatic usage is a first-class concern. + + - name: "verify api contract" + instructions: | + Review the changed files and verify that all HTTP endpoints (method, path, + request body shape, and response shape) match the project's intended REST API + contract. Check the README or any spec/contract file in the repo for reference. + Flag any deviations — missing fields, wrong status codes, inconsistent naming. + Do not make changes; only report findings as a comment. + + pre_merge_checks: + docstrings: + mode: warning + threshold: 80 + title: + mode: warning + requirements: | + - Use Conventional Commits format (feat:, fix:, chore:, docs:, test:, refactor:) + - Keep under 80 characters + - Be descriptive and specific + description: + mode: off + issue_assessment: + mode: off + + tools: + # Secret scanners + gitleaks: + enabled: true + trufflehog: + enabled: true + + # IaC / infrastructure + checkov: + enabled: true + trivy: + enabled: true + hadolint: + enabled: true + + # General static analysis + semgrep: + enabled: true + opengrep: + enabled: true + + # File-type linters + yamllint: + enabled: true + actionlint: + enabled: true + markdownlint: + enabled: true + dotenvLint: + enabled: true + checkmake: + enabled: true + osvScanner: + enabled: true + github-checks: + enabled: true + timeout_ms: 120000 + + # Disable irrelevant tools for this .NET project + shellcheck: + enabled: false + ruff: + enabled: false + biome: + enabled: false + swiftlint: + enabled: false + phpstan: + enabled: false + phpmd: + enabled: false + phpcs: + enabled: false + golangci-lint: + enabled: false + detekt: + enabled: false + eslint: + enabled: false + flake8: + enabled: false + pylint: + enabled: false + rubocop: + enabled: false + buf: + enabled: false + regal: + enabled: false + pmd: + enabled: false + clang: + enabled: false + cppcheck: + enabled: false + clippy: + enabled: false + sqlfluff: + enabled: false + prismaLint: + enabled: false + oxc: + enabled: false + shopifyThemeCheck: + enabled: false + luacheck: + enabled: false + brakeman: + enabled: false + htmlhint: + enabled: false + languagetool: + enabled: false + circleci: + enabled: false + fortitudeLint: + enabled: false + stylelint: + enabled: false + blinter: + enabled: false + psscriptanalyzer: + enabled: false + +chat: + art: true + auto_reply: true + +knowledge_base: + opt_out: false + web_search: + enabled: true + code_guidelines: + enabled: true + filePatterns: + - "CLAUDE.md" + - "docs/adr/README.md" + learnings: + scope: auto + issues: + scope: auto + pull_requests: + scope: auto + mcp: + usage: auto + +code_generation: + docstrings: + language: en-US + path_instructions: + - path: "**/*.cs" + instructions: | + - Use XML documentation comments (///) for public APIs + - Include , , and tags + - Keep documentation concise and meaningful + - Don't state the obvious - add value + unit_tests: + path_instructions: + - path: "test/**/*.cs" + instructions: | + - Use xUnit framework with [Fact] and [Theory] attributes + - Follow the two-pattern naming convention (exactly 3 underscore-delimited segments): + - Controller tests: {HttpMethod}_{Resource}_{Condition}_Returns{Outcome} + - Service/Validator tests: {MethodName}_{StateUnderTest}_{ExpectedBehavior} + - Use [Trait("Category", "Unit")] attribute for all unit tests + - Use Moq for mocking dependencies + - Use FluentAssertions for readable assertions + - Ensure async tests return Task + - Structure tests with Arrange, Act, Assert comments + +issue_enrichment: + auto_enrich: + enabled: true + planning: + enabled: true + auto_planning: + enabled: true + labels: + - planning + labeling: + labeling_instructions: [] + auto_apply_labels: false diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c4ea545 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,339 @@ +# https://github.com/dotnet/runtime/blob/main/.dockerignore + +### VisualStudio ### + +# Tool Runtime Dir +**/.dotnet/ +**/.packages/ +**/.tools/ + +# User-specific files +**/*.suo +**/*.user +**/*.userosscache +**/*.sln.docstates + +# Build results +**/artifacts/ +**/.idea/ +**/[Dd]ebug/ +**/[Dd]ebugPublic/ +**/[Rr]elease/ +**/[Rr]eleases/ +**/bld/ +**/[Bb]in/ +**/[Oo]bj/ +**/msbuild.log +**/msbuild.err +**/msbuild.wrn +**/msbuild.binlog +**/.deps/ +**/.dirstamp +**/.libs/ +**/*.lo +**/*.o + +# Cross building rootfs +**/cross/rootfs/ +**/cross/android-rootfs/ + +# Visual Studio +**/.vs/ + +# Ionide +**/.ionide/ + +# MSTest test Results +**/[Tt]est[Rr]esult*/ +**/[Bb]uild[Ll]og.* + +#NUNIT +**/*.VisualState.xml +**/TestResult.xml + +# Build Results of an ATL Project +**/[Dd]ebugPS/ +**/[Rr]eleasePS/ +**/dlldata.c + +**/*_i.c +**/*_p.c +**/*.ilk +**/*.meta +**/*.obj +**/*.pch +**/*.pdb +!**/_.pdb +**/*.pgc +**/*.pgd +**/*.rsp +**/*.sbr +**/*.tlb +**/*.tli +**/*.tlh +**/*.tmp +**/*.tmp_proj +**/*.log +**/*.vspscc +**/*.vssscc +**/.builds +**/*.pidb +**/*.svclog +**/*.scc + +# Chutzpah Test files +**/_Chutzpah* + +# Visual C++ cache files +**/ipch/ +**/*.aps +**/*.ncb +**/*.opendb +**/*.opensdf +**/*.sdf +**/*.cachefile +**/*.VC.db + +# Visual Studio profiler +**/*.psess +**/*.vsp +**/*.vspx + +# TFS 2012 Local Workspace +**/$tf/ + +# Guidance Automation Toolkit +**/*.gpState + +# ReSharper is a .NET coding add-in +**/_ReSharper*/ +**/*.[Rr]e[Ss]harper +**/*.DotSettings.user + +# JustCode is a .NET coding addin-in +**/.JustCode + +# TeamCity is a build add-in +**/_TeamCity* + +# DotCover is a Code Coverage Tool +**/*.dotCover + +# NCrunch +**/_NCrunch_* +**/.*crunch*.local.xml + +# MightyMoose +**/*.mm.* +**/AutoTest.Net/ + +# Web workbench (sass) +**/.sass-cache/ + +# Installshield output folder +**/[Ee]xpress/ + +# DocProject is a documentation generator add-in +**/DocProject/buildhelp/ +**/DocProject/Help/*.HxT +**/DocProject/Help/*.HxC +**/DocProject/Help/*.hhc +**/DocProject/Help/*.hhk +**/DocProject/Help/*.hhp +**/DocProject/Help/Html2 +**/DocProject/Help/html + +# Publish Web Output +**/*.[Pp]ublish.xml +**/*.azurePubxml +**/*.pubxml +**/*.publishproj + +# NuGet Packages +**/*.nupkg +**/*.nuget.g.props +**/*.nuget.g.targets +**/*.nuget.cache +**/**/packages/* +**/project.lock.json +**/project.assets.json +**/*.nuget.dgspec.json + +# Windows Azure Build Output +**/csx/ +**/*.build.csdef + +# Windows Store app package directory +**/AppPackages/ + +# Others +**/*.Cache +**/ClientBin/ +**/[Ss]tyle[Cc]op.* +**/~$* +**/*.dbmdl +**/*.dbproj.schemaview +**/*.pfx +**/*.publishsettings +**/node_modules/ +**/*.metaproj +**/*.metaproj.tmp +**/bin.localpkg/ + +# RIA/Silverlight projects +**/Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +**/_UpgradeReport_Files/ +**/Backup*/ +**/UpgradeLog*.XML +**/UpgradeLog*.htm + +# SQL Server files +**/*.mdf +**/*.ldf + +# Business Intelligence projects +**/*.rdl.data +**/*.bim.layout +**/*.bim_*.settings + +# Microsoft Fakes +**/FakesAssemblies/ + +# C/C++ extension for Visual Studio Code +**/browse.VC.db +# Local settings folder for Visual Studio Code +**/**/.vscode/** +!**/**/.vscode/c_cpp_properties.json + +### MonoDevelop ### + +**/*.pidb +**/*.userprefs + +### Windows ### + +# Windows image file caches +**/Thumbs.db +**/ehthumbs.db + +# Folder config file +**/Desktop.ini + +# Recycle Bin used on file shares +**/$RECYCLE.BIN/ + +# Windows Installer files +**/*.cab +**/*.msi +**/*.msm +**/*.msp + +# Windows shortcuts +**/*.lnk + +### Linux ### + +**/*~ + +# KDE directory preferences +**/.directory + +### OSX ### + +**/.DS_Store +**/.AppleDouble +**/.LSOverride + +# Icon must end with two \r +**/Icon + +# Thumbnails +**/._* + +# Files that might appear on external disk +**/.Spotlight-V100 +**/.Trashes + +# Directories potentially created on remote AFP share +**/.AppleDB +**/.AppleDesktop +**/Network Trash Folder +**/Temporary Items +**/.apdisk + +# vim temporary files +**/[._]*.s[a-w][a-z] +**/[._]s[a-w][a-z] +**/*.un~ +**/Session.vim +**/.netrwhist +**/*~ + +# Visual Studio Code +**/.vscode/ + +# Private test configuration and binaries. +**/config.ps1 +**/**/IISApplications + +# VS debug support files +**/launchSettings.json + +# Snapcraft files +**/.snapcraft +**/*.snap +**/parts/ +**/prime/ +**/stage/ + +# CLR prebuilt generated files +!**/src/pal/prebuilt/idl/*_i.c + +# Valid 'debug' folder, that contains CLR debugging code +!**/src/**/debug + +# Ignore folders created by the CLR test build +**/TestWrappers_x64_[d|D]ebug +**/TestWrappers_x64_[c|C]hecked +**/TestWrappers_x64_[r|R]elease +**/TestWrappers_x86_[d|D]ebug +**/TestWrappers_x86_[c|C]hecked +**/TestWrappers_x86_[r|R]elease +**/TestWrappers_arm_[d|D]ebug +**/TestWrappers_arm_[c|C]hecked +**/TestWrappers_arm_[r|R]elease +**/TestWrappers_arm64_[d|D]ebug +**/TestWrappers_arm64_[c|C]hecked +**/TestWrappers_arm64_[r|R]elease +**/tests/src/common/test_runtime/project.json + +**/Vagrantfile +**/.vagrant + +# CMake files +**/CMakeFiles/ +**/cmake_install.cmake +**/CMakeCache.txt +**/Makefile + +# Cross compilation +**/cross/rootfs/* +**/cross/android-rootfs/* +# add x86 as it is ignored in 'Build results' +!**/cross/x86 + +#python import files +**/*.pyc + +# JIT32 files +**/src/jit32 + +# performance testing sandbox +**/sandbox + +# Allow the SQLite database to be included +!src/Dotnet.Samples.AspNetCore.WebApi/Data/players-sqlite3.db diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..679eefe --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +# Database provider: sqlite (default, no Docker dependency) | postgres (opt-in, requires Docker) +DATABASE_PROVIDER=sqlite + +# Required only when DATABASE_PROVIDER=postgres +# For Docker Compose, "postgres" resolves to the postgres service container +DATABASE_URL=Host=postgres;Database=players;Username=postgres;Password=P0579r35_p455W0rd! + +# Required only when DATABASE_PROVIDER=postgres (used by the postgres service in compose.yaml) +POSTGRES_PASSWORD=P0579r35_p455W0rd! diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0dd5f5b --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +logs/ +bin/ +obj/ +TestResults/ +storage/*.db +.env +.claude/settings.local.json diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..ef592cf --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,6 @@ +{ + "MD013": false, + "MD024": { + "siblings_only": true + } +} diff --git a/.runsettings b/.runsettings new file mode 100644 index 0000000..df1ca91 --- /dev/null +++ b/.runsettings @@ -0,0 +1,22 @@ + + + + + + + cobertura + true + $(MSBuildProjectDirectory) + + + + + .*\.Tests\.dll$ + + + + + + + + diff --git a/.sonarcloud.properties b/.sonarcloud.properties new file mode 100644 index 0000000..a17e587 --- /dev/null +++ b/.sonarcloud.properties @@ -0,0 +1,108 @@ +# ============================================================================= +# SonarCloud Automatic Analysis configuration +# https://docs.sonarsource.com/sonarqube-cloud/advanced-setup/automatic-analysis/ +# +# SonarCloud's automatic analysis (GitHub App, no CI workflow) reads THIS file. +# Wildcard patterns are NOT supported here, so all exclusion paths must be +# listed explicitly. +# ============================================================================= + +# 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/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/Migrations/Npgsql/20260409151000_InitialCreate.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151000_InitialCreate.Designer.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151100_SeedStarting11.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151100_SeedStarting11.Designer.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151200_SeedSubstitutes.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151200_SeedSubstitutes.Designer.cs + +# ============================================================================= +# Coverage exclusions +# Test files and generated/infrastructure code should not count against +# production code coverage metrics. +# ============================================================================= + +sonar.coverage.exclusions=\ + test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerControllerTests.cs,\ + test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerServiceTests.cs,\ + test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerValidatorTests.cs,\ + test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/DatabaseFakes.cs,\ + test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerFakes.cs,\ + 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/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/Migrations/Npgsql/20260409151000_InitialCreate.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151000_InitialCreate.Designer.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151100_SeedStarting11.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151100_SeedStarting11.Designer.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151200_SeedSubstitutes.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151200_SeedSubstitutes.Designer.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Data/PlayerDbContext.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Program.cs + +# ============================================================================= +# Duplicate code (CPD) exclusions +# Automatic analysis does not support wildcard patterns, so each file is +# listed explicitly. +# +# Migrations — EF Core migration files are intentionally repetitive +# (sequential InsertData calls generated by EF Core tooling). +# +# PlayerDbContext.cs — scaffolded EF Core infrastructure. +# +# PlayerData.cs — parallel WithId/without-Id method pairs mirror each other +# by design; duplication is structural, not accidental. +# +# PlayerRequestModelValidator.cs — the "Create" and "Update" rule sets share +# common rules by design; duplication is intentional. +# +# Test files — Fakes, Mocks, and Stubs are intentionally repetitive by design. +# ============================================================================= + +sonar.cpd.exclusions=\ + 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/Migrations/Npgsql/20260409151000_InitialCreate.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151000_InitialCreate.Designer.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151100_SeedStarting11.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151100_SeedStarting11.Designer.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151200_SeedSubstitutes.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151200_SeedSubstitutes.Designer.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Data/PlayerDbContext.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Utilities/PlayerData.cs,\ + src/Dotnet.Samples.AspNetCore.WebApi/Validators/PlayerRequestModelValidator.cs,\ + test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerControllerTests.cs,\ + test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerServiceTests.cs,\ + test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerValidatorTests.cs,\ + test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/DatabaseFakes.cs,\ + test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerFakes.cs,\ + 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 diff --git a/.vs/Dotnet.Samples.AspNetCore.WebApi/DesignTimeBuild/.dtbcache.v2 b/.vs/Dotnet.Samples.AspNetCore.WebApi/DesignTimeBuild/.dtbcache.v2 new file mode 100644 index 0000000000000000000000000000000000000000..5499efedc2f658aec8996acea8ed92431e60587a GIT binary patch literal 277001 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 literal 0 HcmV?d00001 diff --git a/.vs/Dotnet.Samples.AspNetCore.WebApi/FileContentIndex/f5bc1017-3a26-4825-bddc-3e3d9a72a097.vsidx b/.vs/Dotnet.Samples.AspNetCore.WebApi/FileContentIndex/f5bc1017-3a26-4825-bddc-3e3d9a72a097.vsidx new file mode 100644 index 0000000000000000000000000000000000000000..5d5514ea6dededbf71a1c7a454774e5ebc18a194 GIT binary patch literal 280857 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

4US6o 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

}mx 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%0Dh zeK*eBc;v=QH<50l+{EV6C4rlSZW6mm>87rmhHlz))3%%DZaQ|;xtkeoX1iJHX1SY9 z-KKJzp4-gaX6d%p-B#0WE@oBcinv8<_ouz zxt-GOdTzJxc1Lb6a(kIucy3X-#l$TZZeP3o((O;&{>&}=ZaH%Yg*$ZIVd4%mci47^ zxm$JJYUYl$JC5CP;*Oi{xb2R|?s)D_v^!b4)54vW?sVWzhwjXFXO26I+}XN2E8Q8T zEZw=~&RuujbLW-2aNI@aE}HJ5bQcSEX}L@7E>m~ebeC;+Irc=|6Dxe{8bf{s@sgbO zjMy_W&uH=pix-2a51=(2TDzfzn8>rbyrRQ1C>np!TZ<+nH1`N-M&ZfGldGPLJz02i z&P#nf^rQhL4b$l5Mx(UIePfoF%_e_lDB-SX_hvuB=Td5-iP zU*9ac)s@h zf)}3x)8@S@C% znqE|RQR!*h(~+mOrxQ;PJU#Vd%Zptv_Pp5l;?#>NW$q=RmqcEYdr8MjN-r6E$=pk& zmntuHsH9#R^4e!6z0CKr*vnEc%e<`RWu=!@Ubgg_p4SY#X5=+fui5gNmDikmt-x!w zy;kY9MqXQc?ZV3kUMKZBJ+D)EorTxcUbpFWTVA*Ab$ed7?{$Y>Z{YPtUSWAfz7{H@XEed4!m;gl}m46d4s?k5zhV&bAp5XFwGp!WUdLK=8X>?DkleF?kD~YsXNGpR8t&jLt z-?u7XuK3d6*|2nd8PF8hms@Fe>dV5Hm9Gq6Nghcl$5)}RVqaChZF6_Uqpr}mW8cnb zXhN?;-zli`^4);CB;QSZ&*4VI?S^h@>HCK7+dlt=zOQ}1@B_(HjllK;-wz@`(0-8l zLCX(DerWii<%fqTe$w%ik)KLGReoywY3Qf9pHBTu`B~s+sh-|G3T z!f(y}cEfKMetY8QEkE!0dC$))zZ27Hf#0?LZrAUXesARW#(tswV(1r@UrhaC=J%U^ zzx4Y;5eDXA^&(`irK&X#0!YU-bROz+VjgrR6W1{<7^aNB(l` zQ^Qy;15pnee$dDQLk5Ny81$h@Yu`MRqbHkJg=j<^1ad^vU7E-8+>WP7RbY$2Ug3G3 zZPW0YZ&wK{5`~C`i*FZ3Sr+ zq_ZG1f-DTOG|0L^HVm?Hkj;Xo7c|qLSq9Bv(6WM79<+*}H3(YEpzR0kIB2(m_AF@6 zgFFuMY0wFRPA}+8f)0J22c1RG?FBt6=tV&<4tlMipnsj9=mteEDEdKB21OMV^PnFG z{btbb2mLbWFM~1)$}A`cL0JU@D;T)Jpcf1V!C({&buerP!)`EY2cs&e8bMVARX-TJ z!8i`a-C#TnCUG$JgK0CErNP_}=KWyd28%pcRKa2vY|n%3RVeCVqZc;%ys|}ebpASu zzI>u@o9HX0Ff^jji0K;s zy)aF~v=yd#m=3~p6sFTKU4)qtW^R~;VKxl2ahOfQY#BCn*i6D^8a9itS%%F?*owkd z7Pfj}yB)TtVLlH#e%J}ajt)C<*y)CyD(rT`Za?g~VJ{1N-LO}Ny>VC+VNr#}BrFzT zzaRFOVc8GMK{)7#gJn3Jg`+GS72#+^3y@*ug;g9@&2Zca$Gvbo3diGcya*>MoTTA2 z38#~AIt^#*;j9Q}lW;Z-=PH~#;UWkZ{ctgiL_HEKk=URwR-?v^^p}`4dultT6>~_04*e~MJkC@ z8mS_(SNPjVUO})U+mGxhvgeV#jGPsoRXc9vcr-7HoPcIBk-NfQTJl(&2E;VX9Y)?t zu?@S@1eBCm}66<&Fzl_Y+JM3Ju}KaTt)@>A-~qhKWpVtVGIAd7+`3InGt0&%91F{qO28Vd6adctQ%!TlntV69yN`q8AQ!2YNb&tk6PWR z)sI?L)LKMs6}9cC-HqA{o)&bPQ74Z&%c$!|JvZv*QLi8MhCCcAbX4@CViXlsR7|42 z74^GOzlcg5mEEZ9Mdc_er_sQP25~gVqCpW2%4jf(2GeNhN5l1KSVY4z8V;l3l71i% zjXF_fMO6@0X;jhYZZx){v5dxXG;T-ZG8zw~@hqCiXyQkcVKjB3X)BuPXqH5?ESe|L zycx~g(R@HJ{AiIzODkGt(XtgS^JrPDiu$T$@%lP{!9c4oG&6QrrAI@9RpqTJ`aj@B zMB81pLt2Ahb?jBgS#=^BXVXZU7HMc?OoQN6&tLVzRWDjyO;(d~wKZ97w^#H2YOk|e z=+z=yEsE8uT0OeIdUUl_d0UhA*7kVKO4h7&&1$V#`I^;Rv-)e+V9gq>S=E{~UelAc zxLk`zYsto1qSumgEg7w)dM%CD(rhi8ueGwZ)?|(H$&PFrHCY>P ztc{Oc8|$@kwl-d@O-5@|y)IVPg}E-&y0F)Uvo73q;jfEeU4-j`w#MrsSr_TL$ks)3 zU9{IlzAn1!qF5LGby2R1!MYf&i)vkr*Tr;Q%+|$xU2I$5;;e5Oudf#C>tcOSu2076 z+m2b^k#2}&LrgX{*EVWmqgLOjr5m+uqn2;fIvcfeqc+&6jW%l4Ms2)N-@0Mu8*A3a zT1w^K*r;vLpYCtOdLzy@l5!&%ZKQf5>u+T9ji%mcPBwb^MnBu=mm6hwqa17udK-gs zV=&pMh8yGd#(12FN+k!I^ib8w_NI?@~;xludv$n~SL(NP2Xty^7-75d?hZiKoK z=|*2S=<{4Rmb}u;zjdG=G0^wBYxI@2He2*XvNlWlx>sAawi5dKQ(G-<&9t@9QfO)L zw>tc7P^6{SvZZCAWvP`%>*iX8S{3};3SPUU84nFoXiYHH_DI`vT4U0VtDQhQT02ec zjI={DSo#%)tzBQcDa~fJJJs%t=9c`$5l_z|?PWAwq3MD4D1D)QK_h1EC)&?-VCx{# z!9WKK9U3}J=^dj(dZ?&hram#&QKF-vUSHQ*Xl+q(wWiOB)=jP3S{GXPwH|0a(e$g3 zSn1f-vDR^{<4ni7j=MVU>A0`sQpY15S2~{PMCrt#F&Af(Bs$4-($-0?la5ZhIw^HB z(8*9IBb|(OLi^8k8tPQ*G}URQ)22?_I?Z+3(`jF)rA`Mr9qM$X(@Licoi26e@)$o0 zbQbBXsk4^ON}Ww~Hq}i-H>GYyx|!={M>o5=Io8d&ZmsK<)~!spTDsNNtz5Tyx>e{_ zU$;u#8t7J~TNB+@x*h6vqT2)A9_jX2wyFeNS9i4TbakiHouTebb!Vx&isvHTt-7mqH`Cp=?&i8X(A^3D&Xj(_)~oB@itZWw zdy1aYJxBL^-CNbYt-7amFV?+8_cGmU>t3#V9o_5dUQhQ5-79r(q+y-KRsAx=eK0)n!kYg)U274s#ju)Wcj4r+PTo!=)Z6J&N>5>rtXdsUBr|)Y79| zkGgtP=uuyfNTS66*qmAV?}s?yb1 zR})=Lb+ypr|IgB!=r)ckZNt|xJ}=Pr@5K8OXB&uQao^XP>&I^Fx)Hk3bYtko)Qx>NKD%j$1aCKW-Nd^| zbyMq}>+Z?ACv;EMJx%v?-7|F0zIzTRH16g#_OIRCb+hPZ=w{i?rkg`I_uX8(86Wf0 zEw^rY>lWx1-7UFWo@}*Ys~6Nav+bR2Ion3IUD)dd1!1&&cO1nxYzJ(o*)FqPWBW5f zEPH{%3%0jx@7Q6_4o8BGiT}Q{17`53Rs7UhH{fT$x|y*pV|#o>#sp)UF~gW+tT29N{KB|o z+%ujS&x}_lUYXc4ab)5`y#hW%027{x!bBqpD(;TJ7;pxH0WgpZGy}uHGH?t$gTNp% zNDK;t$^f4=F!{>ljmeHl#w0Lln2bzTCMTx8m{LqRraV&<)0%0=bYi+OJup2nvnL+O zT$s7i#%wqxhw7Hjx(p2^UU?k z&CCPynt8)~V!mg-F+a0#W`VN+EGQNd3q1=13k!<}7Vj)F788p-i-pC3#gU~e9TADK zBv{fcMV1mvjpYl=fOG`w@o&X?%laoPf03rXtXbKx^1{jooh=1e(X1F&A}fiN%*x2> zg;mZfu#+0fW9vhhS! z`Pi|sXXC^s&L&`!W|Lu)XH#HPWz)c>Cz}@bys@WXPs5&xJ$p8EBANx84VxXC zGn*@$2R2V^;cO{v8QFs0Kly6IS1)|EN8vSJ{~uqk`Fg|GjITLg17AzNR(!4b+VFMc z>&(}MuPa|Sz8?7cAHLb}%?sar@U7xogThw6olq6ZUmtw8;k&?h1%+sQ*ZBU%53l@i zqCVrm_@U>=SAIO8H~7~ zqy@hSeo_1q`DNnQJ-;4_tGvGP>w{kzzXHEHejWIAlA32}6xN&i(7ELfN0vCyk!bRg^;9}(BNwyDI zbJ%cr;qXfON7!??aJX@}b9m?Q!Qqp`7spB#82ua&9Dsx2;5h^ikwfNCI8+Xe!@vPw z_P-o{lc7ZJxxCRvYesMb+(>RBH;J3fP35L>GjPke4cumKdu|K2 zBexT`Gj}WQesQ2iMyV=%3b4b2hWGVhscM*hsuY>#}gl~d^CKteDr)e@af1=cQD=Y0_PLn68V() zbBm-3e=7ded|q?Zch5UM-}nrC##;=ZEuRCQBcC&0u6zN$IR0P3e>(od`44^r{73R1 z&wn!isr+Z)KO_H{#9tp`wGpcqv3eIP{Qg_4{zdapQu6>J0^Bq>_F_4*afkRVwX{dD|Sun2C+Yhy%c*d z_F3$U*jF-RA(D3h6v>H06Ngb8W^vq$#O1x#n=EU2R0O`k@cm(n2#bXeU3Goi`t`K4np9Y&}vXOOL z(%yCl(Vazi!`4o8U!r58rMj>YM$)Rfp zwgO&AISvm2?*hK4-~vuB22y|$U7r?#1l2jk&CEAv?3-EvxtL;6PXJWqmwFuNLr*I zQWPnR)QeO^Y9b9H;lrLHA4I;3tVK2=TamrULF6cM61j+6MV>@{ivCOVQuI#rLG(%V zS(L3P7g4SPDYbGJl3UyY9_FzAnO{S zGY!@T?PusZgLQ&+Mvob~%g{&$>nE&#VDo}bFxY&+<`cF7wgt9Ncs-LqgB(FfZ6Qs8 z5Hf>T0k0DMHfXSc*Nmnb*sWmq3A+w098|fGPXk~l(R6`E3)mUhS#(pNi2^MXuuJHR zK>q`}9njSPy8+z`XkCDP2YZHQ0N6{^@uSop1@!PoC65YtINagD;2=<)42KCwT#G8t znZzR`xqCzL9Go(oo^V>=yn^!%&U@5d!TEwhC^$b*euPROxU5j80v7=nfQy666D|uX zE#PY58sUn>>MIh%$l4;q3bzWk5pFY5hH#f~*KoIRcW@7IZ*YIY+bhw1Zyme=yjgg> z!s7@J{3gTuJG^_ueBu4S@VST2j>@-BfKLsd20lG}M)*wdnGsq=s1&|_!Ph%{{SU!S z`2L1(fNu%k4gTK2-``~LMeSa9LBI~WJ9J;rb%>53=7q2nVnc}XAXo!kMEC;{4a6g$ z8xTqWw!)?z*dEvg*avpA)Ldfy$TTp6Js9>s*zEutfZ+*!!XlVjPby@wxWt+dD>+$I3Kprl7j@05~KoYKst~fWB{2!_8<$$266y7gW7|- zgL)%$5YGn%pcE(r%7Y4^5~u=d1T}$LK;J-jpe1MnI)JXA8|VSd3Cx)qcO$?=Fd0k% z)4oCDXuP2gs53-}fIHTVtq z7xF^+Pw-#hzrp_vz9XCf+X6h+Jb;(r4R{CMgOA`7_zb>+AHmPye?#~S!WzOB!Yg^I z!Uu$J2pu5}*jV7fh;MUH5Hth>AwkFx3WN%wk>fo6B1ddQa~}G!(71C4uP~M?@K>3971?3yc-%vV2T(H5y(x3no2}MCMP%IP&#X|{DB9sIr zLn%-ylm=yhGD4Z4;E$f6{0rp=%5SKDL0v)p1$7N|1N8;!E7Tp-J=AxoA5cG`enai3 zpTkSLN~jvDf$E_~s0nI@+Cwc+E7S&cfI2~)p)Sx?(ALm4&|aXuLfb*xLpwtIfc6RP z3tC6G5w?j~;52|Hp($t@nt|q^1!xgkf|j8bXcbz6Hb5JpP0(g&KhXXM!=0Gw0r4k* zLBn8Rh%h7=G7J@lCyZMdPl&9;$f%6M)j3KS4U7)P9>xmO8a=)+-C%lyse_5Z#AyXj z3MLOzgek#PV8TZ>m`0fXLDw}rw`BEu-r(6$kBimW6BoUNr-5gHXNG43%te^jFu%b3 z4l{=tU>;$fVE%#mH-;j>0l1$njdI>_r&UQxcYmp37AQr?mzQ4Y#(n7ng&7xJ#;eU@*Xd`t2zBeO0aZ}P#B z8cYv+BQ%dlH$vF58zoywww3HvvI7FL zk{QVc$)1w0BtO!2%cbO2@+5i2mQwOXiWRnDQgl)mu(vH$D(ru-?Nyq5u za8f!eotLgiH%d22H%s41&*@b0M*1NAApI!AR)zx^L_#M6lR?m#6O;^E1|#EMMk(VU z<5R|&k~z|btgGoD(@~~}Oz$#%%fw}}G6k8EOhu+ardj3_VmUHPnS;!|%thu&_FuA> zvRAUtvhQU-%W{yVlZDX{SCTA6mMY64>si)LR*==m8e}c9j6moQO?B%$~=^-aBCnKjUr%_Iee4ga< zB2j4Y)bcsXc`fIioVlEZoUNRToCi7M?X#R0xjf_oa%pPyqE_$fKb`tjF=j%oH)b0oXs&+4G_o;TB+HtiLY6og3(desoPVKzf1+^<` zhptOJ?UmYlwNGmQREM)Vpm*q~j#qW$>R8orQm2DD8FfnPd{bwk&Q6^tbvdewR+mLx z2X%AmmQi)5Zj-t*lyj;7r_)O6Ue$d>#Sc{z52hXgnSS+|)z_E$UQ_r=_oX_fI<7jS zI*%-x>JlAZ<&E-D`J{YRzA67yepcb2!dZm}vAEc*V@(&73PuI1f>R->kX7ha zC@M4+Mio~o-cQTd?qkvL%lS#j6NUge;2Qn{%9 zr1~$_gX*p7d)4DDRrRClpQ?1Ka8(4=l|`#!RdK2WRgx-2l}VMSsw-93s%})>sk&G7 zpz2-K4^_Wa4XS2UdsVBdP1Q-&S+%2TC)KX1y{X1k)2ap4l4_G`c<-WyqZ%$qT&sbp z0n}jB5Y&*>P}DG}k*QIrQL53Yu~*}$##xPvnmRRUH3c;#H4SQ7)RU{HP*0_vR?S;A zAJxnhZo_zK%y>&uOQ#m57O57k7X0U^{yD0jqE{QedeN(Qz24|`r`JNSv)*j<=0$Hl z^yX7Q0rQA30cemKv<8FSf(DPpI+ofR3RaC+*l8HC(4kc*9UeTmp=*hs$5tyw zxV{3dEUp%sHy75u2Q-fI0o5tG)P)=F!mb=JDq=Auof4I?(dAh?CCkJZL&6Sc|O z6m6>;rS^JYl4WsAUYwd&fN&Bq*OvD2APeJOCbm(=cI*d9@ zI=<-`bR2XNI%%DvPD!U;r=nBUsp*7wjXF&_J#{|nEOa(HJDr2hQRk-fr1Mkvo$k5r zrS6^Xi|$8aFbe46bxFEpU8*jVt~Xs7wQ*JHs&)0c7TsQTJLz^I*0gcly>!#M1>K@< zNw=z7({0jiLD2`%sspxwQV(7aK@U+6RS!)MqaJ2G3O#x~Ry_`S#D6Zx!RiV0r1g~a z)bup#=Rsq^@*Id@nU$Wko}-?Vo~xcmJ^v8vvS1;j^+b*OeV zZ-D}PNOXu|l%we`rn?f`O!^)`!3`Y+oiUw7sS4^yP>_K_3KTAwZZT|U*uk(X>1gnx zBXZgr7LkiZh83Aq!#7B0QkVz78va3A8zRQ=Y{4tkAa#VS53)FhXQW#UugHiPJ{UeB z8)5iD*=Bzz1S4K34n#bRcsJq$;c_E7M7I%crY4>+Up%ag5ecD5BdQTUM*KFgGH^8T zKt#g;P*F+!Hg>t#pkjZBJsb8slYyD!KN5_TMj9g>7Jo(-te=dWDT9P606e*YYEn8x z@w_xfSvR5iZKgX>x{L=24j=4y|KmE!Pwcj zgK=P-F)kTbjcdj&#;=Xv8NWCF&G?7$?<7q^SlRTV0+?V-NG47vVB%=vY|@HyS<>F5he_U~Xi_pMn^a7yCXFVqOx~FMf;m7Y?@Yd% z{2*Nw!Yx^vY)y71dy}il&Gg>%gXu@pPo`f?*_d)Or89*S0FUqtmL9l!;Tu_;Dan-H zlwwLXrJ1@ibwfEKwIis$0u?E^&*3LfovFdpXlgPwn_5l9#}B5Cran!bO|6s&|8SZ9ym_eAqnIW5@ zn6V=yX*6bxW=v-6&2%*==$ODv&P>Tn#Y~f#X7k(|v==-Pc2c-2o2M~PYo6XbgLyXd z9L@8`%-+nsnVXphGcRV@o24@gVEu)cm07G=oLQ1tdb1R>4CcSW{I}Yz#1JmKQyA7@ zk9&K(*`u?^#h$o58GCZ}^t5Mb&l)|S=+(5Blf5{5$@VhX%WSX7UT1ro?R|@u274dv zo3L+S-?V*;_HDKgWgme&^qsO@v|X}YZ@Xf<(RNSU%_sx1%%J$gvdQu{%Z26Eas>L% zR!C%7kdQzY!NLg*dhXKRkQCE;4N##qx^Gg&iR`>?jN7Od6QW^1dp&Dy7R zd+To2G3&T>*}C5Pt@WMt(t2&Zvp!g#tS{CNHZU8!4Z((N!(`*xMrWh9G1;`DRi3O( z*`{LCXwz)dV$pw=JL+7qWX2zxqs__Y!S-9*pNV^?jp1a=-425TZ*j* zTd&AI+3IZdwkBIA+YV#|N4aA2cIc=KAMl%ChiHev4x=5Oc9`wR5W=*hwxh9QvSYSm zLw%5)fXenM*r{3cl%tXeo9w5t&upK?KC6A=?Sp;Jc0Su#Vu5dGZRcp`*)Cw0XqRG_ z#s2=ETdmz{<5n+j_3l<5ZuRZf8`2J}lUrxE`EZ+mF+a|2pKkZ*b{z={kU-l>w^JC> zpn9R_#6K!m+JGJ<9?9O0zg z$+%N;=c_vlckbPVp$FAn&+htg*XFJZ!M8W>ZnL{B?tXRmH+R?W?d0C1dvxx>+=HVm z9<}W5@pPYi_u08m=01)4^zJje&xV?5_Z?lgbKMt7t2#mL4v9GNMpRN}*P&Vng=;A7 z3XYAAKd2evnyx9H9M4W{o#;qIA(9i-iGgIK1n48|j_5MN7#cBU2hl-~2&RLI*q?)$ zg7oCs$u~rNoYYPlCxes4$=Rtdrv$ctP8p{xb}3F(>=2xKlDiQ#s#-bivG#I$avHac zyEBZ?YZGvb$GsRgR@}dEDkt-F(_=npV9e5q7M(wN9VKi ziwn#JM+V<`bMfwC=c0Dex|m#?9NK@AbSb)&U1~1PE?-?{*c#G0%g*KCa_{<+>%T~( zhC-5Fx!$&l&Qo#I>xuJo=9t}L$pa+v9e31Riy)y^S3Tb--f)xp)t zwWDjDYmAQ8X7NAaT5zp*t+To2#2Aw_MzEcPJTN7%~HHQEtg@sqUYX z`^oO-$FDYi_2O51zk2tp55M~M|3L-u1~V>+Ls)An1lGZCHh%Mh5s-fK;kVpxo!=(^ ziZ#L~NsM=bcm}K&cGmB_-&McEXsWZ{JB*|9hl4*Pf5`p_{#Y=1#2*KLV#HKaazyV> zgFo;6`QXn-f1dp1OvaH5^OxW+lfQ2LRiR_n--^G#`Fn@nHnf(Zlght2|AyZ;|2X-_ z)jyPfWd9iad+;9|EfZ*9@Sn|pt^D`ee}DV$zy06elthauZruY_<|GRu(Kc2$(RV1^ zFTSfN(nSrf?`8_DWe3k5q?jfxCJG-diu^o#dcGwMHD{zMLZ%&C79LRygt8t~?Re6` zEMBa$Hm{S{8M_h6>!r#Y)tacELO}tRu!v$}>FK-) z-bAduu!{1gcvG=b@dlr=cFAR z;HBgJ#rqHMKS_}8nfHRYDX*#C^wIm|ef56!{>O)d4|fu+W2cCvC?1R<`%rxBd}IXe z#N=c4aqw}XykU%OOBkqwB6pOXq7V$RpWsvUDfyIrDn3=8@R7662cH>HVcGbcd~QBZ zKF_|t`2NfH;Ct!uWG;I&YLi~R2w%XL-j~VOwXZL}?kFOze)#(BDAF3akANL+ZS;mJSJCE8BW4J-MnQ#ZXzV0F1W4I6D zO`)wZJUTSRp=k}xY2i`BqlL$a_O0-}4&R@|j(*4RJy5bW<^pzCw7Z0kg^rWl28mPh zhhTeTL+9wq?PBOMx)DNGL)TEOk8=FLZh>`y31V59LZx+JKCpyx;lS|TC2$qEM{OcX z4+HN}z=wjoAPzyCgSetXi-=hfQI``?hJpdxB=6V+c%isTI0lTqfE(G);3L3ifUf}G z0sbae2ND0AWI@EBBOT!a_y8e543Glk06lV<0V)!K0mcCM`b&V{LB0iPf^^7j1R0QZ z2oj;d14+4+3Cbg6iC84Uh(V1(&4}6roe+fyx&&PjE4RPrIkka9?UNF}5e(ijpxehz5~c@^>{|52%D4bQpXXLKtEgau{kDo?+aFu|qv9fo<5h zk3Nij7)uyy7?&_@1LizUm_VSgr^o&yOfF0zOkr|_IHx(uUM z&}tlJ2#D&m)UeLsprakY=D_qh7Ng#GxdVT^cO zZ{qq8*NpT9Ye{gzI>dE~>lQaJk+P~k;wHt-zi~Uo7am{F@fH8}@ioNPA-?`E?sjqa zNnWKLi#r~7fDgip)jU(B2Rk-iRVyOz1~1!6x#T$WM{qBL5`67s<_01&(lzM^!ZHo>9RZ zc}2M}ihxnt8#(IOR#B{@*hKMyx?2?2MsbVcBZ^P5i;-FsrJtw`(F6#Qcrl{B5LJUw z^ih;3Y7{MsC+gv7L;wlbUNF>|Vs}Z6gn}Uyl^`dBV3I>oON0<1qWl825vW~2Awq-^ z1q2bENbN`X9pxp;Ym|>DJCdRhNBdZsJ^56n@n$tMa4-zN&K0NN=2m!*&^C0AT1PCj;fEUL{+0|Q4LYy z5Bevnf1~1_0iVo&e2_?yJJTeU5qYAhc~w91}uQ0Z_z*T%*JyLH~JVP?zu5I+{ zj3-J6pbLfJyBVX5QQ;4c^p#@lV=OeKA)P6V1SRt*)SU+R^Cf{LYv2qd?RT--$K(Tt-*K3S5#hPR7V{Nex zv5v9+gX1P*TgSGE?IpI?*mkkK$MzB1XKdf(Vr(on9-APgQqu%aHczKSi?OBHN^CW@ z7TXxx9NSN9zvFNs@8iIcwID2Wa8#cTAr3hXB@QhP&xkS7<35fZNuQ8o95q3nqou=V zOmS>+oZ>jg=@_SboI2Fxl0s@y5wXCrImNle`H5c4h;}PfS}iUvJ}xOPeOzk%Z;U@B{tp#ctN8mftu|@(l2-3& z^^w+VoMDjGDdA)S8V~T1wk~ZkZ(XHVlU^%Eawp2y#u#EsZBBcY_RoaT!-sP^-qKN} zlTPPDI-7L4rb~zMUg@GSiYr}D>Ds3oqpRumOt(4R=k#XN+eB%~l*oWVCYWoWF%cj= zo@n__-8FS@XzoShE^)AAn#Fn_eV$8V$HZ=lbz~P~5}j$(1h4_ER*A0@KPG-5A1Y^w zb5fa+j|KfC3EdZoS2Rc@J|wYD;w6bo5?55fqp}=D;V98g!chr8ie%IoOS(g%E@_MWS<-kTg0qKYIwU6$;zzt5 zLHT4NqQD5+Cc8=)t!G)XkgQ5pBfN;XUb0U_&63+7n3dd>5*jcVz_H{YIfIZ&azk=c za!c}`$$yi7ml|M!O)25=)T&`;H6%YKKPCUfejtT4mhUO-Q@EsXC%-S%wOFTOaf#)i z!Ag#LGAhNeB*OXzm*ol%bv(#Z+@|=F;wzrSDee=UtnVp)r1+KMcZz@G=|vVmI(6{0 z0NlQbdq+wTnSp2t_@SK+cQo9PQi>Tj5nS`QN>dzgQKdNHGQcZ7#eY-$ozg9(x0D{_ zNJeE%a?~cKfX_^+r8K7WOliRZ9pqO=_YZ0kvnKul0arwru%W~OY93?0xD}>T4>iRMx3%QhC9I_*9Om+*0{W%x(7(z z`Bdp?($lABz_Wth@@Jo(bHWhZS*2O0Ii9__%J-P>bACJK zx0>Hte(3y|@?**G`~0r+r^n9NpcS z-O;q18Du6iGt`l=hAz|0&zXzNb>=?vNLM${nfGWC%lw(qpe4@a$R}$Jx?<3VF=l}` zE;NOSECi_If6wrN67~#V8UD_|QNT^i8#sbhNGOalUtBb(&_yvW>S9r}iUQ3g%Wal- z6a|t-Mn+VsWm!-`g}NwIAffmM8DW~+%9A@;>*Te2OHhrj^x?C#k~ z(hd+*jZ3`GZbS$*`!nK(*>h4H6AHD00&3?#!%6`t_ki!x4ZZT9chdVa>IdJTy zNrn(?4pRyYc1+c~#eZXXoPfU@g`9`I^o8jTIm;_*4;xyy*<&n?eWM%(3L^3wA1>{stZYVC8s@B71{KOFlb>yN5GKKs+Y zKfPi6aewldAl#pp{=Dzc*ZzDX*Z8^i=O@OV_7~CL_Wh0ZH`U*Ce>eTT_HVL(br zUtfK9#h{bElYM6})1hbQp6Q+$G_?0D_2iDe_FVK__1sV~%ln?AVQEhuQ1YaqART>T zt`~?Yo_gpAS%lKdZ7;9AWTe)Vsh72vLob(Joya+=bgwL`#87(GtEty-s3qz(Lm5!7 zRjs`u;O zZ+idQ`(5u3y%)Wg#E|>Y``G*3`@Z*M@BisB!Z~z(V11B%==;d}2z@MlY<(R2bnero zPhFp&Pr6U0Pqt5viu%<0wBUwYvYk<~}A=jve#vA%HEcJD*IaY_?Z8d z11~ty;rJlC!*MRBhEdw(oXfc{mySHj7g;X&{amiway?>DY55bQiP4tjrZEeW?0R=! z?xDO*AonKfuelS zSAp_mlt5x=4r(zh0qvAE6KT8ZDn_W3ZYtfgbaP=i-|t#jM?MV7r0^ILQP@=2T==>0 zD`mit2OiawsO>^o2Ub<3@c$OEDq@Ew-Xbm(5+fMeP07ZAic9idBD5c<_!6~*kDuOg@#9wX$=i&=K{9^j{+YUmsQ}KB4;pRg3M89UP5aK>a0=7iOMqM z)sZnk7!Uhe+JeY@nxNC@`Y2p}@1iTmXOKPm%i~4@EwT?26FLIctRGj&n6`e9tbFN!KXCTloazsc?S&{u;*gm$bxB2u|u&F^7O^+i#rteT^uLZ zi_@f(akaRiIJ|c$epURo_+9a*;xEXwku%0i64?=Zgaph~Ji>p7@9#@EmvAZJQNlN+ z^$_Tc2(qEdH}nWWmbgJMuEh5eKT7u$5Qb%pQZkk`n$AM zY3tIqrR_>Ply)gikmaap)GL){OY2J;N}J1YF2jQY1_nWfq(POzl);xFmLZp+m0>L7 zwu~Kll}1y>R>l!0XO?MSrjFc86G|gZnQWOH(qCo5d#CceQ)0nWl&37urOc}`|0?sk z%$qX5lzCU?W0~K}{88r5GJj+I62=>#i?^UDVOC`}Wwr(R_BkNNL!Hf>%iPL5l=**U zIhTc%MU+Js)M_oMEPYv8StfE&{gm=k%g<1L#`1ere*da}{Zm()x_YUreOuasMRo(6C?i0tDQCb5!lF`4llO)jI>H7c1i6KddW5W=# z!gej!T~C+54xig_d{^B~b$_8esy)M*SmgcMOA>d}=qbebYu%4^kB|9ZJ;-`6^)S}M zRF9?}TRq~CgF`)8s@ibi!TC|o3iI^qIoI=4&lo*wiouPrfTMD7V@uKU9G7$R-3A=)efkOLb+0PAJu(U z_e~LHj-gBkH8-R>M17Wv)url6bz^l%>u#%mAVW_qG@_owcGv1hq#GyvK5!>liO`DqTh>q1HN#Tgh_8-Wm{sA=*Aiypf zTV-r(Ykp968d~tF)5F#ayCdptXb;P^W_;wc=B4(J+Uwf;+7DQf;2B>FtA&%7kNG~wM?}vG^&>t=cfKyambG3V8KV$v9YJYvS)uydp zaLjgFy|)$q6K7ig+kU;YU-+Kk)ZSlhy=m+JudPpQ-L*BNq?xrKU+G#BUcOFko!h## z&8BT$a8hjBe6-EK?a$0t9K71LJx%~^ue`mg_G;T}`SS^^+U}Dg^mdFAYj%RnspvM_ zsrF|gW7}EMN9+Q@@CaY+G97OCx$Ro}Gw-nNySC?TFUZpRXVf1K_9H#TKDB+Ok9R-N zaInLv9b`KM0>JQWJRWe|R69P}(X^v)$EBUF?ZnzCQJCL(#wi-@!rMiW#{Xj5rKf~H zbkAHJ2JzGNa&7I_wVQ6YzTIVeJ7BVNdn*`@+#Yx}UH_!-U}kG-pSgV&y0Si7`y3ku zn-1+yL}~lpVKQm^6C~QYb?aVRw`<+Kbsw$!qCmlpQF0GPR#5t+!VpOk3!~@~m1U?9 z=>pLa2%$&U_WMKWiyucN$$Oyh(^l3^RG6{uwetp>=}{Dk!u;wj3!P1VpiDLJJ3)G)6RFG(9)HH2u@` z?`95^=}4XdOb|hbv?Of~Dcz9>N`!p;QHP^kyP2Vxv6-dWI|_?Y8H{4zW*Z7yn_Zd{ z%^AW-T;CiDcP^9@Ns2B-(r@pYPwkKXrUgL`>>yk4#4?e>0Fjlcg=dQ=q->F(C4V-N zSjZ$zEg~?A2$fqdv<3b_a9V$C{jK$1t>=`d(kl`xVZ&dx6njZc z4z?4XFlH)VvBboh5^KAq)fJXmt!`R0zMiekZ8*2#jbbC`4hW_NVUp{-{!ZAxv*Z7OY=+Vjz#U3-f5 z1XAcfZF}O4Pn;5uDyA;KQHY^xBtJTGtF&eORlid_J6OT2^&p;s^lmRPf~T~ zoXMPrxDjzFH5Wo?6TKC$!+Y`x`UwI>sZ@fBl|u~YeCOWJ_i=vA^Us1-gpgEP7qk}I zoXr-nsc6$6D3LZw+Y@bL+9t@Equq&iXWAojsinQ6Lr9014g6D8NNOOFDJ8!^m;yA4lGH2?P`V{V z>4MTFr7x7e0uw{&p3*-keW&!pC}nA4wnYfyj33ckTVoIea0>7jVdKA3cB1S;SxVUz zerd`^yG3VAnNYr<{FCyK^1`IJc|-YN$3?DGT&RetNT}$kz?^A8<&w&kr9+!bYDeW6 z-XSUz10I+g2bLJ@c+pY8u;J)BLd6crI8_I#E@Y}}$eke(hKz4v*w;FtIyKL9ol{*> zUE9jVM1BIz95uhF*%OjFnuwazgnm%#U~i0?qG4*3+Kok}wQtlOjUa213s8$7cYv?} zRR&BMxPPbqx4~#ZwFLdhrr{WB z<9jel##uttJIrKh2r;0fp`&4@L1(Q-ew`#94L`l;E*D z<_LGOU;LCh3k7N1X>jME+2b{M7gW&-RE7N{LM}YR-fW^3&ff)*}?!`r! zGl(jHr-1zhz-zkr-No!;b+LEJ1&lV}z+BS0q_-C-5to3Jv2f|qVx^x5A+UbH;xT$h znwj6oz%h+!7SbL>6&o0&6WAOsy}RsSgbxEEbg=O|v%bS6KDlCJX){ISiriFYX4k~E z*1^hXv|}&@%fz@KWp1vKvN8p$8Njew#57`Z*R8wm-Sq&^kGrPsI=JiXuHveVt7cb` ztJKw7S4SpZtJ&4~)Vt;c<)UjU*EA-nXnNN;*RCPVf)dNM5#&Lxt)NhW7zNS|*Tt?& zT$j17FgI0Kxo*Ug()AnHpIo0?wLYxfurm@E2nRNrzdD?+yx+y}BhX%||qnp$L_B`iqCO3Vkehc}n;J3ltRN4&Kx=pfy&)-bml1HC^j~m zagA$^8jjD#X#-Ry&N;3)?l^V`wBh8;Nz6&gNevdB?VS=#xmIj&F<`>pjNto3Tp5kV zNKbI+Vwo^<7I78>HNx4Ivxc+I7FPKN+y~Ax^J?Vybl{vg7tWQRMs&Elxk$Mv&BzV)Mw1io;WuAoFp>ldeaKCErb0`1y4Osp63N4xNNR? z?s)F48=lFsK&`fjaU*A{m}a(Xy0F``JF$z{Rk#E|U%(D0%_nord}Xdpugds}GIRJi+>zJj2YakBWCy|DLu`9 zXW6V|bCpdcn_4!FSa3CqB@pMb3yl%DYY-7?5vF^RLu?!ri!-d{;N@+Q<5`ZS9KD=k zIrVa4x%6_Go|PtY4v<680GAIOyhIy`wh|ppcxzPXjl>5FIf+AK5E}NklDL+*vAbE! z600O@qZ5Q-{6mfO>L7&5|TtDjU`Patt4&a3CXg;Hd_HIBv~z4C)ot-i(PdP zzM7v5@0wSVH>aw!TaD)SZ;jTEDlTPgR(sxK3xG{Jrf=O1hb zWn;RJ65jx$tVrp_{Gu6166R06HgfIdD)8os+2nw_Po%oqExKZT$ow~%momS|{3`P=nFE>E zGM{CBm-$2H-!lIljk?S+dR?QV2y7)sEzje9L z<%=$Nh9L*67sRY(uFF!FjV?P~_BP93PS(>b@A~qtFWnhUKpub8pi2Q0aSZMH} zL1>^~kS~MOpguvaL7@Tu6q>|NgWk3Q!K49c;51-eZ*(2&nsr<1_G*g)47ZRuz3UXg zz{qLe;}MaW2R)>E$n?3_}85+jcuoaWFZi8V_;{EW@I2&^n7dy3~8b1)l7Sa+iTTe zb|Arl)P{~wG>bG#0e-TTNH#qi#I@!-&Ci;DY94ByYTnzn1O%1>>D;LnnHHrf>x)5+ zDB-nSYkAQ!QKJmNpabW8ucg!L3KnaatKmp z60KD0Jxp;hkC`JJO=^v@;sQsCHiQzPXX?M{sgyhi(7Twi^s9kT{9+Q+o=H(%EQlvBWq4ueo*hO{N=&(fsp$?xq6lP4v z_;Vn|WqVMyeRG1nsgAWyfle_7=sFELjn+oviavRr*E)v^6r4?S;A_W}1hW9Av_ey= zRH2o^hTnEg#7e5<)rF`>)m_yJ%2s)!Rh$^LNhmasc`XX_wZwXjS8=UvkIGK*H^RM)hqAovo}8^T#FENz}a&4Pk##>EWX|OHu7z5 zF7vkYEm_svvhNnYTSHY0QLllEOg4(i`s_Q@)LY*lp=0xX?fVvTE8lx>8lw|5L&iuj z;R|N)gw?(uGxNKQogYUGb^M4gqRPNF?(C-ve7a_ZnIK)n(!xr0KZ&0&80KPV>t{ot z`MLIUi`f|_M4m!Vv8Te*74rf^zqr=B-n+p&^1Si9_5A5MwA2XBJp93OL80J&|QFHTHJysrMoNn>~nY&zH&D+s|#0VRdj@U+T7Fjp2|JF-?Q+ZmG^AC7wNrh?{#=@!h74@`w&XLdq3U>`jhE6 z4R>nq)IT@LA$B8oF7Eat?vLg4_(%HqNB;(3(iS*(8+?>~PFeqLUFUS5A* z-hTr5`B?va;irHrX+P`dG5y=0Z~uXh|M}$^pzy9anpCA7Z^dAyT literal 0 HcmV?d00001 diff --git a/.vs/Dotnet.Samples.AspNetCore.WebApi/v18/.futdcache.v2 b/.vs/Dotnet.Samples.AspNetCore.WebApi/v18/.futdcache.v2 new file mode 100644 index 0000000000000000000000000000000000000000..0fb25e97f3ef2674dd6c011015043b7579e76b19 GIT binary patch literal 390 zcmZQ#U|^^XvWh9rFD*(=jVVek$S;m@$uG%EEzt{3%q_@CE!J}^F7Qh&an3JF)eBEe zaxBPxBSS5e^54G7|kE(stAn)fWs50D9$t2LJ#7 literal 0 HcmV?d00001 diff --git a/.vs/Dotnet.Samples.AspNetCore.WebApi/v18/.suo b/.vs/Dotnet.Samples.AspNetCore.WebApi/v18/.suo new file mode 100644 index 0000000000000000000000000000000000000000..e48b7b40fa6f8f0f475cf772c30ad9629a69709c GIT binary patch literal 24064 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 H>4W|UuzwrV literal 0 HcmV?d00001 diff --git a/.vs/ProjectEvaluation/dotnet.samples.aspnetcore.webapi.projects.v10.bin b/.vs/ProjectEvaluation/dotnet.samples.aspnetcore.webapi.projects.v10.bin new file mode 100644 index 0000000000000000000000000000000000000000..0db95dfa220878b1b21d34c36b17c031334928e4 GIT binary patch literal 63590 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 literal 0 HcmV?d00001 diff --git a/.vs/ProjectEvaluation/dotnet.samples.aspnetcore.webapi.strings.v10.bin b/.vs/ProjectEvaluation/dotnet.samples.aspnetcore.webapi.strings.v10.bin new file mode 100644 index 0000000000000000000000000000000000000000..1f41238598234bea090fe209516b20c5b16ca0c9 GIT binary patch literal 202397 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 literal 0 HcmV?d00001 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..15b043a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,257 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Stadium Release Names + +This project uses famous football stadiums (A-Z) that hosted FIFA World Cup matches (with notable fallbacks where necessary): + +| Letter | Stadium Name | Location | Tag Name | +| ------ | ----------- | -------- | -------- | +| A | Azteca | Mexico (1970, 1986, 2026) | `azteca` | +| B | Bernabéu | Spain (1982) | `bernabeu` | +| C | Centenario | Uruguay (1930) | `centenario` | +| D | Düsseldorf (Merkur Spiel-Arena) | Germany (2006) | `dusseldorf` | +| E | Ekaterinburg Arena | Russia (2018) | `ekaterinburg` | +| F | Frankfurt Waldstadion | Germany (1974, 2006) | `frankfurt` | +| G | Gelsenkirchen | Germany (2006) | `gelsenkirchen` | +| H | Hard Rock Stadium | USA (2026) | `hardrock` | +| I | Ibn Batouta Stadium | Morocco (2030) | `ibnbatouta` | +| J | Johannesburg Soccer City | South Africa (2010) | `johannesburg` | +| K | Kazan Arena | Russia (2018) | `kazan` | +| L | Lusail | Qatar (2022) | `lusail` | +| M | Maracanã | Brazil (1950, 2014) | `maracana` | +| N | Nantes Beaujoire | France (1998) | `nantes` | +| O | Olympiastadion Berlin | Germany (1974, 2006) | `olympiastadion` | +| P | Parc des Princes | France (1938, 1998) | `parcdesprinces` | +| Q | Qatar 974 | Qatar (2022) | `qatar974` | +| R | Rose Bowl | USA (1994) | `rosebowl` | +| S | San Siro | Italy (1934, 1990) | `sansiro` | +| T | Toronto BMO Field | Canada (2026) | `toronto` | +| U | Ullevi | Sweden (1958) | `ullevi` | +| V | Volgograd Arena | Russia (2018) | `volgograd` | +| W | Wembley | England (1966) | `wembley` | +| X | Xiamen Egret Stadium | (famous fallback) | `xiamen` | +| Y | Yokohama International Stadium | Japan (2002) | `yokohama` | +| Z | Zentralstadion Leipzig | Germany (1974, 2006) | `zentralstadion` | + +--- + +## [Unreleased] + +### Added + +- `CLAUDE.md` as the single source of truth for project guidance and AI-agent instructions (issue #505); consolidates content previously split between `CLAUDE.md` and `.github/copilot-instructions.md`. +- `Invariants` section to `CLAUDE.md` documenting port, API contract, commit format, and CHANGELOG update requirements. +- `Architecture Decision Records` section to `CLAUDE.md` referencing `docs/adr/` with guidance on when to load and when to create new ADRs. +- Pre-commit step 6 to `CLAUDE.md`: update CLAUDE.md and create/amend the relevant ADR when a commit introduces or changes an architectural decision. +- `docs/adr/` path instruction to `.coderabbit.yaml` covering `**/*.csproj` (DATABASE_PROVIDER runtime selection) and `.github/workflows/**` (explicit provider requirement in CI jobs that run migrations or integration tests). +- `DATABASE_PROVIDER` environment variable (`sqlite` default, `postgres` opt-in) to select the database engine at startup (issue #249). +- PostgreSQL 17 support via `Npgsql.EntityFrameworkCore.PostgreSQL` 10.0.1; migrations in `Migrations/Npgsql/` use proper PostgreSQL column types (`uuid`, `boolean`, `timestamp with time zone`). +- `ProviderSpecificMigrationsAssembly` that filters the EF Core migration set to the active provider's namespace, ensuring `MigrateAsync()` applies the correct migrations for both SQLite and PostgreSQL. +- `postgres` Docker Compose profile and service (`postgres:17-alpine`), started only when `DATABASE_PROVIDER=postgres` is set; the API service uses `depends_on` with `required: false` so SQLite mode incurs no dependency on the postgres service. +- `.env.example` documenting `DATABASE_PROVIDER`, `DATABASE_URL`, and `POSTGRES_PASSWORD`. +- `.env` added to `.gitignore`. +- ADR-0014 (`docs/adr/0014-configurable-database-provider.md`) documenting the decision; supersedes ADR-0003. +- ADR-0015: Use Full-Replace PUT as the Partial Update Strategy +- ADR-0016: Adopt AI-Assisted Development Workflow +- ADR-0017: Adopt Spec-Driven Development (SDD) + +### Changed + +- Improved `CLAUDE.md` project guidance via the [CLAUDE.md Management plugin](https://claude.com/plugins/claude-md-management): added `Migrations/`, `Utilities/` (src) and `Integration/` (test) to the structure tree; documented `DATABASE_URL` Npgsql format, `STORAGE_PATH` SQLite path override, and exact `dotnet ef migrations add` commands for both providers; collapsed issue template bullets for conciseness; clarified `MigrateAsync()` runs automatically at startup. +- `adr/` directory moved to `docs/adr/`; all references in `README.md`, `CONTRIBUTING.md`, and `.coderabbit.yaml` updated to the new path (issue #505). +- `.coderabbit.yaml` controller path instruction updated: status code list corrected from `(200, 201, 400, 404, 409, 500)` to `(200, 201, 404, 409, 500)` with explicit note that FluentValidation failures must return 422 via `TypedResults.Problem(new HttpValidationProblemDetails(...))` (issue #505). +- `.coderabbit.yaml` knowledge base guidelines updated from `.github/copilot-instructions.md` to `CLAUDE.md` and `docs/adr/README.md` (issue #505). +- `.coderabbit.yaml` caching description in Tech Stack corrected to reflect both 10-minute sliding and 1-hour absolute expiration (issue #505). +- `AddDbContextPoolWithSqlite` renamed to `AddDbContextPool` in `ServiceCollectionExtensions`; now reads `DATABASE_PROVIDER` and wires either `UseSqlite` or `UseNpgsql` accordingly. +- `compose.yaml`: `api` service receives `DATABASE_PROVIDER` and `DATABASE_URL` environment variables; `postgres-data` named volume added. +- `scripts/entrypoint.sh`: SQLite file-presence check is skipped when `DATABASE_PROVIDER=postgres`. +- ADR-0003 status updated to "Superseded by ADR-0014". +- `README.md`: added Database section documenting SQLite and PostgreSQL modes. + +### Fixed + +- Populate `BuildTargetModel` in Npgsql seed migration designer files so Npgsql's SQL generator can resolve column types when applying `InsertData` operations. +- Suppress `PendingModelChangesWarning` for the postgres provider path — hand-crafted designer files cannot replicate Npgsql-injected runtime annotations (`Relational:MaxIdentifierLength`, `UseIdentityByDefaultColumn`), causing a false-positive that aborted `MigrateAsync()` at startup. +- Normalize `DATABASE_PROVIDER` to lowercase in `entrypoint.sh` via `tr` so `POSTGRES`, `Postgres`, etc. are handled consistently with `AddDbContextPool`. +- Trim and normalize `DATABASE_PROVIDER` in `AddDbContextPool` before the provider switch; add explicit `sqlite`/empty case; throw `InvalidOperationException` for unrecognized values so typos no longer silently fall through to SQLite. +- Move `Npgsql.EntityFrameworkCore.PostgreSQL` package from the "Development dependencies" `ItemGroup` to "Runtime dependencies". + +### Removed + +--- + +## [2.1.2 - Frankfurt] - 2026-04-26 + +### Changed + +- Field validation failures now return `422 Unprocessable Entity` (RFC 4918) instead of `400 Bad Request`; `400 Bad Request` is now reserved for malformed requests (unparseable JSON, route/body mismatch); unsupported media types return `415 Unsupported Media Type` (RFC 9110 §15.5.16) via the `[Consumes]` attribute. Error responses follow the Problem Details format (RFC 9457). + +### Removed + +- Remove `## How to Release` section from `CHANGELOG.md`; capitalize release codenames in `##` version headers and reference links (#468) + +--- + +## [2.1.1 - Ekaterinburg] - 2026-04-12 + +### Added + +- Extract `test` job from `release` in CD pipeline so tests run in isolation + before any publish step; add `linux/arm64` to build platforms; add + `id-token: write` and `attestations: write` permissions to `release`; set + `provenance: mode=max` and attest the image digest with + `actions/attest-build-provenance@v4.1.0` (`push-to-registry: true`); add + `--no-merges` to the changelog `git log` command; add empty changelog guard; + normalize first-release message to `"No changes (first release)"` (#465) + +- Add `adr/0013-testing-strategy.md` documenting the decision to implement the full test pyramid as a deliberate educational choice (#421) +- Add `test/.../Integration/PlayerWebApplicationTests.cs` with 14 HTTP-layer integration tests covering all player endpoints and `/health` via `WebApplicationFactory` backed by in-memory SQLite; includes `Utilities/TestAuthHandler.cs` to bypass `[Authorize]` on `GET /players/{id:Guid}`; expose `Program` to the test project via `public partial class Program {}` in `Program.cs`; add `Microsoft.AspNetCore.Mvc.Testing` to the test project (#421) +- Add `test/.../Integration/PlayerRepositoryTests.cs` with 9 integration tests covering `Repository` (`GetAllAsync`, `FindByIdAsync`, `RemoveAsync`) and `PlayerRepository` (`FindBySquadNumberAsync`, `SquadNumberExistsAsync`); all tests use `DatabaseFakes.MigrateAsync()` on in-memory SQLite and are tagged `[Trait("Category", "Integration")]` (#461) +- Add `ValidateAsync_SquadNumberNegative_ReturnsValidationError` test to exercise the `GreaterThan(0)` rule with a negative value, which passes `NotEmpty()` but fails the greater-than rule (#427) +- Add `ValidateAsync_FirstNameEmptyInUpdateRuleSet_ReturnsValidationError` test to verify the `"Update"` rule set enforces structural field validation (#427) +- Add `adr/` directory with 12 Architecture Decision Records documenting architectural choices, technology decisions, and design trade-offs (#372) +- Add ADR index and template at `adr/README.md` (#372) +- Add Architecture Decisions section to `README.md` referencing the ADR index (#372) +- Add ADR guidance section to `CONTRIBUTING.md` (#372) +- Add ADR context loading instructions to `.github/copilot-instructions.md` (#372) + +### Changed + +- Call `.DisableRateLimiting()` on `MapHealthChecks("/health")` to enforce the exemption from the global rate limiter at the endpoint level, ensuring health check probes are never throttled (#451) +- Move `UseCors()` before `MapControllers()` in `Program.cs` to follow the standard ASP.NET Core middleware pipeline order; add an `Infrastructure` service registration section separating cross-cutting concerns (health checks, CORS, rate limiting, Swagger) from the `Controllers` section; add descriptive phrases to top-level section banners; add inline comments explaining the purpose and ordering rationale of each middleware; document the dev-only CORS policy intent in both `Program.cs` and `ServiceCollectionExtensions.AddCorsDefaultPolicy` (#451) +- 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 + `docker save | wc -c`); replace `apt-get` with `apk` and + `useradd`/`groupadd` with `adduser`/`addgroup` accordingly (#456) +- Refactor `scripts/entrypoint.sh`: add `log()` helper with timestamp prefix, + replace raw `echo` calls, and print API base URL on startup (#456) +- Rename `ValidateAsync_SquadNumber_BelongsToPlayerBeingUpdated_ReturnsNoErrors` to `ValidateAsync_SquadNumberBelongsToPlayerBeingUpdated_ReturnsNoErrors` to align with the 3-segment naming convention for service/validator tests (#427) +- Make CSharpier step in `/pre-commit` conditional (skip with a note if not installed), consistent with the Docker and CodeRabbit steps (#427) +- Add "Verify tag commit is reachable from master" step to CD workflow using `git merge-base --is-ancestor` before any build or publish steps (#439) +- Rename five controller test methods to normalize data-state vocabulary: `NonExisting` → `Nonexistent` for the POST 201 scenario, `NonExisting` → `Unknown` for the four 404-by-lookup scenarios (#452) +- Add XML doc `` block to `PlayerFakes` documenting the three-term data-state vocabulary (`existing`, `nonexistent`, `unknown`) (#452) + +### Fixed + +- `GET /players` now returns `200 OK` with an empty list `[]` when no players exist, instead of `404 Not Found` (#425) +- AutoMapper `Player → PlayerResponseModel` profile now explicitly ignores the `Id` source member via `ForSourceMember`, making the exclusion intentional rather than implicit (#425) + +### Removed + +--- + +## [2.1.0 - Dusseldorf] - 2026-03-29 + +### Added + +- Add SonarCloud configuration via `.sonarcloud.properties` with explicit CPD exclusions for migrations, generated code, test files, and structurally repetitive production files (#426, #435) +- Add bug report issue template (`.github/ISSUE_TEMPLATE/bug_report.md`) (#426) + +### Changed + +- Normalize player dataset: add Lo Celso (squad 27), correct Fernández/Mac Allister/Messi team data, replace random UUIDs with deterministic UUID v5 values (#435) +- Align CRUD test fixtures: Lo Celso (squad 27) for Create and Delete, Messi (squad 10) for Retrieve, Damián Martínez (squad 23) for Update (#435) +- Bump `codecov/codecov-action` from 5.5.2 to 5.5.3 (#423) + +### Fixed + +- Scope `BeUniqueSquadNumber` validator to `"Create"` rule set to prevent false rejection of valid `PUT` requests (#424) + +--- + +## [2.0.0 - Centenario] - 2026-03-18 + +### Changed + +- Refactor error responses to use RFC 7807 Problem Details (#418) +- Add `/squadNumber/` path segment to PUT and DELETE routes — **breaking change** (#418) +- Extract `NotFoundTitle` constant and remove redundant null-conditional operator (#418) +- Rename test methods to follow Microsoft .NET naming standard (#396) +- Upgrade AutoMapper from 14.x to 16.1.1 (#414) — see Security section +- Bump FluentAssertions from 8.8.0 to 8.9.0 (#417) +- Bump coverlet.collector from 8.0.0 to 8.0.1 (#419) +- Bump softprops/action-gh-release from 2.6.0 to 2.6.1 (#416) + +### Fixed + +- Add squad number mismatch guard in `PutAsync` and update README (#418) +- Strengthen test assertions for Problem Details responses (#418) +- Fix broken 201 assertion in controller test for `Post_Players_NonExisting` (#396) +- Add missing edge case tests for `UpdateAsync`, `DeleteAsync`, and + `DateOfBirth` boundary validation (#396) + +### Security + +- Sanitize player data before logging to prevent log forging +- Upgrade AutoMapper from 14.x to 16.1.1 to resolve high-severity security vulnerability GHSA-rvv3-g6hj-g44x (#414) + +--- + +## [1.1.0 - Bernabeu] - 2026-02-09 + +### Changed + +- Upgrade to .NET 10 LTS from .NET 8 (#368) +- Update Microsoft.AspNetCore.OpenApi to 10.0.0 +- Update Microsoft.EntityFrameworkCore.Sqlite to 10.0.0 +- Update Microsoft.EntityFrameworkCore.Design to 10.0.0 +- Update Microsoft.VisualStudio.Web.CodeGeneration.Design to 10.0.0 +- Update Docker images to .NET 10 SDK and runtime (now based on Ubuntu 24.04 LTS instead of Debian 12) +- Update Dockerfile user creation commands for Ubuntu compatibility (`groupadd`/`useradd` instead of `adduser`) +- Update CI/CD pipelines to use .NET 10 SDK +- Token efficiency strategy for Copilot/AI agents with optimized instruction loading and improved token counting script (#364) +- Bump Swashbuckle.AspNetCore from 10.1.0 to 10.1.2 +- Bump docker/login-action from 3.6.0 to 3.7.0 +- Bump softprops/action-gh-release from 2.2.0 to 2.5.0 +- Bump actions/checkout from 6.0.1 to 6.0.2 + +--- + +## [1.0.0 - Azteca] - 2026-01-22 + +Initial release. See [README.md](README.md) for complete feature list and documentation. + +--- + + + +--- + +[unreleased]: https://github.com/nanotaboada/Dotnet.Samples.AspNetCore.WebApi/compare/v2.1.2-frankfurt...HEAD +[2.1.2 - Frankfurt]: https://github.com/nanotaboada/Dotnet.Samples.AspNetCore.WebApi/compare/v2.1.1-ekaterinburg...v2.1.2-frankfurt +[2.1.1 - Ekaterinburg]: https://github.com/nanotaboada/Dotnet.Samples.AspNetCore.WebApi/compare/v2.1.0-dusseldorf...v2.1.1-ekaterinburg +[2.1.0 - Dusseldorf]: https://github.com/nanotaboada/Dotnet.Samples.AspNetCore.WebApi/compare/v2.0.0-centenario...v2.1.0-dusseldorf +[2.0.0 - Centenario]: https://github.com/nanotaboada/Dotnet.Samples.AspNetCore.WebApi/compare/v1.1.0-bernabeu...v2.0.0-centenario +[1.1.0 - Bernabeu]: https://github.com/nanotaboada/Dotnet.Samples.AspNetCore.WebApi/compare/v1.0.0-azteca...v1.1.0-bernabeu +[1.0.0 - Azteca]: https://github.com/nanotaboada/Dotnet.Samples.AspNetCore.WebApi/releases/tag/v1.0.0-azteca diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..7a46969 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,250 @@ +# 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 +``` + +## Claude Code + +- Run `/pre-commit` to execute the full pre-commit checklist for this project. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..3cd0444 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +. Translations are available at +. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..18feb63 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,64 @@ +# Contributing + +Thank you for improving this project! We value small, precise changes that solve real problems. +We value **incremental, detail‑first contributions** over big rewrites or abstractions. + +## 1. Philosophy + +> "Nobody should start to undertake a large project. You start with a small _trivial_ project, and you should never expect it to get large. If you do, you'll just overdesign and generally think it is more important than it likely is at that stage. Or worse, you might be scared away by the sheer size of the work you envision. So start small, and think about the details. Don't think about some big picture and fancy design. If it doesn't solve some fairly immediate need, it's almost certainly over-designed. And don't expect people to jump in and help you. That's not how these things work. You need to get something half-way _useful_ first, and then others will say "hey, that _almost_ works for me", and they'll get involved in the project." — [Linus Torvalds](https://web.archive.org/web/20050404020308/http://www.linuxtimes.net/modules.php?name=News&file=article&sid=145) + +## 2. Code & Commit Conventions + +- **Conventional Commits** + Follow : + - `feat: …` for new features + - `fix: …` for bug fixes + - `chore: …` for maintenance + +- **Logical Commits** + Group changes by purpose. It’s okay to have multiple commits in a PR, but if they’re mere checkpoints, squash them into a single logical commit. + +- **Lint & Tests** + Run existing linters/formatters and ensure all tests pass. + +## 3. Pull Request Workflow + +- **One logical change per PR.** +- **Rebase or squash** before opening to keep history concise. +- **Title & Description** + - Title uses Conventional Commits style. + - Description explains _what_ and _why_—keep context minimal. + +## 4. Issue Reporting + +- Search existing issues first. +- Provide a minimal reproducible example and clear steps. + +## 5. Automation & Checks + +We enforce quality via CI on every push and PR: + +- **Commitlint** for commit‑message style +- **Linters/Formatters** +- **Unit tests** + +Failures must be fixed before review. + +## 6. Architecture Decision Records + +When proposing changes that affect: + +- Project structure or overall architecture +- Technology choices or dependencies +- API contracts or data model design +- Non-functional requirements (performance, security, scalability) +- Development workflows or processes + +Please create an ADR in the `docs/adr/` directory following the existing template. See [`docs/adr/README.md`](docs/adr/README.md) for the template and index. + +## 7. Code of Conduct & Support + +- Please see `CODE_OF_CONDUCT.md` for behavioral expectations and reporting. +- For quick questions or discussions, open an issue with the `discussion` label or mention a maintainer. + +Thanks again for helping keep this project small, simple, and impactful! diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4eeeb5b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,70 @@ +# ------------------------------------------------------------------------------ +# Stage 1: Builder +# This stage builds the application and its dependencies. +# ------------------------------------------------------------------------------ +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS builder + +WORKDIR /src + +# Restore dependencies +COPY src/Dotnet.Samples.AspNetCore.WebApi/*.csproj ./Dotnet.Samples.AspNetCore.WebApi/ +RUN dotnet restore ./Dotnet.Samples.AspNetCore.WebApi + +COPY src/Dotnet.Samples.AspNetCore.WebApi/ ./Dotnet.Samples.AspNetCore.WebApi/ + +WORKDIR /src/Dotnet.Samples.AspNetCore.WebApi + +# Build solution and publish release +RUN dotnet publish -c Release -o /app/publish + +# ------------------------------------------------------------------------------ +# Stage 2: Runtime +# This stage creates the final, minimal image to run the application. +# ------------------------------------------------------------------------------ +FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS runtime + +WORKDIR /app + +# Install curl for health check +RUN apk add --no-cache curl + +# Metadata labels for the image. These are useful for registries and inspection. +LABEL org.opencontainers.image.title="🧪 Web API made with .NET 10 (LTS) and ASP.NET Core" +LABEL org.opencontainers.image.description="Proof of Concept for a Web API made with .NET 10 (LTS) and ASP.NET Core" +LABEL org.opencontainers.image.licenses="MIT" +LABEL org.opencontainers.image.source="https://github.com/nanotaboada/Dotnet.Samples.AspNetCore.WebApi" + +# Set environment variables +ENV ASPNETCORE_URLS=http://+:9000 +ENV ASPNETCORE_ENVIRONMENT=Production +# Alpine images default to invariant globalization mode. Explicitly opt in since +# this API uses ISO-8601 dates and ASCII data — ICU is not required. +ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=true + +# Copy published app from builder +COPY --from=builder /app/publish/ . + +# Copy metadata docs for container registries (e.g.: GitHub Container Registry) +COPY --chmod=444 README.md ./ + +# https://rules.sonarsource.com/docker/RSPEC-6504/ + +# Copy entrypoint and healthcheck scripts +COPY --chmod=555 scripts/entrypoint.sh ./entrypoint.sh +COPY --chmod=555 scripts/healthcheck.sh ./healthcheck.sh + +# Add non-root user and make volume mount point writable +RUN addgroup -S aspnetcore && \ + adduser -S -G aspnetcore aspnetcore && \ + mkdir -p /storage && \ + chown aspnetcore:aspnetcore /storage + +USER aspnetcore + +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ + CMD ["./healthcheck.sh"] + +EXPOSE 9000 + +ENTRYPOINT ["./entrypoint.sh"] +CMD ["dotnet", "Dotnet.Samples.AspNetCore.WebApi.dll"] diff --git a/Dotnet.Samples.AspNetCore.WebApi.sln b/Dotnet.Samples.AspNetCore.WebApi.sln new file mode 100644 index 0000000..415fa74 --- /dev/null +++ b/Dotnet.Samples.AspNetCore.WebApi.sln @@ -0,0 +1,36 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{6E3A447C-C7E3-4E1C-B70D-CE1C4E6B13E1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dotnet.Samples.AspNetCore.WebApi", "src\Dotnet.Samples.AspNetCore.WebApi\Dotnet.Samples.AspNetCore.WebApi.csproj", "{1ED8A3DB-5092-4D79-ABA2-403797A6898C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{0A2F456B-EF02-40FA-A529-E4DA81E545AB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dotnet.Samples.AspNetCore.WebApi.Tests", "test\Dotnet.Samples.AspNetCore.WebApi.Tests\Dotnet.Samples.AspNetCore.WebApi.Tests.csproj", "{F45E3AF3-F6A2-4494-AEF2-2A228D5E8F1E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1ED8A3DB-5092-4D79-ABA2-403797A6898C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1ED8A3DB-5092-4D79-ABA2-403797A6898C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1ED8A3DB-5092-4D79-ABA2-403797A6898C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1ED8A3DB-5092-4D79-ABA2-403797A6898C}.Release|Any CPU.Build.0 = Release|Any CPU + {F45E3AF3-F6A2-4494-AEF2-2A228D5E8F1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F45E3AF3-F6A2-4494-AEF2-2A228D5E8F1E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F45E3AF3-F6A2-4494-AEF2-2A228D5E8F1E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F45E3AF3-F6A2-4494-AEF2-2A228D5E8F1E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {1ED8A3DB-5092-4D79-ABA2-403797A6898C} = {6E3A447C-C7E3-4E1C-B70D-CE1C4E6B13E1} + {F45E3AF3-F6A2-4494-AEF2-2A228D5E8F1E} = {0A2F456B-EF02-40FA-A529-E4DA81E545AB} + EndGlobalSection +EndGlobal diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..116476c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Nano Taboada + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..fde2fda --- /dev/null +++ b/README.md @@ -0,0 +1,355 @@ +# 🧪 RESTful API with .NET and ASP.NET Core + +[![.NET CI](https://github.com/nanotaboada/Dotnet.Samples.AspNetCore.WebApi/actions/workflows/dotnet-ci.yml/badge.svg)](https://github.com/nanotaboada/Dotnet.Samples.AspNetCore.WebApi/actions/workflows/dotnet-ci.yml) +[![.NET CD](https://github.com/nanotaboada/Dotnet.Samples.AspNetCore.WebApi/actions/workflows/dotnet-cd.yml/badge.svg)](https://github.com/nanotaboada/Dotnet.Samples.AspNetCore.WebApi/actions/workflows/dotnet-cd.yml) +[![CodeQL](https://github.com/nanotaboada/Dotnet.Samples.AspNetCore.WebApi/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/nanotaboada/Dotnet.Samples.AspNetCore.WebApi/actions/workflows/github-code-scanning/codeql) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=nanotaboada_Dotnet.Samples.AspNetCore.WebApi&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=nanotaboada_Dotnet.Samples.AspNetCore.WebApi) +[![codecov](https://codecov.io/gh/nanotaboada/Dotnet.Samples.AspNetCore.WebApi/branch/master/graph/badge.svg?token=hgJc1rStJ9)](https://codecov.io/gh/nanotaboada/Dotnet.Samples.AspNetCore.WebApi) +[![CodeFactor](https://www.codefactor.io/repository/github/nanotaboada/Dotnet.Samples.AspNetCore.WebApi/badge)](https://www.codefactor.io/repository/github/nanotaboada/Dotnet.Samples.AspNetCore.WebApi) +[![License: MIT](https://img.shields.io/badge/License-MIT-3DA639.svg)](https://opensource.org/licenses/MIT) +![Dependabot](https://img.shields.io/badge/Dependabot-contributing-025E8C?logo=dependabot&logoColor=white&labelColor=181818) +![Copilot](https://img.shields.io/badge/Copilot-contributing-8662C5?logo=githubcopilot&logoColor=white&labelColor=181818) +![Claude](https://img.shields.io/badge/Claude-contributing-D97757?logo=claude&logoColor=white&labelColor=181818) +![CodeRabbit](https://img.shields.io/badge/CodeRabbit-reviewing-FF570A?logo=coderabbit&logoColor=white&labelColor=181818) + +Proof of Concept for a RESTful API built with .NET 10 (LTS) and ASP.NET Core. Manage football player data with SQLite, Entity Framework Core, Swagger documentation, and in-memory caching. + +## Features + +- 🏗️ **Clean layered architecture** - Repository pattern, dependency injection, and async operations throughout +- 📚 **Interactive API exploration** - Swagger UI documentation with health monitoring endpoints +- ⚡ **Performance optimizations** - In-memory caching, rate limiting, and efficient database queries +- ✅ **Input Validation** - FluentValidation rule sets with CRUD-scoped constraints and RFC 7807 error responses +- 🐳 **Full containerization** - Multi-stage Docker builds with Docker Compose orchestration +- 🔄 **Complete CI/CD pipeline** - Automated testing, code quality checks, Docker publishing, and GitHub releases + +## Tech Stack + +| Category | Technology | +| -------- | ---------- | +| **Framework** | [.NET 10](https://github.com/dotnet/core) (LTS) | +| **Web Framework** | [ASP.NET Core 10.0](https://github.com/dotnet/aspnetcore) | +| **API Documentation** | [Swashbuckle](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) (OpenAPI 3.0) | +| **Validation** | [FluentValidation 12](https://github.com/FluentValidation/FluentValidation) | +| **Mapping** | [AutoMapper 14](https://github.com/AutoMapper/AutoMapper) | +| **Database** | [SQLite 3](https://github.com/sqlite/sqlite) (default) · [PostgreSQL 17](https://github.com/postgres/postgres) (opt-in) | +| **ORM** | [Entity Framework Core 10.0](https://github.com/dotnet/efcore) | +| **Logging** | [Serilog 9](https://github.com/serilog/serilog) | +| **Testing** | [xUnit](https://github.com/xunit/xunit), [Moq](https://github.com/devlooped/moq), [FluentAssertions](https://github.com/fluentassertions/fluentassertions) | +| **Containerization** | [Docker](https://github.com/docker) & [Docker Compose](https://github.com/docker/compose) | + +## Architecture + +Layered architecture with dependency injection via constructors and interface-based contracts. + +```mermaid + +%%{init: { + "theme": "default", + "themeVariables": { + "fontFamily": "Fira Code, Consolas, monospace", + "textColor": "#555", + "lineColor": "#555", + "clusterBkg": "#f5f5f5", + "clusterBorder": "#ddd" + } +}}%% + +graph RL + + Tests[Tests] + + subgraph Layer1[" "] + Program[Program] + Serilog[Serilog] + Swashbuckle[Swashbuckle] + end + + subgraph Layer2[" "] + Controllers[Controllers] + Validators[Validators] + FluentValidation[FluentValidation] + AspNetCore[ASP.NET Core] + end + + subgraph Layer3[" "] + Services[Services] + Mappings[Mappings] + AutoMapper[AutoMapper] + MemoryCache[MemoryCache] + end + + subgraph Layer4[" "] + Repositories[Repositories] + Data[Data] + EFCore[EF Core] + end + + Models[Models] + + %% Strong dependencies + + %% Layer 1 + Controllers --> Program + Serilog --> Program + Swashbuckle --> Program + + %% Layer 2 + Services --> Controllers + Validators --> Controllers + FluentValidation --> Validators + AspNetCore --> Controllers + + %% Layer 3 + Repositories --> Services + MemoryCache --> Services + Mappings --> Services + AutoMapper --> Mappings + Models --> Mappings + + %% Layer 4 + Models --> Repositories + Models --> Data + Data --> Repositories + EFCore --> Data + EFCore -.-> Repositories + + %% Soft dependencies + + Services -.-> Tests + Controllers -.-> Tests + + %% Node styling with stroke-width + classDef core fill:#b3d9ff,stroke:#6db1ff,stroke-width:2px,color:#555,font-family:monospace; + classDef deps fill:#ffcccc,stroke:#ff8f8f,stroke-width:2px,color:#555,font-family:monospace; + classDef test fill:#ccffcc,stroke:#53c45e,stroke-width:2px,color:#555,font-family:monospace; + classDef feat fill:#ffffcc,stroke:#fdce15,stroke-width:2px,color:#555,font-family:monospace; + + class Data,Models,Repositories,Services,Controllers,Program,Validators,Mappings core; + class AutoMapper,FluentValidation,Serilog,Swashbuckle deps; + class Tests test; + class AspNetCore,EFCore,MemoryCache feat; +``` + +> *Arrows follow the injection direction (A → B means A is injected into B). Solid = runtime dependency, dotted = structural. Blue = core domain, red = third-party, green = tests. Controllers call Services; Services call Repositories — bypassing layers is not permitted.* +> +> *Significant design decisions are documented as ADRs in [`docs/adr/`](docs/adr/README.md).* + +## API Reference + +Interactive API documentation is available via Swagger UI at `https://localhost:9000/swagger/index.html` when the server is running. + +> 💡 Swagger documentation is only available in development mode for security reasons. + +| Method | Endpoint | Description | Status | +| ------ | -------- | ----------- | ------ | +| `GET` | `/players` | List all players | `200 OK` | +| `GET` | `/players/{id:Guid}` | Get player by ID *(requires authentication)* | `200 OK` | +| `GET` | `/players/squadNumber/{squadNumber:int}` | Get player by squad number | `200 OK` | +| `POST` | `/players` | Create new player | `201 Created` | +| `PUT` | `/players/squadNumber/{squadNumber:int}` | Update player by squad number | `204 No Content` | +| `DELETE` | `/players/squadNumber/{squadNumber:int}` | Remove player by squad number | `204 No Content` | +| `GET` | `/health` | Health check | `200 OK` | + +Error codes: `400 Bad Request` (validation failed) · `404 Not Found` (player not found) · `409 Conflict` (duplicate squad number on `POST`) + +For complete endpoint documentation with request/response schemas, explore the [interactive Swagger UI](https://localhost:9000/swagger/index.html). + +## Database + +The database engine is selected via the `DATABASE_PROVIDER` environment variable. + +| Value | Engine | Requires | +| ----- | ------ | -------- | +| `sqlite` (default) | SQLite — file-based, zero infrastructure | Nothing — clone and run | +| `postgres` | PostgreSQL 17 — server-based, production-parity | Docker | + +### SQLite mode (default) + +```bash +# Local run — no setup required +dotnet run --project src/Dotnet.Samples.AspNetCore.WebApi + +# Docker Compose +docker compose up +``` + +### PostgreSQL mode (opt-in) + +```bash +# Docker Compose with the postgres profile +DATABASE_PROVIDER=postgres docker compose --profile postgres up +``` + +When using PostgreSQL you can override the connection string and password: + +```bash +DATABASE_URL=Host=localhost;Database=players;Username=postgres;Password=P0579r35_p455W0rd! +POSTGRES_PASSWORD=P0579r35_p455W0rd! +``` + +Copy `.env.example` to `.env` and edit as needed — `.env` is git-ignored. + +## Prerequisites + +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 (optional, for creating new migrations) + + ```bash + dotnet tool install --global dotnet-ef + ``` + +## Quick Start + +### Clone the repository + +```bash +git clone https://github.com/nanotaboada/Dotnet.Samples.AspNetCore.WebApi.git +cd Dotnet.Samples.AspNetCore.WebApi +``` + +### Run the application + +```bash +dotnet watch run --project src/Dotnet.Samples.AspNetCore.WebApi/Dotnet.Samples.AspNetCore.WebApi.csproj +``` + +The server will start on `https://localhost:9000`. + +### Access the application + +- API: `https://localhost:9000` +- Swagger Documentation: `https://localhost:9000/swagger/index.html` +- Health Check: `https://localhost:9000/health` + +## Containers + +This project includes full Docker support with multi-stage builds and Docker Compose for easy deployment. + +### Build the Docker image + +```bash +docker compose build +``` + +### Start the application + +```bash +docker compose up +``` + +> 💡 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 + +```bash +docker compose down +``` + +### Reset the database + +To remove the volume and let the app re-create and re-seed the database on next startup: + +```bash +docker compose down -v +``` + +The containerized application runs on port 9000 and includes health checks that monitor the `/health` endpoint. + +### Pull Docker images + +Each release publishes multiple tags for flexibility: + +```bash +# By semantic version (recommended for production) +docker pull ghcr.io/nanotaboada/dotnet-samples-aspnetcore-webapi:1.0.0 + +# By stadium name (memorable alternative) +docker pull ghcr.io/nanotaboada/dotnet-samples-aspnetcore-webapi:azteca + +# Latest release +docker pull ghcr.io/nanotaboada/dotnet-samples-aspnetcore-webapi:latest +``` + +## Environment Variables + +The application can be configured using environment variables for different scenarios: + +### Local Development (`.vscode/launch.json`) + +For local development and debugging: + +```bash +# ASP.NET Core environment mode +ASPNETCORE_ENVIRONMENT=Development + +# Server URLs +ASPNETCORE_URLS=https://localhost:9000 + +# Show detailed error messages +ASPNETCORE_DETAILEDERRORS=1 + +# Graceful shutdown timeout +ASPNETCORE_SHUTDOWNTIMEOUTSECONDS=3 +``` + +### Container Deployment (`compose.yaml`) + +For production deployment: + +```bash +# Database storage path +# Points to the persistent Docker volume +STORAGE_PATH=/storage/players-sqlite3.db +``` + +> 💡 Additional environment variables (`ASPNETCORE_ENVIRONMENT=Production` and `ASPNETCORE_URLS=http://+:9000`) are set in the `Dockerfile`. + +## Contributing + +Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on the code of conduct and the process for submitting pull requests. + +**Key guidelines:** + +- Follow [Conventional Commits](https://www.conventionalcommits.org/) for commit messages +- Ensure all tests pass (`dotnet test`) +- Keep changes small and focused +- Review [.github/copilot-instructions.md](.github/copilot-instructions.md) for architectural patterns + +**Testing:** + +Run the test suite with xUnit: + +```bash +# Run all tests +dotnet test + +# Run tests with coverage report +dotnet test --results-directory "coverage" --collect:"XPlat Code Coverage" --settings .runsettings +``` + +## Command Summary + +| Command | Description | +| ------- | ----------- | +| `dotnet watch run --project src/...` | Start development server with hot reload | +| `dotnet build` | Build the solution | +| `dotnet test` | Run all tests | +| `dotnet test --collect:"XPlat Code Coverage"` | Run tests with coverage report | +| `dotnet csharpier .` | Format source code | +| `dotnet ef migrations add ` | Create a new migration | +| `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 | +| `docker compose down -v` | Stop and remove Docker volume | +| **AI Commands** | | +| `/pre-commit` | Runs linting, tests, and quality checks before committing | +| `/pre-release` | Runs pre-release validation workflow | + +## Legal + +This project is provided for educational and demonstration purposes and may be used in production at your own discretion. All trademarks, service marks, product names, company names, and logos referenced herein are the property of their respective owners and are used solely for identification or illustrative purposes. diff --git a/RELEASES.md b/RELEASES.md new file mode 100644 index 0000000..86b14c6 --- /dev/null +++ b/RELEASES.md @@ -0,0 +1,73 @@ +# Releases + +Releases follow the pattern `v{SEMVER}-{STADIUM}` (e.g., `v1.0.0-azteca`). Codenames are drawn alphabetically from the [stadium list](CHANGELOG.md#stadium-release-names) of famous football stadiums that hosted FIFA World Cup matches. + +## Workflow + +### 1. Create a Release Branch + +Branch protection prevents direct pushes to `master`, so all release prep goes through a PR: + +```bash +git checkout master && git pull +git checkout -b release/v1.0.0-azteca +``` + +### 2. Update CHANGELOG.md + +Move items from `[Unreleased]` to a new release section in [CHANGELOG.md](CHANGELOG.md): + +```bash +# Move items from [Unreleased] to new release section +# Example: [1.0.0 - azteca] - 2026-01-22 +git add CHANGELOG.md +git commit -m "docs: prepare changelog for v1.0.0-azteca release" +git push origin release/v1.0.0-azteca +# Open a PR, get it reviewed, and merge into master +``` + +### Pre-release Checklist + +Before creating the tag, verify all of the following: + +- [ ] `CHANGELOG.md` `[Unreleased]` section is moved to a new versioned release entry +- [ ] `dotnet build --configuration Release` passes +- [ ] `dotnet test --settings .runsettings` passes +- [ ] Stadium name is valid and follows alphabetical order (see [stadium list](CHANGELOG.md#stadium-release-names)) +- [ ] All CI checks on `master` are green + +### 3. Create and Push Tag + +After the PR is merged, create and push the version tag from `master`: + +```bash +git checkout master && git pull +git tag -a v1.0.0-azteca -m "Release 1.0.0 - Azteca" +git push origin v1.0.0-azteca +``` + +### 4. Automated CD Workflow + +Pushing the tag triggers the CD workflow which automatically: + +1. Validates the stadium name +2. Builds and tests the project in Release configuration +3. Publishes Docker images to GitHub Container Registry with three tags +4. Creates a GitHub Release with auto-generated changelog from commits + +> 💡 Always update CHANGELOG.md before creating the tag. See [CHANGELOG.md](CHANGELOG.md) for the complete stadium list (A-Z) and release history. + +## Docker Pull + +Each release publishes multiple tags for flexibility: + +```bash +# By semantic version (recommended for production) +docker pull ghcr.io/nanotaboada/dotnet-samples-aspnetcore-webapi:1.0.0 + +# By stadium name (memorable alternative) +docker pull ghcr.io/nanotaboada/dotnet-samples-aspnetcore-webapi:azteca + +# Latest release +docker pull ghcr.io/nanotaboada/dotnet-samples-aspnetcore-webapi:latest +``` diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 0000000..aea613a --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,75 @@ +# ASP.NET Core 10.0 +# Build and test ASP.NET Core projects targeting .NET 10 on Linux. +# https://learn.microsoft.com/en-us/azure/devops/pipelines/ecosystems/dotnet-core + +# Pipeline triggers: +# - Disabled: CI is fully covered by GitHub Actions (.github/workflows/dotnet-ci.yml) +# - Kept for educational/reference purposes only +trigger: none + +pr: none + +# Agent pool configuration (equivalent to GitHub Actions 'runs-on: ubuntu-latest') +pool: + vmImage: "ubuntu-latest" + +# Environment variables (equivalent to GitHub Actions 'env:') +variables: + buildConfiguration: "Release" + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 # Skip .NET welcome message + DOTNET_NOLOGO: true # Suppress .NET logo in output + NUGET_PACKAGES: $(Pipeline.Workspace)/.nuget/packages + +# Pipeline steps (equivalent to GitHub Actions 'steps:') +steps: + # Checkout repository (equivalent to actions/checkout) + # Azure Pipelines does this implicitly, but making it explicit for clarity + - checkout: self + displayName: "Checkout repository" + + # Cache NuGet packages (equivalent to actions/setup-dotnet cache feature) + # Uses packages.lock.json hash as cache key for faster restores + - task: Cache@2 + displayName: "Cache NuGet packages" + inputs: + key: 'nuget | "$(Agent.OS)" | **/packages.lock.json' + restoreKeys: | + nuget | "$(Agent.OS)" + path: $(NUGET_PACKAGES) + + # Install .NET 10 SDK (equivalent to actions/setup-dotnet) + # performMultiLevelLookup: allows finding other SDK versions if needed + - task: UseDotNet@2 + displayName: "Set up .NET 10" + inputs: + version: "10.x" + performMultiLevelLookup: true + + # Restore NuGet packages (equivalent to 'dotnet restore') + # feedsToUse: 'select' uses feeds from NuGet.config + - task: DotNetCoreCLI@2 + displayName: "Restore dependencies" + inputs: + command: "restore" + projects: "**/*.csproj" + feedsToUse: "select" + + # Build the solution (equivalent to 'dotnet build') + # --no-restore: skip restore since we just did it (faster build) + - task: DotNetCoreCLI@2 + displayName: "Build projects" + inputs: + command: "build" + projects: "**/*.sln" + arguments: "--configuration $(buildConfiguration) --no-restore" + + # Run unit tests (equivalent to 'dotnet test') + # --no-build: use already-built assemblies (faster) + # publishTestResults: automatically publish test results to Azure DevOps (built-in feature) + - task: DotNetCoreCLI@2 + displayName: "Run tests" + inputs: + command: "test" + projects: "**/*Tests/*.csproj" + arguments: "--configuration $(buildConfiguration) --no-build" + publishTestResults: true diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..2379c0c --- /dev/null +++ b/codecov.yml @@ -0,0 +1,68 @@ +# Codecov Repository YAML +# https://docs.codecov.com/docs/codecov-yaml + +coverage: + # https://docs.codecov.com/docs/commit-status + status: + project: + default: + target: 80% + threshold: 100% + if_not_found: success + if_ci_failed: success + patch: + default: + target: 0% + threshold: 100% + if_not_found: success + +# https://docs.codecov.com/docs/components#component-options +component_management: + default_rules: + statuses: + - type: project + target: auto + branches: + - "!main" + individual_components: + - component_id: controllers + name: Controllers + paths: + - "src/Dotnet.Samples.AspNetCore.WebApi/Controllers/" + - component_id: services + name: Services + paths: + - "src/Dotnet.Samples.AspNetCore.WebApi/Services/" + +comment: + layout: "header, diff, flags, components" + behavior: default + require_changes: false + +# https://docs.codecov.com/docs/ignoring-paths +ignore: + - .*\.sln + - .*\.csproj + - .*\.json + - .*\.yml + - .*\.png + - "**/*.md" + + - .*\/test\/.* + - .*\/scripts\/.* + - .*\/Program\.cs + - "**/LICENSE" + - "**/README.md" + + - .*\/Configurations\/.* + - .*\/Data\/.* + - .*\/Enums\/.* + - .*\/Extensions\/.* + - .*\/Mappings\/.* + - .*\/Middlewares\/.* + - .*\/Migrations\/.* + - .*\/Models\/.* + - .*\/Properties\/.* + - .*\/Repositories\/.* + - .*\/Utilities\/.* + - .*\/Validators\/.* diff --git a/commitlint.config.mjs b/commitlint.config.mjs new file mode 100644 index 0000000..197735d --- /dev/null +++ b/commitlint.config.mjs @@ -0,0 +1,14 @@ +import conventional from "@commitlint/config-conventional"; + +export default { + ...conventional, + rules: { + "header-max-length": [2, "always", 80], + "body-max-line-length": [2, "always", 80], + }, + ignores: [ + // bypass Dependabot-style commits + (message) => /^chore\(deps(-dev)?\): bump /.test(message), + (message) => /Signed-off-by: dependabot\[bot\]/.test(message), + ], +}; diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..f79155e --- /dev/null +++ b/compose.yaml @@ -0,0 +1,45 @@ +name: dotnet-samples-aspnetcore-webapi + +services: + api: + image: dotnet-samples-aspnetcore-webapi + container_name: aspnetcore-app + build: + context: . + dockerfile: Dockerfile + ports: + - "9000:9000" + volumes: + - storage:/storage/ + environment: + - DATABASE_PROVIDER=${DATABASE_PROVIDER:-sqlite} + - DATABASE_URL=${DATABASE_URL:-Host=postgres;Database=players;Username=postgres;Password=postgres} + - STORAGE_PATH=/storage/players-sqlite3.db + depends_on: + postgres: + condition: service_healthy + required: false + restart: unless-stopped + + postgres: + image: postgres:17-alpine + container_name: aspnetcore-postgres + profiles: [postgres] + environment: + POSTGRES_DB: players + POSTGRES_USER: postgres + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} + volumes: + - postgres-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + +volumes: + storage: + name: dotnet-samples-aspnetcore-webapi_storage + postgres-data: + name: dotnet-samples-aspnetcore-webapi_postgres-data diff --git a/docs/adr/0001-adopt-traditional-layered-architecture.md b/docs/adr/0001-adopt-traditional-layered-architecture.md new file mode 100644 index 0000000..c764e2f --- /dev/null +++ b/docs/adr/0001-adopt-traditional-layered-architecture.md @@ -0,0 +1,32 @@ +# 0001. Adopt Traditional Layered Architecture + +Date: 2026-04-02 + +## Status + +Accepted (Under Reconsideration — see [Issue #266](https://github.com/nanotaboada/Dotnet.Samples.AspNetCore.WebApi/issues/266)) + +## Context + +The project needed a structural pattern to organise the codebase for a REST API managing football player data. The primary goal is educational clarity — the project is a learning reference, so the pattern must be immediately understandable to developers familiar with standard layered architecture. + +The two main candidates considered were traditional layered architecture (Controllers → Services → Repositories → Data) and Clean Architecture (with explicit domain, application, and infrastructure rings). Clean Architecture offers better separation of concerns and testability at the cost of significantly more boilerplate and conceptual overhead for a small PoC. + +## Decision + +We will organise the codebase into four layers: HTTP (`Controllers`, `Validators`), Business (`Services`, `Mappings`), Data (`Repositories`, `Data`), and a cross-cutting `Models` layer. Each layer depends only on the layer below it; controllers must not access repositories directly, and business logic must not live in controllers. + +## Consequences + +### Positive +- The pattern is widely understood and requires no prior knowledge of advanced architectural styles. +- The folder structure maps directly to responsibilities, making the codebase easy to navigate. +- Onboarding friction is low — contributors can locate any piece of logic by layer name alone. + +### Negative +- Domain entities (`Player`) are shared across all layers, mixing persistence concerns with domain logic. +- Dependencies flow in multiple directions at the model level, limiting flexibility. +- Tight coupling between layers makes large-scale refactoring harder as the project grows. + +### Neutral +- This decision is expected to be superseded when Issue #266 (Clean Architecture migration) is implemented. This ADR will remain in the repository as a record of the original decision and its context. diff --git a/docs/adr/0002-use-mvc-controllers-over-minimal-api.md b/docs/adr/0002-use-mvc-controllers-over-minimal-api.md new file mode 100644 index 0000000..38eb86a --- /dev/null +++ b/docs/adr/0002-use-mvc-controllers-over-minimal-api.md @@ -0,0 +1,33 @@ +# 0002. Use MVC Controllers over Minimal API + +Date: 2026-04-02 + +## Status + +Accepted + +## Context + +ASP.NET Core offers two approaches for building HTTP endpoints: traditional MVC controllers (attribute-routed classes inheriting from `ControllerBase`) and Minimal API (lambda-based route handlers registered directly in `Program.cs`). Microsoft has been investing in Minimal API as the preferred path for new greenfield services since .NET 6. + +For this project, the primary concern is educational clarity. The codebase serves as a learning reference for developers exploring REST API patterns in .NET, and it is part of a cross-language comparison set where consistent structural conventions across stacks matter. + +## Decision + +We will use MVC controllers with attribute routing. Each controller is a class that encapsulates related HTTP handlers, receives its dependencies via constructor injection, and delegates all business logic to the service layer. + +## Consequences + +### Positive +- Controllers provide explicit, visible grouping of related endpoints in a single class. +- Constructor injection and interface-based dependencies make controllers straightforward to unit test with mocks. +- The pattern is familiar to developers coming from any MVC background (Spring MVC, Django CBVs, Rails controllers), lowering the barrier for the target audience. +- Attribute routing (`[HttpGet]`, `[HttpPost]`, etc.) keeps route definitions co-located with their handlers. + +### Negative +- Controllers introduce more boilerplate than Minimal API: class declarations, action method signatures, and `[FromRoute]`/`[FromBody]` attributes. +- Minimal API is the direction Microsoft is actively investing in for new projects; staying on MVC controllers means diverging from that trajectory over time. +- The framework overhead of MVC (model binding pipeline, action filters, etc.) is larger than Minimal API, though negligible at this project's scale. + +### Neutral +- This decision may be revisited if the project migrates to Clean Architecture (Issue #266), at which point Minimal API endpoints or vertical slice handlers could be a better fit for the new structure. diff --git a/docs/adr/0003-use-sqlite-for-data-storage.md b/docs/adr/0003-use-sqlite-for-data-storage.md new file mode 100644 index 0000000..23cd661 --- /dev/null +++ b/docs/adr/0003-use-sqlite-for-data-storage.md @@ -0,0 +1,33 @@ +# 0003. Use SQLite for Data Storage + +Date: 2026-04-02 + +## Status + +Superseded by [ADR-0014](0014-configurable-database-provider.md) + +## Context + +The project requires a relational database to persist football player data. The main candidates were SQLite, PostgreSQL, and SQL Server. The project is a proof-of-concept and learning reference, not a production service. Deployment simplicity and zero-configuration setup are higher priorities than scalability or advanced database features. + +The cross-language comparison set (Go/Gin, Java/Spring Boot, Python/FastAPI, Rust/Rocket, TypeScript/Node.js) all use SQLite for the same reasons, so consistency across the set also favours it. + +## Decision + +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. +- 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. +- Some EF Core features (e.g., certain migration operations) behave differently with the SQLite provider. +- Not representative of a production database choice, which may mislead learners about real-world persistence decisions. + +### Neutral +- Issue #249 tracks adding PostgreSQL support for environments that require a production-grade database. When implemented, SQLite will remain the default for local development and this ADR will be supplemented by a new ADR documenting the PostgreSQL decision. diff --git a/docs/adr/0004-use-uuid-as-database-primary-key.md b/docs/adr/0004-use-uuid-as-database-primary-key.md new file mode 100644 index 0000000..ec603e4 --- /dev/null +++ b/docs/adr/0004-use-uuid-as-database-primary-key.md @@ -0,0 +1,32 @@ +# 0004. Use UUID as Database Primary Key + +Date: 2026-04-02 + +## Status + +Accepted + +## Context + +Every `Player` record requires a stable, unique identity for the database. The two common choices are a sequential integer (`int` / `IDENTITY`) and a UUID (`Guid`). Sequential integers are compact, human-readable, and index-friendly, but they leak information: an external caller who knows ID `42` can infer there are at least 42 records and probe adjacent IDs. UUIDs are opaque and decoupled from the database's row insertion order. + +Crucially, the internal primary key is never surfaced in the API (see ADR 0005 for why `squadNumber` is the public-facing identifier). This means the choice of key type has no direct impact on API consumers. + +## Decision + +We will use `Guid` (`UUID v4`) as the primary key for the `Player` entity. The `Player` model declares `public Guid Id { get; set; } = Guid.NewGuid()`, which generates the value at the application layer before EF Core calls `SaveChanges`. `PlayerDbContext` additionally configures `entity.Property(player => player.Id).ValueGeneratedOnAdd()`, which signals to EF Core that the column is auto-generated — this is consistent with the field initializer approach and ensures migrations reflect the intended behaviour. The `Id` property is never included in API request or response models. + +## Consequences + +### Positive +- Opaque IDs prevent callers from guessing or enumerating records by probing sequential values. +- The field initializer (`Guid.NewGuid()`) generates the UUID at the application layer before the record reaches the database, decoupling identity generation from the persistence engine. +- Aligns with .NET defaults: `Guid.NewGuid()` requires no additional libraries or configuration. + +### Negative +- `Guid` occupies 16 bytes vs 4 bytes for `int`, increasing index and storage size. +- Random UUID v4 values are not monotonically increasing, which can cause B-tree index fragmentation on write-heavy workloads. This is not a concern at this project's scale with SQLite, but would matter at production load on PostgreSQL or SQL Server. +- UUIDs are not human-readable, making raw database inspection or log correlation harder. + +### Neutral +- Because `Id` is never exposed through the API, consumers are fully isolated from this decision. A future migration to sequential IDs or UUID v7 (time-ordered) would be a pure database/model change with no API contract impact. diff --git a/docs/adr/0005-use-squad-number-as-api-mutation-key.md b/docs/adr/0005-use-squad-number-as-api-mutation-key.md new file mode 100644 index 0000000..d6d1d87 --- /dev/null +++ b/docs/adr/0005-use-squad-number-as-api-mutation-key.md @@ -0,0 +1,34 @@ +# 0005. Use Squad Number as API Mutation Key + +Date: 2026-04-02 + +## Status + +Accepted + +## Context + +The API exposes endpoints for retrieving, updating, and deleting individual players. Each of these operations requires a route parameter that identifies the target player. Two candidates exist: the internal UUID primary key (`Id`) and the domain-meaningful squad number (`SquadNumber`). + +Exposing the UUID would couple API consumers to an internal database concern. UUIDs carry no domain meaning — a caller who knows that player "Lionel Messi" wears number 10 cannot construct the URL from that knowledge alone; they would first need to discover the UUID from a prior `GET /players` call. + +`SquadNumber` is the natural identifier for a football player within a team context. It is the value coaches, fans, and data consumers use to refer to players. It is also the field that other API operations (`POST` uniqueness check, `PUT` route matching) already reason about. + +## Decision + +We will use `squadNumber` as the route parameter for mutation endpoints and primary lookup routes: `GET /players/squadNumber/{n}`, `PUT /players/squadNumber/{n}`, and `DELETE /players/squadNumber/{n}`. The internal `Id` (UUID) is never included in any request model or response model. A secondary `GET /players/{id:Guid}` route exists for internal or administrative lookups by UUID but is not part of the primary API contract. + +## Consequences + +### Positive +- URLs are human-readable and predictable from domain knowledge alone (`/players/squadNumber/10` unambiguously refers to the number-10 player). +- API consumers are fully decoupled from the internal persistence model. +- Consistency: the same identifier used in the domain (`SquadNumber`) is used in the API surface, reducing the mental translation between domain concepts and HTTP calls. + +### Negative +- Squad numbers are unique only within a single team and only for the duration of a season. They can be reassigned when a player leaves or retires. This project models a single team's squad, so cross-team ambiguity is not a concern, but the limitation is real. +- Uniqueness must be enforced at the application layer. The `BeUniqueSquadNumber` async validator rule in the `Create` rule set guards against duplicate squad numbers on insert; the `Update` rule set intentionally omits this check because the player already exists. +- `squadNumber` is not a stable long-term identifier — if squad number reassignment were ever supported, consumers holding a bookmarked URL would silently reach a different player. + +### Neutral +- The `GET /players/{id:Guid}` endpoint exists as a secondary route for internal or administrative lookups by UUID. It is intentionally excluded from the primary API contract and is not the recommended path for general consumers. diff --git a/docs/adr/0006-use-rfc-7807-problem-details-for-errors.md b/docs/adr/0006-use-rfc-7807-problem-details-for-errors.md new file mode 100644 index 0000000..d435544 --- /dev/null +++ b/docs/adr/0006-use-rfc-7807-problem-details-for-errors.md @@ -0,0 +1,32 @@ +# 0006. Use RFC 7807 Problem Details for Errors + +Date: 2026-04-02 + +## Status + +Accepted + +## Context + +HTTP APIs need a consistent format for communicating errors to consumers. Without a standard, each endpoint might return a different error shape — plain strings, custom JSON objects, or raw status codes with empty bodies — making client-side error handling fragile and unpredictable. + +RFC 7807 (Problem Details for HTTP APIs) defines a standard JSON structure for error responses with well-known fields (`type`, `title`, `status`, `detail`, `instance`). ASP.NET Core 7+ includes built-in support for this format via `TypedResults.Problem` and `TypedResults.ValidationProblem`. + +## Decision + +We will use RFC 7807 Problem Details for all error responses. Validation failures will use `TypedResults.ValidationProblem`, which extends the standard shape with a `errors` field containing field-level messages. All other errors (not found, conflict, internal server error) will use `TypedResults.Problem` with an appropriate HTTP status code and a human-readable `detail` field. + +## Consequences + +### Positive +- A single, predictable error shape across all endpoints simplifies client-side error handling — consumers check `status` and `detail` without branching on response format. +- The format is an IETF standard, meaning it is recognisable to developers and interoperable with API tooling, gateways, and monitoring systems that understand Problem Details. +- Built-in ASP.NET Core support means no custom serialisation code is needed. +- Validation errors include field-level detail via the `errors` dictionary, giving consumers enough context to display meaningful feedback. + +### Negative +- The response payload is more verbose than a plain string message, which adds minor overhead for simple error cases. +- Consumers must understand the Problem Details schema to interpret `type` URIs and distinguish error categories beyond HTTP status codes. + +### Neutral +- The `type` field conventionally holds a URI that identifies the problem type. Most responses rely on the ASP.NET Core default, but individual controllers may override it with problem-specific URIs where appropriate — for example, `PlayerController` sets `Type = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409"` for conflict responses. In a production API, all `type` URIs would point to project-owned documentation pages. diff --git a/docs/adr/0007-use-fluentvalidation-over-data-annotations.md b/docs/adr/0007-use-fluentvalidation-over-data-annotations.md new file mode 100644 index 0000000..826510d --- /dev/null +++ b/docs/adr/0007-use-fluentvalidation-over-data-annotations.md @@ -0,0 +1,35 @@ +# 0007. Use FluentValidation over Data Annotations + +Date: 2026-04-02 + +## Status + +Accepted + +## Context + +ASP.NET Core provides two primary mechanisms for validating request models: Data Annotations (attributes applied directly to model properties, e.g. `[Required]`, `[Range]`) and FluentValidation (a separate library that defines validation rules in dedicated validator classes). + +Data Annotations are built into the framework and require no additional dependencies, but they are limited to declarative, property-level rules. Complex cross-property validation, async database checks (e.g., verifying that a squad number is unique), and operation-specific rules (different rules for create vs. update) are difficult or impossible to express with annotations alone. + +The player creation flow requires an async uniqueness check against the database (`BeUniqueSquadNumber`). The update flow must skip that same check because the player already exists. This kind of operation-contextual validation is a first-class concern in FluentValidation via named rule sets. + +## Decision + +We will use FluentValidation for all request model validation. Validators are defined in dedicated classes under `Validators/` and registered with the DI container via `AddValidatorsFromAssemblyContaining()`. Validation rules are grouped into named rule sets (`Create` and `Update`) to make operation-specific behaviour explicit. Controllers invoke the appropriate rule set by name. + +## Consequences + +### Positive +- Validation logic lives in its own class, separate from the model and the controller, respecting the Single Responsibility Principle. +- Named rule sets (`Create`, `Update`) make it explicit which rules apply to which HTTP operations, eliminating hidden branching inside the validator. +- Async rules (e.g., `MustAsync(BeUniqueSquadNumber)`) integrate naturally, enabling database-backed validation without awkward workarounds. +- Validators are plain classes and trivial to unit test in isolation. + +### Negative +- FluentValidation is an additional NuGet dependency not bundled with the framework. +- The `ValidateAsync` overload called internally is the `IValidationContext` overload, not the generic one — an implementation detail that must be respected when mocking validators in controller tests (see Copilot instructions for the correct Moq setup). +- Teams unfamiliar with FluentValidation face a learning curve around rule sets and the async API. + +### Neutral +- FluentValidation does not replace model binding. Invalid JSON structure or type mismatches are still caught by the ASP.NET Core model binder before FluentValidation runs. diff --git a/docs/adr/0008-use-automapper-for-dto-mapping.md b/docs/adr/0008-use-automapper-for-dto-mapping.md new file mode 100644 index 0000000..20339be --- /dev/null +++ b/docs/adr/0008-use-automapper-for-dto-mapping.md @@ -0,0 +1,32 @@ +# 0008. Use AutoMapper for DTO Mapping + +Date: 2026-04-02 + +## Status + +Accepted + +## Context + +The API separates its public contract (request/response DTOs) from its internal persistence model (`Player` entity). Every controller action that reads or writes data must map between these representations. Without a mapping library, this means writing explicit property-assignment code (`responseModel.Name = entity.Name`, etc.) in every service method — boilerplate that grows linearly with the number of fields and operations. + +AutoMapper provides convention-based mapping: if property names match between source and destination, the mapping is configured once in a profile and applied everywhere. Custom mappings (renaming, transforming, ignoring fields) are expressed declaratively in the profile class. + +## Decision + +We will use AutoMapper for all DTO-to-entity and entity-to-DTO mappings. Mappings are defined in `PlayerMappingProfile` under `Mappings/` and registered via `AddAutoMapper(typeof(PlayerMappingProfile))`. `PlayerRequestModel` does not expose an `Id` property, so callers structurally cannot set or override the internal primary key — no explicit `.ForMember(...Ignore())` is required on the `PlayerRequestModel → Player` mapping. The `Player → PlayerResponseModel` mapping suppresses the AutoMapper validation warning for the unmapped `Id` source member via `.ForSourceMember(source => source.Id, options => options.DoNotValidate())`. + +## Consequences + +### Positive +- Eliminates repetitive property-assignment boilerplate in service methods. +- Mapping configuration is centralised in one profile class, making it easy to audit what is and is not mapped. +- Protection of the internal `Id` is structural — the absence of `Id` on `PlayerRequestModel` is the guarantee, which is simpler and more robust than relying on an explicit ignore rule. + +### Negative +- AutoMapper uses reflection at startup to validate mapping configurations, adding a small startup overhead. +- "Magic" convention-based mapping can be opaque: when a property is silently unmapped due to a name mismatch, the bug only surfaces at runtime (though `AssertConfigurationIsValid()` in tests can catch this). +- Adding AutoMapper as a dependency means contributors must understand profile configuration to modify or extend mappings. + +### Neutral +- AutoMapper's `ProjectTo()` LINQ extension for EF Core queryable projections is available but not currently used. Direct entity retrieval followed by in-memory mapping is sufficient at this project's scale. diff --git a/docs/adr/0009-implement-in-memory-caching.md b/docs/adr/0009-implement-in-memory-caching.md new file mode 100644 index 0000000..4b9424c --- /dev/null +++ b/docs/adr/0009-implement-in-memory-caching.md @@ -0,0 +1,34 @@ +# 0009. Implement In-Memory Caching + +Date: 2026-04-02 + +## Status + +Accepted + +## Context + +The player list endpoint (`GET /players`) queries the database on every request. For a read-heavy API where the data changes infrequently, this is unnecessary load. Caching the result reduces database round-trips and demonstrates a common performance pattern for learners. + +The two main caching approaches in .NET are `IMemoryCache` (process-local, in-memory) and distributed caching (Redis, SQL Server — shared across multiple instances via `IDistributedCache`). Distributed caching is appropriate for multi-instance deployments where cache consistency across nodes matters. + +This project runs as a single instance (local development or a single Docker container) with no horizontal scaling requirement. + +## Decision + +We will use `IMemoryCache` managed in the service layer (`PlayerService`). Cache entries use a 10-minute sliding expiration combined with a one-hour absolute expiration: each access within the sliding window resets the TTL, but the entry is always evicted after one hour regardless of access frequency. On a cache miss, the service queries the repository, stores the result under a well-known key with these options, and returns it. Write operations (create, update, delete) invalidate the cache entry so the next read reflects the current state. + +## Consequences + +### Positive +- Zero additional infrastructure: `IMemoryCache` is built into `Microsoft.Extensions.Caching.Memory` with no external service dependency. +- The service layer caching pattern is easy to follow and demonstrates the cache-aside pattern clearly for learners. +- Reduces database queries for the most frequent read operation without any changes to the repository or controller layers. + +### Negative +- Cache state is lost on every application restart, so the first request after a restart always hits the database. +- `IMemoryCache` is not shared across multiple instances. If this application were scaled horizontally, each instance would maintain its own independent cache, potentially serving stale or inconsistent data across nodes. +- Memory usage grows with the size of the cached data; unbounded caching of large datasets in a long-running process could contribute to memory pressure. + +### Neutral +- If the project ever moves to a multi-instance deployment (e.g., Kubernetes), replacing `IMemoryCache` with a Redis-backed `IDistributedCache` would require changes only in the service layer and `Program.cs` registration — the repository and controller layers are unaffected. diff --git a/docs/adr/0010-use-serilog-for-structured-logging.md b/docs/adr/0010-use-serilog-for-structured-logging.md new file mode 100644 index 0000000..13af9f6 --- /dev/null +++ b/docs/adr/0010-use-serilog-for-structured-logging.md @@ -0,0 +1,34 @@ +# 0010. Use Serilog for Structured Logging + +Date: 2026-04-02 + +## Status + +Accepted + +## Context + +.NET provides a built-in logging abstraction (`Microsoft.Extensions.Logging`) with pluggable providers. Out of the box it supports console and debug output, but structured log output — where log events are emitted as queryable key-value pairs rather than plain strings — requires a third-party sink. + +Structured logging is important even for a learning project because it demonstrates a production practice: log consumers (Elasticsearch, Seq, Datadog) ingest structured events and can filter, aggregate, and alert on specific fields. Emitting plain strings that embed values in a message template forfeits that capability. + +Serilog is the most widely used structured logging library in the .NET ecosystem. It integrates with `ILogger` via a host-level configuration, meaning application code never imports Serilog directly — it depends only on the framework abstraction. + +## Decision + +We will use Serilog, configured at host level in `Program.cs` via `UseSerilog()`. Two sinks are configured: console (for local development visibility) and file (for persistent log output under `logs/`). Message template parameters are passed as structured properties (`{@Player}`, `{SquadNumber}`), not interpolated strings, so that sinks that support structured output can index them as discrete fields. + +## Consequences + +### Positive +- Application code depends on `ILogger` (the framework abstraction), not on Serilog directly. Swapping the logging backend requires only a `Program.cs` change. +- Structured log events support downstream querying and alerting in log management systems, demonstrating a production-grade logging practice. +- Serilog's enrichers (machine name, thread ID, environment) can be added declaratively in configuration without touching application code. +- The file sink provides a persistent log archive useful for debugging Docker container runs after the container exits. + +### Negative +- Serilog is an additional dependency. Its host-level integration (`UseSerilog`) replaces the default ASP.NET Core logging pipeline, which can surprise contributors who expect the default provider behaviour. +- Misconfigured message templates (e.g., using string interpolation instead of structured parameters) silently degrade structured output without compile-time warnings. + +### Neutral +- The project previously used `Microsoft.Extensions.Logging` directly (removed in PR #192). The migration to Serilog was driven by the need for file sink support and consistent structured output across the cross-language comparison set. diff --git a/docs/adr/0011-use-docker-for-containerization.md b/docs/adr/0011-use-docker-for-containerization.md new file mode 100644 index 0000000..772e5bd --- /dev/null +++ b/docs/adr/0011-use-docker-for-containerization.md @@ -0,0 +1,33 @@ +# 0011. Use Docker for Containerization + +Date: 2026-04-02 + +## Status + +Accepted + +## Context + +The project needs to be runnable without requiring contributors to install the .NET SDK, configure environment variables, or manage database files manually. A containerised delivery format solves the "works on my machine" problem and demonstrates a standard deployment practice. + +Docker is the de facto containerisation standard in the .NET ecosystem and across the cross-language comparison set that this project belongs to. Docker Compose provides a single-command local orchestration layer that handles image building, port mapping, and volume mounting without requiring knowledge of raw `docker run` flags. + +## Decision + +We will provide a multi-stage `Dockerfile` (build stage + runtime stage) and a `compose.yaml` for local orchestration. The application runs on port 9000 in the container, matching the local development port. A named Docker volume persists the SQLite database file across container restarts. The first run copies a pre-seeded database into the volume; subsequent runs reuse the existing volume. + +## Consequences + +### Positive +- `docker compose up` is the only command needed to run the full application from a fresh clone, with no SDK or database setup required. +- Multi-stage builds produce a minimal runtime image: only the published application artifacts are included, not the SDK or intermediate build outputs. +- The containerised environment closely mirrors what the CD pipeline builds and publishes to GitHub Container Registry, reducing environment-specific surprises. +- Port 9000 is consistent across all environments (local, Docker, CI), eliminating port-related configuration drift. + +### Negative +- Docker Desktop is required on developer machines. On macOS and Windows, Docker Desktop is a large install with licensing implications for commercial use. +- The Docker abstraction layer adds a level of indirection that can make debugging harder for contributors unfamiliar with container networking or volume mounts. +- SQLite inside a Docker volume is not suitable for production use — it is an appropriate trade-off only because this project is explicitly a development and learning reference. + +### Neutral +- Images are published to GitHub Container Registry (`ghcr.io`) as part of the CD pipeline, tagged by semantic version, stadium name, and `latest`. See the Releases section of `README.md` for the full tagging convention. diff --git a/docs/adr/0012-use-stadium-themed-semantic-versioning.md b/docs/adr/0012-use-stadium-themed-semantic-versioning.md new file mode 100644 index 0000000..9f09141 --- /dev/null +++ b/docs/adr/0012-use-stadium-themed-semantic-versioning.md @@ -0,0 +1,35 @@ +# 0012. Use Stadium-Themed Semantic Versioning + +Date: 2026-04-02 + +## Status + +Accepted + +## Context + +The project follows Semantic Versioning (`MAJOR.MINOR.PATCH`) for release numbers. Purely numeric version strings are accurate but forgettable — contributors and users rarely remember whether "2.1.0" came before or after "2.0.3" without consulting the changelog. + +The project is football-themed (it manages football player data), and is part of a cross-language comparison set. A naming convention that reflects the project's domain makes releases memorable and reinforces the project's identity. + +Several well-known projects use codename conventions: Ubuntu (alphabetical adjective-animal pairs), Android (alphabetical desserts until Android 10), macOS (California landmarks). The pattern is established and understood. + +## Decision + +We will append a football stadium codename to every release tag, following the format `vMAJOR.MINOR.PATCH-stadium` (e.g., `v2.1.0-dusseldorf`). Stadium names are drawn from a fixed, alphabetically ordered list of venues that hosted FIFA World Cup matches, documented in `CHANGELOG.md`. Names are assigned sequentially A→Z; the next release always uses the next unused letter. The tag format is enforced by the CD pipeline, which validates the stadium name before publishing. + +## Consequences + +### Positive +- Release names are memorable: "the dusseldorf release" is easier to recall and discuss than "2.1.0". +- Alphabetical ordering provides an implicit sequence — contributors can determine release order from the name alone without consulting version numbers. +- The convention is consistent and deterministic: there is no ambiguity about which name comes next. +- Reinforces the football domain theme throughout the project's lifecycle. + +### Negative +- The convention is non-standard and may confuse first-time contributors who expect plain semantic version tags. +- The stadium list is finite (26 letters). If the project ever reaches 26 major releases, the list must be extended or the convention revisited. +- The CD pipeline validation adds a small amount of CI complexity to enforce the format. + +### Neutral +- The stadium list is published in `CHANGELOG.md` under "Stadium Release Names" for reference. The current position in the sequence is always the last released tag — the next unused letter determines the next stadium name. diff --git a/docs/adr/0013-testing-strategy.md b/docs/adr/0013-testing-strategy.md new file mode 100644 index 0000000..176c431 --- /dev/null +++ b/docs/adr/0013-testing-strategy.md @@ -0,0 +1,49 @@ +# 0013. Testing Strategy + +Date: 2026-04-10 + +## Status + +Accepted + +## Context + +The project is a learning reference for a REST API built with ASP.NET Core. Its primary goal is educational clarity — it must be immediately understandable to developers studying the stack, not just functional in production. + +For a CRUD API of this size, a single suite of HTTP-layer integration tests (using `WebApplicationFactory` backed by an in-memory SQLite database) would cover the majority of meaningful risk. Such tests exercise the full request pipeline — routing, middleware, validation, serialization, and database interaction — with minimal setup. + +The question was whether to stop there or to also add unit tests for each layer (controller, service, validator, repository) separately. + +## Decision + +The project implements the full test pyramid: + +- **Unit tests** for each layer in isolation (controller, service, validator, repository), with dependencies replaced by Moq mocks or in-memory fakes. +- **Repository integration tests** that run EF Core queries against in-memory SQLite via the full migration chain, validating query correctness and migration health as a side effect. +- **HTTP integration tests** (`PlayerWebApplicationTests`) that send real HTTP requests through `WebApplicationFactory`, exercising the entire pipeline end-to-end. + +Some overlap between layers is accepted deliberately. + +## Rationale + +The redundancy is the point. Having all test types in one place allows a reader to see exactly what each layer tests, what it isolates, and what it leaves to the layer above or below. The test suite is as much documentation as it is a safety net. + +The repository integration tests in particular serve two purposes beyond what the HTTP tests provide: they validate individual EF Core queries in isolation (making failures easier to diagnose) and they run `MigrateAsync()` as a side effect, which acts as a canary for migration chain health. + +## Consequences + +### Positive + +- Every layer of the architecture has dedicated tests, making failure diagnosis straightforward. +- The test suite demonstrates the full range of testing patterns available in the .NET ecosystem. +- Migration health is continuously validated as a side effect of the repository tests. + +### Negative + +- Test count is higher than strictly necessary for confidence. +- Maintenance cost is proportionally higher — changes to shared fixtures or fakes may require updates across multiple test classes. + +### Neutral + +- The HTTP integration tests (`PlayerWebApplicationTests`) are the minimum viable test suite. All other test classes add depth, not breadth. +- The `409 Conflict` branch on `POST /players` is unreachable via the HTTP pipeline: the `BeUniqueSquadNumber` rule in the `"Create"` validation rule set catches duplicates first and returns `400`. This path is covered by the controller unit test, where validation is mocked to pass. diff --git a/docs/adr/0014-configurable-database-provider.md b/docs/adr/0014-configurable-database-provider.md new file mode 100644 index 0000000..8f0c8a5 --- /dev/null +++ b/docs/adr/0014-configurable-database-provider.md @@ -0,0 +1,87 @@ +# 0014. Configurable Database Provider + +Date: 2026-05-02 + +## Status + +Accepted — supersedes [ADR-0003](0003-use-sqlite-for-data-storage.md) + +## Context + +The project used SQLite as its only database engine (ADR-0003). This meant all environments — local development, Docker Compose, and any deployment — ran the same SQLite file-based database. For the majority of contributors this is ideal: zero infrastructure required. + +However, a fixed SQLite-only setup has limits: + +- SQLite does not support concurrent writes, so multi-instance deployments are not possible. +- Developers who want to validate production-parity behaviour (e.g. PostgreSQL-specific query plans, type semantics, or constraint handling) have no path to do so without modifying the project. +- A fixed SQLite-in-dev / PostgreSQL-in-prod split (a common alternative) introduces a different problem: subtle behavioral differences between environments that are hard to catch locally. + +The goal is to make the database engine fully configurable with a single environment variable, so the same stack is used consistently across every environment a developer chooses to run. + +## Decision + +We will introduce a `DATABASE_PROVIDER` environment variable that selects the database engine at startup: + +- **`DATABASE_PROVIDER=sqlite`** (default): SQLite everywhere. Zero infrastructure required. Works on any machine without Docker. Clone and run. +- **`DATABASE_PROVIDER=postgres`**: PostgreSQL everywhere. Requires Docker. Opt-in for developers who want a server-based engine or full production parity. + +The default is `sqlite` to keep the barrier to entry as low as possible. + +### Provider selection at startup + +`ServiceCollectionExtensions.AddDbContextPool` reads `DATABASE_PROVIDER` and wires the appropriate EF Core provider: + +```csharp +switch (provider.ToLowerInvariant()) +{ + case "postgres": + options.UseNpgsql(Environment.GetEnvironmentVariable("DATABASE_URL")); + break; + default: + options.UseSqlite($"Data Source={dataSource}"); + break; +} +``` + +### Provider-specific EF Core migrations + +SQLite and PostgreSQL require different column type mappings (`TEXT`/`INTEGER` vs `uuid`/`boolean`/`timestamp with time zone`). A single migration set cannot satisfy both providers. To resolve this: + +- SQLite migrations remain in `Migrations/` (namespace `...Migrations`). +- PostgreSQL migrations are placed in `Migrations/Npgsql/` (namespace `...Migrations.Npgsql`). +- A custom `ProviderSpecificMigrationsAssembly` (implementing EF Core's `IMigrationsAssembly`) filters the discovered migrations to only those in the active provider's namespace. It is registered via `options.ReplaceService()`. + +`MigrateAsync()` at startup continues to work for both providers; each sees only its own migration set. + +### Docker Compose profiles + +The `postgres` service is declared under the `postgres` Compose profile so it only starts when explicitly requested: + +```bash +# SQLite (default — no extra Docker service) +docker compose up + +# PostgreSQL (opt-in) +DATABASE_PROVIDER=postgres docker compose --profile postgres up +``` + +## Consequences + +### Positive + +- Developers choose their engine once and use it consistently across all environments. +- SQLite remains the zero-friction default — clone, run, done. +- PostgreSQL is a first-class option for production-parity testing without requiring project-wide changes. +- The Compose profile approach prevents accidental PostgreSQL containers from starting in SQLite mode. +- `dotnet run` continues to work unchanged with SQLite. + +### Negative + +- Two parallel migration sets must be maintained. Adding a schema change requires migrations in both `Migrations/` and `Migrations/Npgsql/`. +- `ProviderSpecificMigrationsAssembly` uses an EF Core internal API (`MigrationsAssembly` from `Microsoft.EntityFrameworkCore.Migrations.Internal`), annotated with `#pragma warning disable EF1001`. This may require updates on major EF Core version upgrades. +- PostgreSQL support is not tested in CI (no Testcontainers integration yet — tracked in issue #353). + +### Neutral + +- The `DATABASE_URL` connection string format follows the Npgsql convention (`Host=...;Database=...;Username=...;Password=...`). +- `.env.example` documents all three new environment variables (`DATABASE_PROVIDER`, `DATABASE_URL`, `POSTGRES_PASSWORD`). `.env` is git-ignored. diff --git a/docs/adr/0015-use-full-replace-put-no-patch.md b/docs/adr/0015-use-full-replace-put-no-patch.md new file mode 100644 index 0000000..09c91e8 --- /dev/null +++ b/docs/adr/0015-use-full-replace-put-no-patch.md @@ -0,0 +1,49 @@ +# 0015. Use Full-Replace PUT as the Partial Update Strategy + +Date: 2026-06-10 + +## Status + +Accepted + +## Context + +HTTP defines two methods for updating an existing resource: + +- **PUT** — full replacement; the client sends the complete resource + representation, and the server replaces the stored state entirely +- **PATCH** — partial update; the client sends only the changed fields + +Both are standard and well-understood. The choice affects API surface +complexity, client implementation requirements, and server-side +validation logic. + +## Decision + +We use PUT for all player update operations +(`PUT /players/squadNumber/{n}`). The request body must contain the +full player representation; the server replaces the stored resource +entirely. No PATCH endpoint is provided at this time. A PATCH +implementation is tracked in the project backlog and remains under +active consideration. + +## Consequences + +### Positive + +- Simpler server-side implementation: a single validation path, + no partial-update merge logic +- PUT semantics are idempotent and well-understood by API consumers +- Consistent with all sibling repos in the cross-language comparison set + +### Negative + +- Clients must send the full resource representation even for + single-field changes +- Fine-grained partial updates require a GET followed by a full PUT +- PATCH tracked in backlog — if implemented, this ADR will be superseded + +### Neutral + +- Standard REST semantics; no ambiguity about the update contract + for current consumers diff --git a/docs/adr/0016-ai-assisted-development-workflow.md b/docs/adr/0016-ai-assisted-development-workflow.md new file mode 100644 index 0000000..5f7c2fd --- /dev/null +++ b/docs/adr/0016-ai-assisted-development-workflow.md @@ -0,0 +1,76 @@ +# 0016. Adopt AI-Assisted Development Workflow + +Date: 2026-06-10 + +## Status + +Accepted + +## Context + +Software development has historically relied on three successive layers +of knowledge: + +1. **Official documentation** — authoritative but static; describes the + intended API but not how real projects apply it +2. **Community collaboration** — Stack Overflow, GitHub Discussions, blog + posts; describes how practitioners actually solve problems, but requires + the developer to synthesize and apply that knowledge themselves +3. **AI synthesis** — models trained on both layers above, capable of + applying idiomatic, stack-specific knowledge directly to a given + codebase + +Agentic AI tools represent a qualitative shift: instead of consulting +knowledge and applying it manually, the developer delegates implementation +to an agent that has absorbed the collective idioms of a stack — "the +Microsoft way", "the ASP.NET Core way" — and can apply them consistently. + +For a cross-language REST API comparison project, this matters +particularly: each implementation should reflect how an experienced +practitioner in that stack would structure the same problem, not a +generic approach that happens to compile. + +Prior to Claude Code, AI assistance was used ad-hoc — pasting code into +web interfaces (ChatGPT, DeepSeek) or via IDE-integrated assistants +(GitHub Copilot). Both approaches lack persistent codebase context and +the ability to act autonomously across a project. + +## Decision + +We adopt Claude Code as the primary development workflow tool for this +project. + +A `CLAUDE.md` file at the repository root serves as the workflow +specification: it documents architecture, coding conventions, invariants, +and explicit boundaries for autonomous operation — what the agent may do +freely, what requires human approval, and what must never be changed. + +CodeRabbit provides an additional automated code review layer +independent of the primary workflow. + +## Consequences + +### Positive + +- Stack-specific idioms are enforced by the agent's collective knowledge + rather than individual developer discipline +- `CLAUDE.md` is living architectural documentation: it must stay + accurate for the workflow to function, creating a natural incentive to + keep it current +- Explicit autonomy boundaries make human oversight intentional rather + than incidental + +### Negative + +- Token economics: long-running work may exceed context limits, requiring + active session management and continuation prompts to resume work + across sessions +- The global `~/.claude/CLAUDE.md` and per-repo `CLAUDE.md` must stay + aligned; drift between them produces inconsistent agent behavior + +### Neutral + +- Development workflow moves to the terminal/CLI rather than an IDE; + this has no impact on the codebase itself +- `CLAUDE.md` is specific to Claude Code; a different tool would require + a different workflow specification format diff --git a/docs/adr/0017-spec-driven-development.md b/docs/adr/0017-spec-driven-development.md new file mode 100644 index 0000000..0732f6e --- /dev/null +++ b/docs/adr/0017-spec-driven-development.md @@ -0,0 +1,61 @@ +# 0017. Adopt Spec-Driven Development (SDD) + +Date: 2026-06-10 + +## Status + +Accepted + +## Context + +In an agentic development workflow, it is easy to begin implementation +before requirements are fully understood. An AI assistant will produce +working code from a vague prompt — but working and correct are not the +same thing. Without a forcing function that requires requirements to be +stated explicitly before implementation begins, scope creep, rework, and +misaligned implementations are likely outcomes. + +GitHub Issues provide a natural spec artifact: persistent, linkable, +and — critically — they survive session boundaries. In a workflow where +context is lost between sessions, an Issue that captures the agreed +approach and acceptance criteria before implementation begins serves as +the durable contract between intent and implementation. + +## Decision + +All new features and non-trivial changes follow a three-step workflow: + +1. **Discuss** — describe the requirement in Claude Code Plan mode; + explore alternatives and consequences before committing to an approach +2. **Specify** — create a GitHub Issue as the spec artifact, capturing + the agreed approach, acceptance criteria, and any constraints +3. **Implement** — work against the Issue; commits reference it via + the `(#issue)` suffix in the Conventional Commits format + +Trivial changes (documentation updates, formatting fixes, dependency +bumps) may skip the Issue step at the developer's discretion. + +## Consequences + +### Positive + +- Requirements are stated explicitly before implementation; the agent + works against a written spec rather than an interpreted prompt +- Issues survive session boundaries — a new session can resume from + the Issue rather than reconstructing intent from scratch +- The implementation trail (Issue → branch → PR → commit) is traceable + without relying on session memory + +### Negative + +- Adds upfront overhead for changes that feel small; the Issue step can + seem bureaucratic for straightforward work +- The line between "spec-worthy" and "trivial" is judgment-dependent + and not enforced by tooling + +### Neutral + +- GitHub Issues double as the project backlog; SDD and backlog + management share the same artifact +- Plan mode discussion is ephemeral — not persisted outside the session; + the Issue is the only durable record of the pre-implementation reasoning diff --git a/docs/adr/README.md b/docs/adr/README.md new file mode 100644 index 0000000..13c9f7d --- /dev/null +++ b/docs/adr/README.md @@ -0,0 +1,67 @@ +# Architecture Decision Records + +This directory contains Architecture Decision Records (ADRs) for this project. ADRs document significant architectural decisions — what was decided, why, and what trade-offs were accepted. + +## Index + +| ADR | Title | Status | +|-----|-------|--------| +| [0001](0001-adopt-traditional-layered-architecture.md) | Adopt Traditional Layered Architecture | Accepted (Under Reconsideration) | +| [0002](0002-use-mvc-controllers-over-minimal-api.md) | Use MVC Controllers over Minimal API | Accepted | +| [0003](0003-use-sqlite-for-data-storage.md) | Use SQLite for Data Storage | Superseded by ADR-0014 | +| [0004](0004-use-uuid-as-database-primary-key.md) | Use UUID as Database Primary Key | Accepted | +| [0005](0005-use-squad-number-as-api-mutation-key.md) | Use Squad Number as API Mutation Key | Accepted | +| [0006](0006-use-rfc-7807-problem-details-for-errors.md) | Use RFC 7807 Problem Details for Errors | Accepted | +| [0007](0007-use-fluentvalidation-over-data-annotations.md) | Use FluentValidation over Data Annotations | Accepted | +| [0008](0008-use-automapper-for-dto-mapping.md) | Use AutoMapper for DTO Mapping | Accepted | +| [0009](0009-implement-in-memory-caching.md) | Implement In-Memory Caching | Accepted | +| [0010](0010-use-serilog-for-structured-logging.md) | Use Serilog for Structured Logging | Accepted | +| [0011](0011-use-docker-for-containerization.md) | Use Docker for Containerization | Accepted | +| [0012](0012-use-stadium-themed-semantic-versioning.md) | Use Stadium-Themed Semantic Versioning | Accepted | +| [0013](0013-testing-strategy.md) | Testing Strategy | Accepted | +| [0014](0014-configurable-database-provider.md) | Configurable Database Provider | Accepted | +| [0015](0015-use-full-replace-put-no-patch.md) | Use Full-Replace PUT as the Partial Update Strategy | Accepted | +| [0016](0016-ai-assisted-development-workflow.md) | Adopt AI-Assisted Development Workflow | Accepted | +| [0017](0017-spec-driven-development.md) | Adopt Spec-Driven Development (SDD) | Accepted | + +## When to Create an ADR + +Create an ADR when a decision affects: + +- Project structure or overall architecture +- Technology choices or dependencies +- API contracts or data model design +- Non-functional requirements (performance, security, scalability) +- Development workflows or processes + +## Template + +```markdown +# [NUMBER]. [TITLE] + +Date: YYYY-MM-DD + +## Status + +[Proposed | Accepted | Deprecated | Superseded by ADR-XXXX] + +## Context + +What is the issue we are facing? What forces are at play (technical, political, +social, project constraints)? Present facts neutrally without bias. + +## Decision + +We will [DECISION IN ACTIVE VOICE WITH FULL SENTENCES]. + +## Consequences + +### Positive +- Consequence 1 + +### Negative +- Trade-off 1 + +### Neutral +- Other effect 1 +``` diff --git a/global.json b/global.json new file mode 100644 index 0000000..512142d --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "10.0.100", + "rollForward": "latestFeature" + } +} diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh new file mode 100644 index 0000000..4a625fb --- /dev/null +++ b/scripts/entrypoint.sh @@ -0,0 +1,35 @@ +#!/bin/sh +set -e + +# Helper function for formatted logging +log() { + local message="$1" + echo "[ENTRYPOINT] $(date '+%Y/%m/%d - %H:%M:%S') | $message" + return 0 +} + +log "✔ Starting container..." + +DATABASE_PROVIDER=$(printf '%s' "${DATABASE_PROVIDER:-sqlite}" | tr '[:upper:]' '[:lower:]') + +if [ "$DATABASE_PROVIDER" = "postgres" ]; then + log "✔ Using PostgreSQL database." +else + VOLUME_STORAGE_PATH="${STORAGE_PATH:-/storage/players-sqlite3.db}" + + if [ ! -f "$VOLUME_STORAGE_PATH" ]; then + log "⚠️ No existing database file found in volume." + log "🗄️ EF Core migrations will initialize the database on first start." + else + log "✔ Existing database file found at $VOLUME_STORAGE_PATH." + fi +fi + +log "✔ Ready!" +log "🚀 Launching app..." +# Derive the API URL from ASPNETCORE_URLS (first entry if semicolon-separated), +# replacing the wildcard host (+, 0.0.0.0) with localhost for display purposes. +_raw_url=$(printf '%s' "${ASPNETCORE_URLS:-http://+:9000}" | cut -d';' -f1) +API_URL=$(printf '%s' "$_raw_url" | sed 's|+|localhost|g; s|0\.0\.0\.0|localhost|g') +log "🔌 API endpoints | $API_URL" +exec "$@" diff --git a/scripts/healthcheck.sh b/scripts/healthcheck.sh new file mode 100644 index 0000000..e3a9877 --- /dev/null +++ b/scripts/healthcheck.sh @@ -0,0 +1,5 @@ +#!/bin/sh +set -e + +# Minimal curl-based health check with timeout and error reporting +curl --fail --silent --show-error --connect-timeout 1 --max-time 2 http://localhost:9000/health diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Configurations/AuthorizeCheckOperationFilter.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Configurations/AuthorizeCheckOperationFilter.cs new file mode 100644 index 0000000..4d8895d --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Configurations/AuthorizeCheckOperationFilter.cs @@ -0,0 +1,46 @@ +using System.Linq; +using Microsoft.AspNetCore.Authorization; +using Microsoft.OpenApi; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Dotnet.Samples.AspNetCore.WebApi.Configurations +{ + /// + /// Adds the Bearer security requirement only to operations that have [Authorize] attributes. + /// + public class AuthorizeCheckOperationFilter : IOperationFilter + { + /// + /// Applies the Bearer security requirement to Swagger operations with [Authorize] attributes. + /// + /// The OpenAPI operation to modify. + /// The operation filter context containing method metadata. + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + // Check if [Authorize] is applied at the method or class level + var hasAuthorize = + context.MethodInfo.GetCustomAttributes(true).OfType().Any() + || context + .MethodInfo.DeclaringType?.GetCustomAttributes(true) + .OfType() + .Any() == true; + + // If there's no [Authorize] attribute, skip adding the security requirement + if (!hasAuthorize) + return; + + // Add security requirement (shows the lock icon) + // In Microsoft.OpenApi 2.x, use OpenApiSecuritySchemeReference instead of OpenApiSecurityScheme with nested Reference + if (operation is OpenApiOperation openApiOperation) + { + openApiOperation.Security ??= new List(); + openApiOperation.Security.Add( + new OpenApiSecurityRequirement + { + { new OpenApiSecuritySchemeReference("Bearer", null), new List() } + } + ); + } + } + } +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Configurations/RateLimiterConfiguration.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Configurations/RateLimiterConfiguration.cs new file mode 100644 index 0000000..a4443db --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Configurations/RateLimiterConfiguration.cs @@ -0,0 +1,25 @@ +namespace Dotnet.Samples.AspNetCore.WebApi.Configurations; + +/// +/// Configuration options for the Fixed Window Rate Limiter. +/// +public class RateLimiterConfiguration +{ + /// + /// Gets or sets the maximum number of permits that can be leased per window. + /// Default value is 60 requests. + /// + public int PermitLimit { get; set; } = 60; + + /// + /// Gets or sets the time window in seconds for rate limiting. + /// Default value is 60 seconds (1 minute). + /// + public int WindowSeconds { get; set; } = 60; + + /// + /// Gets or sets the maximum number of requests that can be queued when the permit limit is exceeded. + /// A value of 0 means no queuing (default). + /// + public int QueueLimit { get; set; } = 0; +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs new file mode 100644 index 0000000..fbfa010 --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs @@ -0,0 +1,295 @@ +using System.Net.Mime; +using Dotnet.Samples.AspNetCore.WebApi.Models; +using Dotnet.Samples.AspNetCore.WebApi.Services; +using FluentValidation; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Dotnet.Samples.AspNetCore.WebApi.Controllers; + +[ApiController] +[Route("players")] +[Produces("application/json")] +public class PlayerController( + IPlayerService playerService, + ILogger logger, + IValidator validator +) : ControllerBase +{ + private const string NotFoundTitle = "Not Found"; + + /* ------------------------------------------------------------------------- + * HTTP POST + * ---------------------------------------------------------------------- */ + + /// + /// Creates a new Player + /// + /// The PlayerRequestModel + /// Created + /// Conflict + /// Unprocessable Entity + [HttpPost(Name = "Create")] + [Consumes(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + public async Task PostAsync([FromBody] PlayerRequestModel player) + { + // Use the "Create" rule set, which includes BeUniqueSquadNumber. + var validation = await validator.ValidateAsync( + player, + options => options.IncludeRuleSets("Create") + ); + + if (!validation.IsValid) + { + var errors = validation + .Errors.GroupBy(e => e.PropertyName) + .ToDictionary(g => g.Key, g => g.Select(e => e.ErrorMessage).ToArray()); + + logger.LogWarning("POST /players validation failed: {@Errors}", errors); + return TypedResults.Problem( + new HttpValidationProblemDetails(errors) + { + Status = StatusCodes.Status422UnprocessableEntity, + Detail = "See the errors field for details.", + Instance = HttpContext?.Request?.Path.ToString(), + } + ); + } + + if (await playerService.RetrieveBySquadNumberAsync(player.SquadNumber) != null) + { + logger.LogWarning( + "POST /players failed: Player with Squad Number {SquadNumber} already exists", + player.SquadNumber + ); + return TypedResults.Conflict( + new ProblemDetails + { + Type = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409", + Title = "Conflict", + Status = StatusCodes.Status409Conflict, + Detail = $"Player with Squad Number '{player.SquadNumber}' already exists.", + Instance = HttpContext?.Request?.Path.ToString(), + } + ); + } + + var result = await playerService.CreateAsync(player); + + logger.LogInformation("POST /players created: {@Player}", result); + return TypedResults.CreatedAtRoute( + routeName: "RetrieveBySquadNumber", + routeValues: new { squadNumber = result.Dorsal }, + value: result + ); + } + + /* ------------------------------------------------------------------------- + * HTTP GET + * ---------------------------------------------------------------------- */ + + /// + /// Retrieves all Players + /// + /// OK + [HttpGet(Name = "Retrieve")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task GetAsync() + { + var players = await playerService.RetrieveAsync(); + + logger.LogInformation("GET /players retrieved {Count} player(s)", players.Count); + + return TypedResults.Ok(players); + } + + /// + /// Retrieves a Player by its internal Id (GUID) + /// + /// The internal Id (GUID) of the Player + /// OK + /// Not Found + [Authorize] + [HttpGet("{id:Guid}", Name = "RetrieveById")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetByIdAsync([FromRoute] Guid id) + { + var player = await playerService.RetrieveByIdAsync(id); + if (player != null) + { + logger.LogInformation("GET /players/{Id} retrieved: {@Player}", id, player); + return TypedResults.Ok(player); + } + else + { + logger.LogWarning("GET /players/{Id} not found", id); + return TypedResults.Problem( + statusCode: StatusCodes.Status404NotFound, + title: NotFoundTitle, + detail: $"Player with Id '{id}' was not found.", + instance: HttpContext?.Request?.Path.ToString() + ); + } + } + + /// + /// Retrieves a Player by its Squad Number + /// + /// The Squad Number of the Player + /// OK + /// Not Found + [HttpGet("squadNumber/{squadNumber:int}", Name = "RetrieveBySquadNumber")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetBySquadNumberAsync([FromRoute] int squadNumber) + { + var player = await playerService.RetrieveBySquadNumberAsync(squadNumber); + if (player != null) + { + logger.LogInformation( + "GET /players/squadNumber/{SquadNumber} retrieved: {@Player}", + squadNumber, + player + ); + return TypedResults.Ok(player); + } + else + { + logger.LogWarning("GET /players/squadNumber/{SquadNumber} not found", squadNumber); + return TypedResults.Problem( + statusCode: StatusCodes.Status404NotFound, + title: NotFoundTitle, + detail: $"Player with Squad Number '{squadNumber}' was not found.", + instance: HttpContext?.Request?.Path.ToString() + ); + } + } + + /* ------------------------------------------------------------------------- + * HTTP PUT + * ---------------------------------------------------------------------- */ + + /// + /// Updates (entirely) a Player by its Squad Number + /// + /// + /// The PlayerRequestModel + /// The Squad Number of the Player + /// No Content + /// Bad Request (route/body squad number mismatch) + /// Not Found + /// Unprocessable Entity (field validation failure) + [HttpPut("squadNumber/{squadNumber:int}", Name = "Update")] + [Consumes(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + public async Task PutAsync( + [FromRoute] int squadNumber, + [FromBody] PlayerRequestModel player + ) + { + // Use the "Update" rule set, which omits BeUniqueSquadNumber. + // The player being updated already exists in the database, so a + // uniqueness check on its own squad number would always fail. + var validation = await validator.ValidateAsync( + player, + options => options.IncludeRuleSets("Update") + ); + if (!validation.IsValid) + { + var errors = validation + .Errors.GroupBy(e => e.PropertyName) + .ToDictionary(g => g.Key, g => g.Select(e => e.ErrorMessage).ToArray()); + + logger.LogWarning( + "PUT /players/squadNumber/{SquadNumber} validation failed: {@Errors}", + squadNumber, + errors + ); + return TypedResults.Problem( + new HttpValidationProblemDetails(errors) + { + Status = StatusCodes.Status422UnprocessableEntity, + Detail = "See the errors field for details.", + Instance = HttpContext?.Request?.Path.ToString(), + } + ); + } + if (player.SquadNumber != squadNumber) + { + logger.LogWarning( + "PutAsync squad number mismatch: route {SquadNumber} != body {PlayerSquadNumber}", + squadNumber, + player.SquadNumber + ); + return TypedResults.Problem( + statusCode: StatusCodes.Status400BadRequest, + title: "Bad Request", + detail: "Squad number in the route does not match squad number in the request body.", + instance: HttpContext?.Request?.Path.ToString() + ); + } + if (await playerService.RetrieveBySquadNumberAsync(squadNumber) == null) + { + logger.LogWarning("PUT /players/squadNumber/{SquadNumber} not found", squadNumber); + return TypedResults.Problem( + statusCode: StatusCodes.Status404NotFound, + title: NotFoundTitle, + detail: $"Player with Squad Number '{squadNumber}' was not found.", + instance: HttpContext?.Request?.Path.ToString() + ); + } + await playerService.UpdateAsync(player); + // Sanitize user-provided player data before logging to prevent log forging + var sanitizedPlayerString = player + .ToString() + ?.Replace(Environment.NewLine, string.Empty) + .Replace("\r", string.Empty) + .Replace("\n", string.Empty); + logger.LogInformation( + "PUT /players/squadNumber/{SquadNumber} updated: {Player}", + squadNumber, + sanitizedPlayerString + ); + return TypedResults.NoContent(); + } + + /* ------------------------------------------------------------------------- + * HTTP DELETE + * ---------------------------------------------------------------------- */ + + /// + /// Deletes a Player by its Squad Number + /// + /// The Squad Number of the Player + /// No Content + /// Not Found + [HttpDelete("squadNumber/{squadNumber:int}", Name = "Delete")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeleteAsync([FromRoute] int squadNumber) + { + if (await playerService.RetrieveBySquadNumberAsync(squadNumber) == null) + { + logger.LogWarning("DELETE /players/squadNumber/{SquadNumber} not found", squadNumber); + return TypedResults.Problem( + statusCode: StatusCodes.Status404NotFound, + title: "Not Found", + detail: $"Player with Squad Number '{squadNumber}' was not found.", + instance: HttpContext?.Request?.Path.ToString() + ); + } + else + { + await playerService.DeleteAsync(squadNumber); + logger.LogInformation("DELETE /players/squadNumber/{SquadNumber} deleted", squadNumber); + return TypedResults.NoContent(); + } + } +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Data/PlayerDbContext.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Data/PlayerDbContext.cs new file mode 100644 index 0000000..15d5c0d --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Data/PlayerDbContext.cs @@ -0,0 +1,41 @@ +using Dotnet.Samples.AspNetCore.WebApi.Models; +using Dotnet.Samples.AspNetCore.WebApi.Utilities; +using Microsoft.EntityFrameworkCore; + +namespace Dotnet.Samples.AspNetCore.WebApi.Data; + +/// +/// Represents the EF Core database context for a Player entity. +/// Inherits from and provides a bridge between the entity and the database. +/// +public class PlayerDbContext(DbContextOptions options) : DbContext(options) +{ + /// + /// Gets or sets the representing a collection of Player entities. + /// corresponds to a table in the database, allowing CRUD operations and LINQ queries. + /// + public DbSet Players => Set(); + + /// + /// Configures the model for the Player entity. + /// This method is called by the runtime to configure the model for the context. + /// + /// The model builder. + /// + /// This method is used to configure the model and relationships using the Fluent API. + /// It is called when the model for a derived context is being created. + /// + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity(entity => + { + 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()); + }); + } +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Dotnet.Samples.AspNetCore.WebApi.csproj b/src/Dotnet.Samples.AspNetCore.WebApi/Dotnet.Samples.AspNetCore.WebApi.csproj new file mode 100644 index 0000000..31dfef4 --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Dotnet.Samples.AspNetCore.WebApi.csproj @@ -0,0 +1,38 @@ + + + + net10.0 + enable + enable + true + + + + + + + + + + + + + + + + + + + + + + + true + $(NoWarn);1591 + + + + en-US + + + diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Enums/Enumeration.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Enums/Enumeration.cs new file mode 100644 index 0000000..c563d0a --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Enums/Enumeration.cs @@ -0,0 +1,42 @@ +using System.Reflection; + +namespace Dotnet.Samples.AspNetCore.WebApi.Enums; + +/// +/// Enumeration class +/// https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/enumeration-classes-over-enum-types +/// +public abstract class Enumeration +{ + public int Id { get; private set; } + + public string Text { get; private set; } + + protected Enumeration(int id, string name) => (Id, Text) = (id, name); + + public override string ToString() => Text; + + public static IEnumerable GetAll() + where T : Enumeration => + typeof(T) + .GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) + .Select(f => f.GetValue(null)) + .Cast(); + + public override bool Equals(object? obj) + { + if (obj is not Enumeration enumeration) + { + return false; + } + + var areSameType = GetType().Equals(obj.GetType()); + var areSameValue = Id.Equals(enumeration.Id); + + return areSameType && areSameValue; + } + + public override int GetHashCode() => Id.GetHashCode(); + + public int CompareTo(object other) => Id.CompareTo(((Enumeration)other).Id); +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Enums/Position.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Enums/Position.cs new file mode 100644 index 0000000..7c6be66 --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Enums/Position.cs @@ -0,0 +1,63 @@ +namespace Dotnet.Samples.AspNetCore.WebApi.Enums; + +public class Position : Enumeration +{ + public string Abbr { get; private set; } + + public static readonly Position Goalkeeper = new(1, "Goalkeeper", "GK"); + public static readonly Position RightBack = new(2, "Right-Back", "RB"); + public static readonly Position LeftBack = new(3, "Left-Back", "LB"); + public static readonly Position CentreBack = new(4, "Centre-Back", "CB"); + public static readonly Position DefensiveMidfield = new(5, "Defensive Midfield", "DM"); + public static readonly Position CentralMidfield = new(6, "Central Midfield", "CM"); + public static readonly Position RightWinger = new(7, "Right Winger", "RW"); + public static readonly Position AttackingMidfield = new(8, "Attacking Midfield", "AM"); + public static readonly Position CentreForward = new(9, "Centre-Forward", "CF"); + public static readonly Position SecondStriker = new(10, "Second Striker", "SS"); + public static readonly Position LeftWinger = new(11, "Left Winger", "LW"); + + private Position(int id, string name, string abbr) + : base(id, name) => Abbr = abbr; + + /// + /// Returns a Position object based on the abbreviation. + /// + /// + /// This method searches through all the Position objects and returns the one + /// that matches the provided abbreviation. If no match is found, it returns null. + /// + /// The abbreviation of the Position. + /// + /// A Position object if found; otherwise, null. + /// + public static Position? FromAbbr(string abbr) => + GetAll().FirstOrDefault(position => position.Abbr == abbr); + + /// + /// Returns a Position object based on the ID. + /// + /// + /// This method searches through all the Position objects and returns the one + /// that matches the provided ID. If no match is found, it returns null. + /// + /// The ID of the Position. + /// + /// A Position object if found; otherwise, null. + /// + public static Position? FromId(int id) => + GetAll().FirstOrDefault(position => position.Id == id); + + /// + /// Checks if the provided abbreviation is valid. + /// + /// + /// This method checks if the provided abbreviation is not null or empty and + /// if it corresponds to a valid Position object. + /// + /// The abbreviation to check. + /// + /// True if the abbreviation is valid; otherwise, false. + /// + public static bool IsValidAbbr(string? abbr) => + !string.IsNullOrWhiteSpace(abbr) && FromAbbr(abbr!) is not null; +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Extensions/MiddlewareExtensions.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Extensions/MiddlewareExtensions.cs new file mode 100644 index 0000000..e303545 --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Extensions/MiddlewareExtensions.cs @@ -0,0 +1,21 @@ +using Dotnet.Samples.AspNetCore.WebApi.Middlewares; + +namespace Dotnet.Samples.AspNetCore.WebApi.Extensions; + +/// +/// Extension methods for configuring middleware in the application pipeline. +/// +public static class MiddlewareExtensions +{ + /// + /// Adds global exception handling middleware to the application pipeline. + /// This middleware catches unhandled exceptions and returns RFC 7807 compliant error responses. + /// + /// The web application used to configure the HTTP pipeline, and routes. + /// The WebApplication object for method chaining. + public static WebApplication UseExceptionHandling(this WebApplication app) + { + app.UseMiddleware(); + return app; + } +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Extensions/ServiceCollectionExtensions.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..52fc241 --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,255 @@ +using System.Threading.RateLimiting; +using Dotnet.Samples.AspNetCore.WebApi.Configurations; +using Dotnet.Samples.AspNetCore.WebApi.Data; +using Dotnet.Samples.AspNetCore.WebApi.Mappings; +using Dotnet.Samples.AspNetCore.WebApi.Migrations; +using Dotnet.Samples.AspNetCore.WebApi.Repositories; +using Dotnet.Samples.AspNetCore.WebApi.Services; +using Dotnet.Samples.AspNetCore.WebApi.Utilities; +using Dotnet.Samples.AspNetCore.WebApi.Validators; +using FluentValidation; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.OpenApi; +using Serilog; + +namespace Dotnet.Samples.AspNetCore.WebApi.Extensions; + +/// +/// Extension methods for IServiceCollection to encapsulate service configuration. +/// +public static partial class ServiceCollectionExtensions +{ + /// + /// Adds PlayerDbContext, selecting the database provider based on the + /// DATABASE_PROVIDER environment variable (sqlite by default, postgres + /// to opt in to PostgreSQL). + /// + /// The IServiceCollection instance. + /// The web host environment. + /// The IServiceCollection for method chaining. + public static IServiceCollection AddPlayerDbContext( + this IServiceCollection services, + IWebHostEnvironment environment + ) + { + services.AddDbContextPool(options => + { + var provider = (Environment.GetEnvironmentVariable("DATABASE_PROVIDER") ?? "") + .Trim() + .ToLowerInvariant(); + + switch (provider) + { + case "postgres": + var connectionString = Environment.GetEnvironmentVariable("DATABASE_URL"); + if (string.IsNullOrWhiteSpace(connectionString)) + throw new InvalidOperationException( + "DATABASE_URL is required when DATABASE_PROVIDER=postgres." + ); + options.UseNpgsql(connectionString, npgsql => npgsql.EnableRetryOnFailure()); + // Hand-crafted designer files cannot replicate Npgsql-injected runtime + // annotations (Relational:MaxIdentifierLength, UseIdentityByDefaultColumn), + // causing a false-positive PendingModelChangesWarning. Suppressed here; + // BuildTargetModel is still populated so InsertData SQL generation works. + options.ConfigureWarnings(w => + w.Ignore(RelationalEventId.PendingModelChangesWarning) + ); + break; + + case "sqlite": + case "": + 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}"); + break; + + default: + throw new InvalidOperationException( + $"Unsupported DATABASE_PROVIDER value: '{provider}'. " + + "Valid values are 'sqlite' (default) and 'postgres'." + ); + } + + if (environment.IsDevelopment()) + { + options.EnableSensitiveDataLogging(); + options.LogTo(Log.Logger.Information, LogLevel.Information); + } + + options.ReplaceService(); + }); + + return services; + } + + /// + /// Adds a default CORS policy that allows any origin, method, and header, + /// restricted to the Development environment. + ///
+ /// + ///
+ /// + /// The permissive wildcard policy (AllowAnyOrigin, AllowAnyMethod, AllowAnyHeader) + /// is intentional for local development, where Swagger UI and local frontends + /// need unrestricted cross-origin access. No CORS policy is registered in + /// Production or other environments, where the API is assumed to be consumed + /// server-to-server or to sit behind a reverse proxy on the same origin, making + /// CORS irrelevant. If a production frontend on a different domain is ever added, + /// replace this with a restrictive named policy that enumerates specific allowed + /// origins instead of using a wildcard. + /// + /// The IServiceCollection instance. + /// The web host environment. + /// The IServiceCollection for method chaining. + public static IServiceCollection AddCorsDefaultPolicy( + this IServiceCollection services, + IWebHostEnvironment environment + ) + { + if (environment.IsDevelopment()) + { + services.AddCors(options => + { + options.AddDefaultPolicy(corsBuilder => + { + corsBuilder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader(); + }); + }); + } + + // No CORS configured in Production or other environments + + return services; + } + + /// + /// Adds FluentValidation validators for Player models. + ///
+ /// + ///
+ /// The IServiceCollection instance. + /// The IServiceCollection for method chaining. + public static IServiceCollection AddValidators(this IServiceCollection services) + { + services.AddValidatorsFromAssemblyContaining(); + return services; + } + + /// + /// Sets up Swagger documentation generation and UI for the API. + ///
+ /// + ///
+ /// The IServiceCollection instance. + /// The application configuration. + /// The IServiceCollection for method chaining. + public static IServiceCollection AddSwaggerConfiguration( + this IServiceCollection services, + IConfiguration configuration + ) + { + services.AddSwaggerGen(options => + { + var openApiInfo = configuration.GetSection("OpenApiInfo").Get(); + + options.SwaggerDoc("v1", openApiInfo); + options.IncludeXmlComments(SwaggerUtilities.ConfigureXmlCommentsFilePath()); + options.AddSecurityDefinition("Bearer", SwaggerUtilities.ConfigureSecurityDefinition()); + options.OperationFilter(); + }); + + return services; + } + + /// + /// Registers the PlayerService with the DI container. + ///
+ /// + ///
+ /// The IServiceCollection instance. + /// The IServiceCollection for method chaining. + public static IServiceCollection RegisterPlayerService(this IServiceCollection services) + { + services.AddScoped(); + return services; + } + + /// + /// Adds AutoMapper configuration for Player mappings. + ///
+ /// + ///
+ /// The IServiceCollection instance. + /// The IServiceCollection for method chaining. + public static IServiceCollection AddMappings(this IServiceCollection services) + { + services.AddAutoMapper(config => config.AddProfile()); + return services; + } + + /// + /// Registers the PlayerRepository service with the DI container. + ///
+ /// + ///
+ /// The IServiceCollection instance. + /// The IServiceCollection for method chaining. + public static IServiceCollection RegisterPlayerRepository(this IServiceCollection services) + { + services.AddScoped(); + return services; + } + + /// + /// Adds rate limiting configuration with IP-based partitioning. + ///
+ /// + ///
+ /// The IServiceCollection instance. + /// The application configuration instance. + /// The IServiceCollection for method chaining. + public static IServiceCollection AddFixedWindowRateLimiter( + this IServiceCollection services, + IConfiguration configuration + ) + { + var settings = + configuration.GetSection("RateLimiter").Get() + ?? new RateLimiterConfiguration(); + + services.AddRateLimiter(options => + { + options.GlobalLimiter = PartitionedRateLimiter.Create( + httpContext => + { + var partitionKey = HttpContextUtilities.ExtractIpAddress(httpContext); + + return RateLimitPartition.GetFixedWindowLimiter( + partitionKey: partitionKey, + factory: _ => new FixedWindowRateLimiterOptions + { + PermitLimit = settings.PermitLimit, + Window = TimeSpan.FromSeconds(settings.WindowSeconds), + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + QueueLimit = settings.QueueLimit + } + ); + } + ); + + options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; + }); + + return services; + } +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Mappings/PlayerMappingProfile.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Mappings/PlayerMappingProfile.cs new file mode 100644 index 0000000..f698d03 --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Mappings/PlayerMappingProfile.cs @@ -0,0 +1,61 @@ +using AutoMapper; +using Dotnet.Samples.AspNetCore.WebApi.Enums; +using Dotnet.Samples.AspNetCore.WebApi.Models; + +namespace Dotnet.Samples.AspNetCore.WebApi.Mappings; + +/// +/// Mapping profile for Player. +/// +/// +/// This class defines the mapping configuration between PlayerRequestModel and Player, +/// and between Player and PlayerResponseModel. +/// +public class PlayerMappingProfile : Profile +{ + public PlayerMappingProfile() + { + // PlayerRequestModel → Player + CreateMap() + .ForMember( + destination => destination.Position, + options => + options.MapFrom(source => + Position.FromAbbr(source.AbbrPosition ?? string.Empty) + ) + ) + .ForMember(destination => destination.Starting11, options => options.Ignore()); + + // Player → PlayerResponseModel + CreateMap() + .ForSourceMember(source => source.Id, options => options.DoNotValidate()) + .ForMember( + destination => destination.FullName, + options => + options.MapFrom(source => + $"{source.FirstName} {(string.IsNullOrWhiteSpace(source.MiddleName) ? "" : source.MiddleName + " ")}{source.LastName}".Trim() + ) + ) + .ForMember( + destination => destination.Birth, + options => + options.MapFrom(source => + source.DateOfBirth.HasValue + ? $"{source.DateOfBirth.Value:MMMM d, yyyy}" + : null + ) + ) + .ForMember( + destination => destination.Dorsal, + options => options.MapFrom(source => source.SquadNumber) + ) + .ForMember( + destination => destination.Club, + options => options.MapFrom(source => source.Team) + ) + .ForMember( + destination => destination.Starting11, + options => options.MapFrom(source => source.Starting11 ? "Yes" : "No") + ); + } +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Middlewares/ExceptionMiddleware.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Middlewares/ExceptionMiddleware.cs new file mode 100644 index 0000000..8b907c7 --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Middlewares/ExceptionMiddleware.cs @@ -0,0 +1,121 @@ +using System.Text.Json; +using FluentValidation; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Dotnet.Samples.AspNetCore.WebApi.Middlewares; + +/// +/// Middleware for global exception handling with RFC 7807 Problem Details format. +/// +public class ExceptionMiddleware( + RequestDelegate next, + ILogger logger, + IHostEnvironment environment +) +{ + private const string ProblemDetailsContentType = "application/problem+json"; + + private static readonly JsonSerializerOptions JsonOptions = + new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + + /// + /// Invokes the middleware to handle exceptions globally. + /// + public async Task InvokeAsync(HttpContext context) + { + try + { + await next(context); + } + catch (Exception exception) + { + await HandleExceptionAsync(context, exception); + } + } + + /// + /// Handles the exception and returns an RFC 7807 compliant error response. + /// + private async Task HandleExceptionAsync(HttpContext context, Exception exception) + { + var (status, title) = MapExceptionToStatusCode(exception); + + var problemDetails = new ProblemDetails + { + Type = $"https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/{status}", + Title = title, + Status = status, + Detail = GetExceptionDetail(exception), + Instance = context.Request.Path + }; + + // Add trace ID for request correlation + problemDetails.Extensions["traceId"] = context.TraceIdentifier; + + // codeql[cs/log-forging] Serilog structured logging automatically escapes control characters + logger.LogError( + exception, + "Unhandled exception occurred. TraceId: {TraceId}, Path: {Path}, StatusCode: {StatusCode}", + context.TraceIdentifier, + context.Request.Path, + status + ); + + // Only modify response if headers haven't been sent yet + if (!context.Response.HasStarted) + { + context.Response.StatusCode = status; + context.Response.ContentType = ProblemDetailsContentType; + + await context.Response.WriteAsync( + JsonSerializer.Serialize(problemDetails, JsonOptions) + ); + } + else + { + logger.LogWarning( + "Unable to write error response for TraceId: {TraceId}. Response has already started.", + context.TraceIdentifier + ); + } + } + + /// + /// Maps exception types to appropriate HTTP status codes and titles. + /// + private static (int StatusCode, string Title) MapExceptionToStatusCode(Exception exception) + { + return exception switch + { + ValidationException => (StatusCodes.Status422UnprocessableEntity, "Validation Error"), + ArgumentException + or ArgumentNullException + => (StatusCodes.Status400BadRequest, "Bad Request"), + InvalidOperationException => (StatusCodes.Status400BadRequest, "Invalid Operation"), + DbUpdateConcurrencyException => (StatusCodes.Status409Conflict, "Concurrency Conflict"), + OperationCanceledException => (StatusCodes.Status408RequestTimeout, "Request Timeout"), + _ => (StatusCodes.Status500InternalServerError, "Internal Server Error") + }; + } + + /// + /// Gets the exception detail based on the environment. + /// In Development: includes full exception details and stack trace. + /// In Production: returns a generic message without sensitive information. + /// + private string GetExceptionDetail(Exception exception) + { + if (environment.IsDevelopment()) + { + return $"{exception.Message}\n\nStack Trace:\n{exception.StackTrace}"; + } + + return exception switch + { + ValidationException => exception.Message, + ArgumentException => exception.Message, + _ => "An unexpected error occurred while processing your request." + }; + } +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141647_InitialCreate.Designer.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141647_InitialCreate.Designer.cs new file mode 100644 index 0000000..e237b15 --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141647_InitialCreate.Designer.cs @@ -0,0 +1,69 @@ +// +using System; +using Dotnet.Samples.AspNetCore.WebApi.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Dotnet.Samples.AspNetCore.WebApi.Migrations +{ + [DbContext(typeof(PlayerDbContext))] + [Migration("20260409141647_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.5"); + + modelBuilder.Entity("Dotnet.Samples.AspNetCore.WebApi.Models.Player", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AbbrPosition") + .HasColumnType("TEXT"); + + b.Property("DateOfBirth") + .HasColumnType("TEXT"); + + b.Property("FirstName") + .HasColumnType("TEXT"); + + b.Property("LastName") + .HasColumnType("TEXT"); + + b.Property("League") + .HasColumnType("TEXT"); + + b.Property("MiddleName") + .HasColumnType("TEXT"); + + b.Property("Position") + .HasColumnType("TEXT"); + + b.Property("SquadNumber") + .HasColumnType("INTEGER"); + + b.Property("Starting11") + .HasColumnType("INTEGER"); + + b.Property("Team") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SquadNumber") + .IsUnique(); + + b.ToTable("Players"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141647_InitialCreate.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141647_InitialCreate.cs new file mode 100644 index 0000000..d5eeabc --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141647_InitialCreate.cs @@ -0,0 +1,50 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Dotnet.Samples.AspNetCore.WebApi.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Players", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + FirstName = table.Column(type: "TEXT", nullable: true), + MiddleName = table.Column(type: "TEXT", nullable: true), + LastName = table.Column(type: "TEXT", nullable: true), + DateOfBirth = table.Column(type: "TEXT", nullable: true), + SquadNumber = table.Column(type: "INTEGER", nullable: false), + Position = table.Column(type: "TEXT", nullable: true), + AbbrPosition = table.Column(type: "TEXT", nullable: true), + Team = table.Column(type: "TEXT", nullable: true), + League = table.Column(type: "TEXT", nullable: true), + Starting11 = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Players", x => x.Id); + } + ); + + migrationBuilder.CreateIndex( + name: "IX_Players_SquadNumber", + table: "Players", + column: "SquadNumber", + unique: true + ); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable(name: "Players"); + } + } +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141707_SeedStarting11.Designer.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141707_SeedStarting11.Designer.cs new file mode 100644 index 0000000..96b96d7 --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141707_SeedStarting11.Designer.cs @@ -0,0 +1,222 @@ +// +using System; +using Dotnet.Samples.AspNetCore.WebApi.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Dotnet.Samples.AspNetCore.WebApi.Migrations +{ + [DbContext(typeof(PlayerDbContext))] + [Migration("20260409141707_SeedStarting11")] + partial class SeedStarting11 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.5"); + + modelBuilder.Entity("Dotnet.Samples.AspNetCore.WebApi.Models.Player", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AbbrPosition") + .HasColumnType("TEXT"); + + b.Property("DateOfBirth") + .HasColumnType("TEXT"); + + b.Property("FirstName") + .HasColumnType("TEXT"); + + b.Property("LastName") + .HasColumnType("TEXT"); + + b.Property("League") + .HasColumnType("TEXT"); + + b.Property("MiddleName") + .HasColumnType("TEXT"); + + b.Property("Position") + .HasColumnType("TEXT"); + + b.Property("SquadNumber") + .HasColumnType("INTEGER"); + + b.Property("Starting11") + .HasColumnType("INTEGER"); + + b.Property("Team") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SquadNumber") + .IsUnique(); + + b.ToTable("Players"); + + b.HasData( + new + { + Id = new Guid("01772c59-43f0-5d85-b913-c78e4e281452"), + AbbrPosition = "GK", + DateOfBirth = new DateTime(1992, 9, 1, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Damián", + LastName = "Martínez", + League = "Premier League", + MiddleName = "Emiliano", + Position = "Goalkeeper", + SquadNumber = 23, + Starting11 = true, + Team = "Aston Villa FC" + }, + new + { + Id = new Guid("da31293b-4c7e-5e0f-a168-469ee29ecbc4"), + AbbrPosition = "RB", + DateOfBirth = new DateTime(1998, 4, 5, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Nahuel", + LastName = "Molina", + League = "La Liga", + Position = "Right-Back", + SquadNumber = 26, + Starting11 = true, + Team = "Altético Madrid" + }, + new + { + Id = new Guid("c096c69e-762b-5281-9290-bb9c167a24a0"), + AbbrPosition = "CB", + DateOfBirth = new DateTime(1998, 4, 26, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Cristian", + LastName = "Romero", + League = "Premier League", + MiddleName = "Gabriel", + Position = "Centre-Back", + SquadNumber = 13, + Starting11 = true, + Team = "Tottenham Hotspur" + }, + new + { + Id = new Guid("d5f7dd7a-1dcb-5960-ba27-e34865b63358"), + AbbrPosition = "CB", + DateOfBirth = new DateTime(1988, 2, 11, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Nicolás", + LastName = "Otamendi", + League = "Liga Portugal", + MiddleName = "Hernán Gonzalo", + Position = "Centre-Back", + SquadNumber = 19, + Starting11 = true, + Team = "SL Benfica" + }, + new + { + Id = new Guid("2f6f90a0-9b9d-5023-96d2-a2aaf03143a6"), + AbbrPosition = "LB", + DateOfBirth = new DateTime(1992, 8, 30, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Nicolás", + LastName = "Tagliafico", + League = "Ligue 1", + MiddleName = "Alejandro", + Position = "Left-Back", + SquadNumber = 3, + Starting11 = true, + Team = "Olympique Lyon" + }, + new + { + Id = new Guid("b5b46e79-929e-5ed2-949d-0d167109c022"), + AbbrPosition = "RW", + DateOfBirth = new DateTime(1988, 2, 13, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Ángel", + LastName = "Di María", + League = "Liga Portugal", + MiddleName = "Fabián", + Position = "Right Winger", + SquadNumber = 11, + Starting11 = true, + Team = "SL Benfica" + }, + new + { + Id = new Guid("0293b282-1da8-562e-998e-83849b417a42"), + AbbrPosition = "CM", + DateOfBirth = new DateTime(1994, 5, 23, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Rodrigo", + LastName = "de Paul", + League = "La Liga", + MiddleName = "Javier", + Position = "Central Midfield", + SquadNumber = 7, + Starting11 = true, + Team = "Altético Madrid" + }, + new + { + Id = new Guid("d3ba552a-dac3-588a-b961-1ea7224017fd"), + AbbrPosition = "CM", + DateOfBirth = new DateTime(2001, 1, 16, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Enzo", + LastName = "Fernández", + League = "Liga Portugal", + MiddleName = "Jeremías", + Position = "Central Midfield", + SquadNumber = 24, + Starting11 = true, + Team = "SL Benfica" + }, + new + { + Id = new Guid("9613cae9-16ab-5b54-937e-3135123b9e0d"), + AbbrPosition = "CM", + DateOfBirth = new DateTime(1998, 12, 23, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Alexis", + LastName = "Mac Allister", + League = "Premier League", + Position = "Central Midfield", + SquadNumber = 20, + Starting11 = true, + Team = "Brighton & Hove Albion" + }, + new + { + Id = new Guid("acc433bf-d505-51fe-831e-45eb44c4d43c"), + AbbrPosition = "RW", + DateOfBirth = new DateTime(1987, 6, 23, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Lionel", + LastName = "Messi", + League = "Ligue 1", + MiddleName = "Andrés", + Position = "Right Winger", + SquadNumber = 10, + Starting11 = true, + Team = "Paris Saint-Germain" + }, + new + { + Id = new Guid("38bae91d-8519-55a2-b30a-b9fe38849bfb"), + AbbrPosition = "CF", + DateOfBirth = new DateTime(2000, 1, 30, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Julián", + LastName = "Álvarez", + League = "Premier League", + Position = "Centre-Forward", + SquadNumber = 9, + Starting11 = true, + Team = "Manchester City" + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141707_SeedStarting11.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141707_SeedStarting11.cs new file mode 100644 index 0000000..d466914 --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141707_SeedStarting11.cs @@ -0,0 +1,251 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace Dotnet.Samples.AspNetCore.WebApi.Migrations +{ + /// + public partial class SeedStarting11 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.InsertData( + table: "Players", + columns: new[] + { + "Id", + "AbbrPosition", + "DateOfBirth", + "FirstName", + "LastName", + "League", + "MiddleName", + "Position", + "SquadNumber", + "Starting11", + "Team" + }, + values: new object[,] + { + { + new Guid("01772c59-43f0-5d85-b913-c78e4e281452"), + "GK", + new DateTime(1992, 9, 1, 0, 0, 0, 0, DateTimeKind.Utc), + "Damián", + "Martínez", + "Premier League", + "Emiliano", + "Goalkeeper", + 23, + true, + "Aston Villa FC" + }, + { + new Guid("0293b282-1da8-562e-998e-83849b417a42"), + "CM", + new DateTime(1994, 5, 23, 0, 0, 0, 0, DateTimeKind.Utc), + "Rodrigo", + "de Paul", + "La Liga", + "Javier", + "Central Midfield", + 7, + true, + "Altético Madrid" + }, + { + new Guid("2f6f90a0-9b9d-5023-96d2-a2aaf03143a6"), + "LB", + new DateTime(1992, 8, 30, 0, 0, 0, 0, DateTimeKind.Utc), + "Nicolás", + "Tagliafico", + "Ligue 1", + "Alejandro", + "Left-Back", + 3, + true, + "Olympique Lyon" + }, + { + new Guid("38bae91d-8519-55a2-b30a-b9fe38849bfb"), + "CF", + new DateTime(2000, 1, 30, 0, 0, 0, 0, DateTimeKind.Utc), + "Julián", + "Álvarez", + "Premier League", + null, + "Centre-Forward", + 9, + true, + "Manchester City" + }, + { + new Guid("9613cae9-16ab-5b54-937e-3135123b9e0d"), + "CM", + new DateTime(1998, 12, 23, 0, 0, 0, 0, DateTimeKind.Utc), + "Alexis", + "Mac Allister", + "Premier League", + null, + "Central Midfield", + 20, + true, + "Brighton & Hove Albion" + }, + { + new Guid("acc433bf-d505-51fe-831e-45eb44c4d43c"), + "RW", + new DateTime(1987, 6, 23, 0, 0, 0, 0, DateTimeKind.Utc), + "Lionel", + "Messi", + "Ligue 1", + "Andrés", + "Right Winger", + 10, + true, + "Paris Saint-Germain" + }, + { + new Guid("b5b46e79-929e-5ed2-949d-0d167109c022"), + "RW", + new DateTime(1988, 2, 13, 0, 0, 0, 0, DateTimeKind.Utc), + "Ángel", + "Di María", + "Liga Portugal", + "Fabián", + "Right Winger", + 11, + true, + "SL Benfica" + }, + { + new Guid("c096c69e-762b-5281-9290-bb9c167a24a0"), + "CB", + new DateTime(1998, 4, 26, 0, 0, 0, 0, DateTimeKind.Utc), + "Cristian", + "Romero", + "Premier League", + "Gabriel", + "Centre-Back", + 13, + true, + "Tottenham Hotspur" + }, + { + new Guid("d3ba552a-dac3-588a-b961-1ea7224017fd"), + "CM", + new DateTime(2001, 1, 16, 0, 0, 0, 0, DateTimeKind.Utc), + "Enzo", + "Fernández", + "Liga Portugal", + "Jeremías", + "Central Midfield", + 24, + true, + "SL Benfica" + }, + { + new Guid("d5f7dd7a-1dcb-5960-ba27-e34865b63358"), + "CB", + new DateTime(1988, 2, 11, 0, 0, 0, 0, DateTimeKind.Utc), + "Nicolás", + "Otamendi", + "Liga Portugal", + "Hernán Gonzalo", + "Centre-Back", + 19, + true, + "SL Benfica" + }, + { + new Guid("da31293b-4c7e-5e0f-a168-469ee29ecbc4"), + "RB", + new DateTime(1998, 4, 5, 0, 0, 0, 0, DateTimeKind.Utc), + "Nahuel", + "Molina", + "La Liga", + null, + "Right-Back", + 26, + true, + "Altético Madrid" + } + } + ); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("01772c59-43f0-5d85-b913-c78e4e281452") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("0293b282-1da8-562e-998e-83849b417a42") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("2f6f90a0-9b9d-5023-96d2-a2aaf03143a6") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("38bae91d-8519-55a2-b30a-b9fe38849bfb") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("9613cae9-16ab-5b54-937e-3135123b9e0d") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("acc433bf-d505-51fe-831e-45eb44c4d43c") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("b5b46e79-929e-5ed2-949d-0d167109c022") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("c096c69e-762b-5281-9290-bb9c167a24a0") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("d3ba552a-dac3-588a-b961-1ea7224017fd") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("d5f7dd7a-1dcb-5960-ba27-e34865b63358") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("da31293b-4c7e-5e0f-a168-469ee29ecbc4") + ); + } + } +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141721_SeedSubstitutes.Designer.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141721_SeedSubstitutes.Designer.cs new file mode 100644 index 0000000..0881e2b --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141721_SeedSubstitutes.Designer.cs @@ -0,0 +1,429 @@ +// +using System; +using Dotnet.Samples.AspNetCore.WebApi.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Dotnet.Samples.AspNetCore.WebApi.Migrations +{ + [DbContext(typeof(PlayerDbContext))] + [Migration("20260409141721_SeedSubstitutes")] + partial class SeedSubstitutes + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.5"); + + modelBuilder.Entity("Dotnet.Samples.AspNetCore.WebApi.Models.Player", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AbbrPosition") + .HasColumnType("TEXT"); + + b.Property("DateOfBirth") + .HasColumnType("TEXT"); + + b.Property("FirstName") + .HasColumnType("TEXT"); + + b.Property("LastName") + .HasColumnType("TEXT"); + + b.Property("League") + .HasColumnType("TEXT"); + + b.Property("MiddleName") + .HasColumnType("TEXT"); + + b.Property("Position") + .HasColumnType("TEXT"); + + b.Property("SquadNumber") + .HasColumnType("INTEGER"); + + b.Property("Starting11") + .HasColumnType("INTEGER"); + + b.Property("Team") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SquadNumber") + .IsUnique(); + + b.ToTable("Players"); + + b.HasData( + new + { + Id = new Guid("01772c59-43f0-5d85-b913-c78e4e281452"), + AbbrPosition = "GK", + DateOfBirth = new DateTime(1992, 9, 1, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Damián", + LastName = "Martínez", + League = "Premier League", + MiddleName = "Emiliano", + Position = "Goalkeeper", + SquadNumber = 23, + Starting11 = true, + Team = "Aston Villa FC" + }, + new + { + Id = new Guid("da31293b-4c7e-5e0f-a168-469ee29ecbc4"), + AbbrPosition = "RB", + DateOfBirth = new DateTime(1998, 4, 5, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Nahuel", + LastName = "Molina", + League = "La Liga", + Position = "Right-Back", + SquadNumber = 26, + Starting11 = true, + Team = "Altético Madrid" + }, + new + { + Id = new Guid("c096c69e-762b-5281-9290-bb9c167a24a0"), + AbbrPosition = "CB", + DateOfBirth = new DateTime(1998, 4, 26, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Cristian", + LastName = "Romero", + League = "Premier League", + MiddleName = "Gabriel", + Position = "Centre-Back", + SquadNumber = 13, + Starting11 = true, + Team = "Tottenham Hotspur" + }, + new + { + Id = new Guid("d5f7dd7a-1dcb-5960-ba27-e34865b63358"), + AbbrPosition = "CB", + DateOfBirth = new DateTime(1988, 2, 11, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Nicolás", + LastName = "Otamendi", + League = "Liga Portugal", + MiddleName = "Hernán Gonzalo", + Position = "Centre-Back", + SquadNumber = 19, + Starting11 = true, + Team = "SL Benfica" + }, + new + { + Id = new Guid("2f6f90a0-9b9d-5023-96d2-a2aaf03143a6"), + AbbrPosition = "LB", + DateOfBirth = new DateTime(1992, 8, 30, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Nicolás", + LastName = "Tagliafico", + League = "Ligue 1", + MiddleName = "Alejandro", + Position = "Left-Back", + SquadNumber = 3, + Starting11 = true, + Team = "Olympique Lyon" + }, + new + { + Id = new Guid("b5b46e79-929e-5ed2-949d-0d167109c022"), + AbbrPosition = "RW", + DateOfBirth = new DateTime(1988, 2, 13, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Ángel", + LastName = "Di María", + League = "Liga Portugal", + MiddleName = "Fabián", + Position = "Right Winger", + SquadNumber = 11, + Starting11 = true, + Team = "SL Benfica" + }, + new + { + Id = new Guid("0293b282-1da8-562e-998e-83849b417a42"), + AbbrPosition = "CM", + DateOfBirth = new DateTime(1994, 5, 23, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Rodrigo", + LastName = "de Paul", + League = "La Liga", + MiddleName = "Javier", + Position = "Central Midfield", + SquadNumber = 7, + Starting11 = true, + Team = "Altético Madrid" + }, + new + { + Id = new Guid("d3ba552a-dac3-588a-b961-1ea7224017fd"), + AbbrPosition = "CM", + DateOfBirth = new DateTime(2001, 1, 16, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Enzo", + LastName = "Fernández", + League = "Liga Portugal", + MiddleName = "Jeremías", + Position = "Central Midfield", + SquadNumber = 24, + Starting11 = true, + Team = "SL Benfica" + }, + new + { + Id = new Guid("9613cae9-16ab-5b54-937e-3135123b9e0d"), + AbbrPosition = "CM", + DateOfBirth = new DateTime(1998, 12, 23, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Alexis", + LastName = "Mac Allister", + League = "Premier League", + Position = "Central Midfield", + SquadNumber = 20, + Starting11 = true, + Team = "Brighton & Hove Albion" + }, + new + { + Id = new Guid("acc433bf-d505-51fe-831e-45eb44c4d43c"), + AbbrPosition = "RW", + DateOfBirth = new DateTime(1987, 6, 23, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Lionel", + LastName = "Messi", + League = "Ligue 1", + MiddleName = "Andrés", + Position = "Right Winger", + SquadNumber = 10, + Starting11 = true, + Team = "Paris Saint-Germain" + }, + new + { + Id = new Guid("38bae91d-8519-55a2-b30a-b9fe38849bfb"), + AbbrPosition = "CF", + DateOfBirth = new DateTime(2000, 1, 30, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Julián", + LastName = "Álvarez", + League = "Premier League", + Position = "Centre-Forward", + SquadNumber = 9, + Starting11 = true, + Team = "Manchester City" + }, + new + { + Id = new Guid("5a9cd988-95e6-54c1-bc34-9aa08acca8d0"), + AbbrPosition = "GK", + DateOfBirth = new DateTime(1986, 10, 16, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Franco", + LastName = "Armani", + League = "Copa de la Liga", + MiddleName = "Daniel", + Position = "Goalkeeper", + SquadNumber = 1, + Starting11 = false, + Team = "River Plate" + }, + new + { + Id = new Guid("c62f2ac1-41e8-5d34-b073-2ba0913d0e31"), + AbbrPosition = "GK", + DateOfBirth = new DateTime(1992, 5, 20, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Gerónimo", + LastName = "Rulli", + League = "Eredivisie", + Position = "Goalkeeper", + SquadNumber = 12, + Starting11 = false, + Team = "Ajax Amsterdam" + }, + new + { + Id = new Guid("5fdb10e8-38c0-5084-9a3f-b369a960b9c2"), + AbbrPosition = "RB", + DateOfBirth = new DateTime(1998, 1, 12, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Juan", + LastName = "Foyth", + League = "La Liga", + MiddleName = "Marcos", + Position = "Right-Back", + SquadNumber = 2, + Starting11 = false, + Team = "Villarreal" + }, + new + { + Id = new Guid("bbd441f7-fcfb-5834-8468-2a9004b64c8c"), + AbbrPosition = "RB", + DateOfBirth = new DateTime(1997, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Gonzalo", + LastName = "Montiel", + League = "Premier League", + MiddleName = "Ariel", + Position = "Right-Back", + SquadNumber = 4, + Starting11 = false, + Team = "Nottingham Forest" + }, + new + { + Id = new Guid("d8bfea25-f189-5d5e-b3a5-ed89329b9f7c"), + AbbrPosition = "CB", + DateOfBirth = new DateTime(1991, 6, 27, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Germán", + LastName = "Pezzella", + League = "La Liga", + MiddleName = "Alejo", + Position = "Centre-Back", + SquadNumber = 6, + Starting11 = false, + Team = "Real Betis Balompié" + }, + new + { + Id = new Guid("dca343a8-12e5-53d6-89a8-916b120a5ee4"), + AbbrPosition = "LB", + DateOfBirth = new DateTime(1991, 10, 28, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Marcos", + LastName = "Acuña", + League = "La Liga", + MiddleName = "Javier", + Position = "Left-Back", + SquadNumber = 8, + Starting11 = false, + Team = "Sevilla FC" + }, + new + { + Id = new Guid("98306555-a466-5d18-804e-dc82175e697b"), + AbbrPosition = "CB", + DateOfBirth = new DateTime(1998, 1, 18, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Lisandro", + LastName = "Martínez", + League = "Premier League", + Position = "Centre-Back", + SquadNumber = 25, + Starting11 = false, + Team = "Manchester United" + }, + new + { + Id = new Guid("9d140400-196f-55d8-86e1-e0b96a375c83"), + AbbrPosition = "DM", + DateOfBirth = new DateTime(1994, 6, 29, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Leandro", + LastName = "Paredes", + League = "Serie A", + MiddleName = "Daniel", + Position = "Defensive Midfield", + SquadNumber = 5, + Starting11 = false, + Team = "AS Roma" + }, + new + { + Id = new Guid("d3b0e8e8-2c34-531a-b608-b24fed0ef986"), + AbbrPosition = "CM", + DateOfBirth = new DateTime(1998, 10, 5, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Exequiel", + LastName = "Palacios", + League = "Bundesliga", + MiddleName = "Alejandro", + Position = "Central Midfield", + SquadNumber = 14, + Starting11 = false, + Team = "Bayer 04 Leverkusen" + }, + new + { + Id = new Guid("7cc8d527-56a2-58bd-9528-2618fc139d30"), + AbbrPosition = "LW", + DateOfBirth = new DateTime(1988, 2, 15, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Alejandro", + LastName = "Gómez", + League = "Serie A", + MiddleName = "Darío", + Position = "Left Winger", + SquadNumber = 17, + Starting11 = false, + Team = "AC Monza" + }, + new + { + Id = new Guid("191c82af-0c51-526a-b903-c3600b61b506"), + AbbrPosition = "DM", + DateOfBirth = new DateTime(1994, 4, 12, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Guido", + LastName = "Rodríguez", + League = "La Liga", + Position = "Defensive Midfield", + SquadNumber = 18, + Starting11 = false, + Team = "Real Betis Balompié" + }, + new + { + Id = new Guid("b1306b7b-a3a4-5f7c-90fd-dd5bdbed57ba"), + AbbrPosition = "RW", + DateOfBirth = new DateTime(1995, 3, 9, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Ángel", + LastName = "Correa", + League = "La Liga", + MiddleName = "Martín", + Position = "Right Winger", + SquadNumber = 15, + Starting11 = false, + Team = "Atlético Madrid" + }, + new + { + Id = new Guid("ecec27e8-487b-5622-b116-0855020477ed"), + AbbrPosition = "AM", + DateOfBirth = new DateTime(2001, 4, 26, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Thiago", + LastName = "Almada", + League = "Major League Soccer", + MiddleName = "Ezequiel", + Position = "Attacking Midfield", + SquadNumber = 16, + Starting11 = false, + Team = "Atlanta United FC" + }, + new + { + Id = new Guid("7941cd7c-4df1-5952-97e8-1e7f5d08e8aa"), + AbbrPosition = "SS", + DateOfBirth = new DateTime(1993, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Paulo", + LastName = "Dybala", + League = "Serie A", + MiddleName = "Exequiel", + Position = "Second Striker", + SquadNumber = 21, + Starting11 = false, + Team = "AS Roma" + }, + new + { + Id = new Guid("79c96f29-c59f-5f98-96b8-3a5946246624"), + AbbrPosition = "CF", + DateOfBirth = new DateTime(1997, 8, 22, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Lautaro", + LastName = "Martínez", + League = "Serie A", + MiddleName = "Javier", + Position = "Centre-Forward", + SquadNumber = 22, + Starting11 = false, + Team = "Inter Milan" + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141721_SeedSubstitutes.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141721_SeedSubstitutes.cs new file mode 100644 index 0000000..53d8e8e --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/20260409141721_SeedSubstitutes.cs @@ -0,0 +1,327 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace Dotnet.Samples.AspNetCore.WebApi.Migrations +{ + /// + public partial class SeedSubstitutes : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.InsertData( + table: "Players", + columns: new[] + { + "Id", + "AbbrPosition", + "DateOfBirth", + "FirstName", + "LastName", + "League", + "MiddleName", + "Position", + "SquadNumber", + "Starting11", + "Team" + }, + values: new object[,] + { + { + new Guid("191c82af-0c51-526a-b903-c3600b61b506"), + "DM", + new DateTime(1994, 4, 12, 0, 0, 0, 0, DateTimeKind.Utc), + "Guido", + "Rodríguez", + "La Liga", + null, + "Defensive Midfield", + 18, + false, + "Real Betis Balompié" + }, + { + new Guid("5a9cd988-95e6-54c1-bc34-9aa08acca8d0"), + "GK", + new DateTime(1986, 10, 16, 0, 0, 0, 0, DateTimeKind.Utc), + "Franco", + "Armani", + "Copa de la Liga", + "Daniel", + "Goalkeeper", + 1, + false, + "River Plate" + }, + { + new Guid("5fdb10e8-38c0-5084-9a3f-b369a960b9c2"), + "RB", + new DateTime(1998, 1, 12, 0, 0, 0, 0, DateTimeKind.Utc), + "Juan", + "Foyth", + "La Liga", + "Marcos", + "Right-Back", + 2, + false, + "Villarreal" + }, + { + new Guid("7941cd7c-4df1-5952-97e8-1e7f5d08e8aa"), + "SS", + new DateTime(1993, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc), + "Paulo", + "Dybala", + "Serie A", + "Exequiel", + "Second Striker", + 21, + false, + "AS Roma" + }, + { + new Guid("79c96f29-c59f-5f98-96b8-3a5946246624"), + "CF", + new DateTime(1997, 8, 22, 0, 0, 0, 0, DateTimeKind.Utc), + "Lautaro", + "Martínez", + "Serie A", + "Javier", + "Centre-Forward", + 22, + false, + "Inter Milan" + }, + { + new Guid("7cc8d527-56a2-58bd-9528-2618fc139d30"), + "LW", + new DateTime(1988, 2, 15, 0, 0, 0, 0, DateTimeKind.Utc), + "Alejandro", + "Gómez", + "Serie A", + "Darío", + "Left Winger", + 17, + false, + "AC Monza" + }, + { + new Guid("98306555-a466-5d18-804e-dc82175e697b"), + "CB", + new DateTime(1998, 1, 18, 0, 0, 0, 0, DateTimeKind.Utc), + "Lisandro", + "Martínez", + "Premier League", + null, + "Centre-Back", + 25, + false, + "Manchester United" + }, + { + new Guid("9d140400-196f-55d8-86e1-e0b96a375c83"), + "DM", + new DateTime(1994, 6, 29, 0, 0, 0, 0, DateTimeKind.Utc), + "Leandro", + "Paredes", + "Serie A", + "Daniel", + "Defensive Midfield", + 5, + false, + "AS Roma" + }, + { + new Guid("b1306b7b-a3a4-5f7c-90fd-dd5bdbed57ba"), + "RW", + new DateTime(1995, 3, 9, 0, 0, 0, 0, DateTimeKind.Utc), + "Ángel", + "Correa", + "La Liga", + "Martín", + "Right Winger", + 15, + false, + "Atlético Madrid" + }, + { + new Guid("bbd441f7-fcfb-5834-8468-2a9004b64c8c"), + "RB", + new DateTime(1997, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + "Gonzalo", + "Montiel", + "Premier League", + "Ariel", + "Right-Back", + 4, + false, + "Nottingham Forest" + }, + { + new Guid("c62f2ac1-41e8-5d34-b073-2ba0913d0e31"), + "GK", + new DateTime(1992, 5, 20, 0, 0, 0, 0, DateTimeKind.Utc), + "Gerónimo", + "Rulli", + "Eredivisie", + null, + "Goalkeeper", + 12, + false, + "Ajax Amsterdam" + }, + { + new Guid("d3b0e8e8-2c34-531a-b608-b24fed0ef986"), + "CM", + new DateTime(1998, 10, 5, 0, 0, 0, 0, DateTimeKind.Utc), + "Exequiel", + "Palacios", + "Bundesliga", + "Alejandro", + "Central Midfield", + 14, + false, + "Bayer 04 Leverkusen" + }, + { + new Guid("d8bfea25-f189-5d5e-b3a5-ed89329b9f7c"), + "CB", + new DateTime(1991, 6, 27, 0, 0, 0, 0, DateTimeKind.Utc), + "Germán", + "Pezzella", + "La Liga", + "Alejo", + "Centre-Back", + 6, + false, + "Real Betis Balompié" + }, + { + new Guid("dca343a8-12e5-53d6-89a8-916b120a5ee4"), + "LB", + new DateTime(1991, 10, 28, 0, 0, 0, 0, DateTimeKind.Utc), + "Marcos", + "Acuña", + "La Liga", + "Javier", + "Left-Back", + 8, + false, + "Sevilla FC" + }, + { + new Guid("ecec27e8-487b-5622-b116-0855020477ed"), + "AM", + new DateTime(2001, 4, 26, 0, 0, 0, 0, DateTimeKind.Utc), + "Thiago", + "Almada", + "Major League Soccer", + "Ezequiel", + "Attacking Midfield", + 16, + false, + "Atlanta United FC" + } + } + ); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("191c82af-0c51-526a-b903-c3600b61b506") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("5a9cd988-95e6-54c1-bc34-9aa08acca8d0") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("5fdb10e8-38c0-5084-9a3f-b369a960b9c2") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("7941cd7c-4df1-5952-97e8-1e7f5d08e8aa") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("79c96f29-c59f-5f98-96b8-3a5946246624") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("7cc8d527-56a2-58bd-9528-2618fc139d30") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("98306555-a466-5d18-804e-dc82175e697b") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("9d140400-196f-55d8-86e1-e0b96a375c83") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("b1306b7b-a3a4-5f7c-90fd-dd5bdbed57ba") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("bbd441f7-fcfb-5834-8468-2a9004b64c8c") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("c62f2ac1-41e8-5d34-b073-2ba0913d0e31") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("d3b0e8e8-2c34-531a-b608-b24fed0ef986") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("d8bfea25-f189-5d5e-b3a5-ed89329b9f7c") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("dca343a8-12e5-53d6-89a8-916b120a5ee4") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("ecec27e8-487b-5622-b116-0855020477ed") + ); + } + } +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151000_InitialCreate.Designer.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151000_InitialCreate.Designer.cs new file mode 100644 index 0000000..436267e --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151000_InitialCreate.Designer.cs @@ -0,0 +1,68 @@ +// +using System; +using Dotnet.Samples.AspNetCore.WebApi.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Dotnet.Samples.AspNetCore.WebApi.Migrations.Npgsql +{ + [DbContext(typeof(PlayerDbContext))] + [Migration("20260409151000_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.7"); + + modelBuilder.Entity("Dotnet.Samples.AspNetCore.WebApi.Models.Player", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AbbrPosition") + .HasColumnType("text"); + + b.Property("DateOfBirth") + .HasColumnType("timestamp with time zone"); + + b.Property("FirstName") + .HasColumnType("text"); + + b.Property("LastName") + .HasColumnType("text"); + + b.Property("League") + .HasColumnType("text"); + + b.Property("MiddleName") + .HasColumnType("text"); + + b.Property("Position") + .HasColumnType("text"); + + b.Property("SquadNumber") + .HasColumnType("integer"); + + b.Property("Starting11") + .HasColumnType("boolean"); + + b.Property("Team") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("SquadNumber") + .IsUnique(); + + b.ToTable("Players"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151000_InitialCreate.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151000_InitialCreate.cs new file mode 100644 index 0000000..5bc486b --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151000_InitialCreate.cs @@ -0,0 +1,53 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Dotnet.Samples.AspNetCore.WebApi.Migrations.Npgsql +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Players", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + FirstName = table.Column(type: "text", nullable: true), + MiddleName = table.Column(type: "text", nullable: true), + LastName = table.Column(type: "text", nullable: true), + DateOfBirth = table.Column( + type: "timestamp with time zone", + nullable: true + ), + SquadNumber = table.Column(type: "integer", nullable: false), + Position = table.Column(type: "text", nullable: true), + AbbrPosition = table.Column(type: "text", nullable: true), + Team = table.Column(type: "text", nullable: true), + League = table.Column(type: "text", nullable: true), + Starting11 = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Players", x => x.Id); + } + ); + + migrationBuilder.CreateIndex( + name: "IX_Players_SquadNumber", + table: "Players", + column: "SquadNumber", + unique: true + ); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable(name: "Players"); + } + } +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151100_SeedStarting11.Designer.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151100_SeedStarting11.Designer.cs new file mode 100644 index 0000000..cb4748a --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151100_SeedStarting11.Designer.cs @@ -0,0 +1,68 @@ +// +using System; +using Dotnet.Samples.AspNetCore.WebApi.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Dotnet.Samples.AspNetCore.WebApi.Migrations.Npgsql +{ + [DbContext(typeof(PlayerDbContext))] + [Migration("20260409151100_SeedStarting11")] + partial class SeedStarting11 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.7"); + + modelBuilder.Entity("Dotnet.Samples.AspNetCore.WebApi.Models.Player", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AbbrPosition") + .HasColumnType("text"); + + b.Property("DateOfBirth") + .HasColumnType("timestamp with time zone"); + + b.Property("FirstName") + .HasColumnType("text"); + + b.Property("LastName") + .HasColumnType("text"); + + b.Property("League") + .HasColumnType("text"); + + b.Property("MiddleName") + .HasColumnType("text"); + + b.Property("Position") + .HasColumnType("text"); + + b.Property("SquadNumber") + .HasColumnType("integer"); + + b.Property("Starting11") + .HasColumnType("boolean"); + + b.Property("Team") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("SquadNumber") + .IsUnique(); + + b.ToTable("Players"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151100_SeedStarting11.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151100_SeedStarting11.cs new file mode 100644 index 0000000..b833b27 --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151100_SeedStarting11.cs @@ -0,0 +1,251 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace Dotnet.Samples.AspNetCore.WebApi.Migrations.Npgsql +{ + /// + public partial class SeedStarting11 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.InsertData( + table: "Players", + columns: new[] + { + "Id", + "AbbrPosition", + "DateOfBirth", + "FirstName", + "LastName", + "League", + "MiddleName", + "Position", + "SquadNumber", + "Starting11", + "Team" + }, + values: new object[,] + { + { + new Guid("01772c59-43f0-5d85-b913-c78e4e281452"), + "GK", + new DateTime(1992, 9, 1, 0, 0, 0, 0, DateTimeKind.Utc), + "Damián", + "Martínez", + "Premier League", + "Emiliano", + "Goalkeeper", + 23, + true, + "Aston Villa FC" + }, + { + new Guid("0293b282-1da8-562e-998e-83849b417a42"), + "CM", + new DateTime(1994, 5, 23, 0, 0, 0, 0, DateTimeKind.Utc), + "Rodrigo", + "de Paul", + "La Liga", + "Javier", + "Central Midfield", + 7, + true, + "Altético Madrid" + }, + { + new Guid("2f6f90a0-9b9d-5023-96d2-a2aaf03143a6"), + "LB", + new DateTime(1992, 8, 30, 0, 0, 0, 0, DateTimeKind.Utc), + "Nicolás", + "Tagliafico", + "Ligue 1", + "Alejandro", + "Left-Back", + 3, + true, + "Olympique Lyon" + }, + { + new Guid("38bae91d-8519-55a2-b30a-b9fe38849bfb"), + "CF", + new DateTime(2000, 1, 30, 0, 0, 0, 0, DateTimeKind.Utc), + "Julián", + "Álvarez", + "Premier League", + null, + "Centre-Forward", + 9, + true, + "Manchester City" + }, + { + new Guid("9613cae9-16ab-5b54-937e-3135123b9e0d"), + "CM", + new DateTime(1998, 12, 23, 0, 0, 0, 0, DateTimeKind.Utc), + "Alexis", + "Mac Allister", + "Premier League", + null, + "Central Midfield", + 20, + true, + "Brighton & Hove Albion" + }, + { + new Guid("acc433bf-d505-51fe-831e-45eb44c4d43c"), + "RW", + new DateTime(1987, 6, 23, 0, 0, 0, 0, DateTimeKind.Utc), + "Lionel", + "Messi", + "Ligue 1", + "Andrés", + "Right Winger", + 10, + true, + "Paris Saint-Germain" + }, + { + new Guid("b5b46e79-929e-5ed2-949d-0d167109c022"), + "RW", + new DateTime(1988, 2, 13, 0, 0, 0, 0, DateTimeKind.Utc), + "Ángel", + "Di María", + "Liga Portugal", + "Fabián", + "Right Winger", + 11, + true, + "SL Benfica" + }, + { + new Guid("c096c69e-762b-5281-9290-bb9c167a24a0"), + "CB", + new DateTime(1998, 4, 26, 0, 0, 0, 0, DateTimeKind.Utc), + "Cristian", + "Romero", + "Premier League", + "Gabriel", + "Centre-Back", + 13, + true, + "Tottenham Hotspur" + }, + { + new Guid("d3ba552a-dac3-588a-b961-1ea7224017fd"), + "CM", + new DateTime(2001, 1, 16, 0, 0, 0, 0, DateTimeKind.Utc), + "Enzo", + "Fernández", + "Liga Portugal", + "Jeremías", + "Central Midfield", + 24, + true, + "SL Benfica" + }, + { + new Guid("d5f7dd7a-1dcb-5960-ba27-e34865b63358"), + "CB", + new DateTime(1988, 2, 11, 0, 0, 0, 0, DateTimeKind.Utc), + "Nicolás", + "Otamendi", + "Liga Portugal", + "Hernán Gonzalo", + "Centre-Back", + 19, + true, + "SL Benfica" + }, + { + new Guid("da31293b-4c7e-5e0f-a168-469ee29ecbc4"), + "RB", + new DateTime(1998, 4, 5, 0, 0, 0, 0, DateTimeKind.Utc), + "Nahuel", + "Molina", + "La Liga", + null, + "Right-Back", + 26, + true, + "Altético Madrid" + } + } + ); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("01772c59-43f0-5d85-b913-c78e4e281452") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("0293b282-1da8-562e-998e-83849b417a42") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("2f6f90a0-9b9d-5023-96d2-a2aaf03143a6") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("38bae91d-8519-55a2-b30a-b9fe38849bfb") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("9613cae9-16ab-5b54-937e-3135123b9e0d") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("acc433bf-d505-51fe-831e-45eb44c4d43c") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("b5b46e79-929e-5ed2-949d-0d167109c022") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("c096c69e-762b-5281-9290-bb9c167a24a0") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("d3ba552a-dac3-588a-b961-1ea7224017fd") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("d5f7dd7a-1dcb-5960-ba27-e34865b63358") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("da31293b-4c7e-5e0f-a168-469ee29ecbc4") + ); + } + } +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151200_SeedSubstitutes.Designer.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151200_SeedSubstitutes.Designer.cs new file mode 100644 index 0000000..2cddcbe --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151200_SeedSubstitutes.Designer.cs @@ -0,0 +1,68 @@ +// +using System; +using Dotnet.Samples.AspNetCore.WebApi.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Dotnet.Samples.AspNetCore.WebApi.Migrations.Npgsql +{ + [DbContext(typeof(PlayerDbContext))] + [Migration("20260409151200_SeedSubstitutes")] + partial class SeedSubstitutes + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.7"); + + modelBuilder.Entity("Dotnet.Samples.AspNetCore.WebApi.Models.Player", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AbbrPosition") + .HasColumnType("text"); + + b.Property("DateOfBirth") + .HasColumnType("timestamp with time zone"); + + b.Property("FirstName") + .HasColumnType("text"); + + b.Property("LastName") + .HasColumnType("text"); + + b.Property("League") + .HasColumnType("text"); + + b.Property("MiddleName") + .HasColumnType("text"); + + b.Property("Position") + .HasColumnType("text"); + + b.Property("SquadNumber") + .HasColumnType("integer"); + + b.Property("Starting11") + .HasColumnType("boolean"); + + b.Property("Team") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("SquadNumber") + .IsUnique(); + + b.ToTable("Players"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151200_SeedSubstitutes.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151200_SeedSubstitutes.cs new file mode 100644 index 0000000..739344f --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/Npgsql/20260409151200_SeedSubstitutes.cs @@ -0,0 +1,327 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace Dotnet.Samples.AspNetCore.WebApi.Migrations.Npgsql +{ + /// + public partial class SeedSubstitutes : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.InsertData( + table: "Players", + columns: new[] + { + "Id", + "AbbrPosition", + "DateOfBirth", + "FirstName", + "LastName", + "League", + "MiddleName", + "Position", + "SquadNumber", + "Starting11", + "Team" + }, + values: new object[,] + { + { + new Guid("191c82af-0c51-526a-b903-c3600b61b506"), + "DM", + new DateTime(1994, 4, 12, 0, 0, 0, 0, DateTimeKind.Utc), + "Guido", + "Rodríguez", + "La Liga", + null, + "Defensive Midfield", + 18, + false, + "Real Betis Balompié" + }, + { + new Guid("5a9cd988-95e6-54c1-bc34-9aa08acca8d0"), + "GK", + new DateTime(1986, 10, 16, 0, 0, 0, 0, DateTimeKind.Utc), + "Franco", + "Armani", + "Copa de la Liga", + "Daniel", + "Goalkeeper", + 1, + false, + "River Plate" + }, + { + new Guid("5fdb10e8-38c0-5084-9a3f-b369a960b9c2"), + "RB", + new DateTime(1998, 1, 12, 0, 0, 0, 0, DateTimeKind.Utc), + "Juan", + "Foyth", + "La Liga", + "Marcos", + "Right-Back", + 2, + false, + "Villarreal" + }, + { + new Guid("7941cd7c-4df1-5952-97e8-1e7f5d08e8aa"), + "SS", + new DateTime(1993, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc), + "Paulo", + "Dybala", + "Serie A", + "Exequiel", + "Second Striker", + 21, + false, + "AS Roma" + }, + { + new Guid("79c96f29-c59f-5f98-96b8-3a5946246624"), + "CF", + new DateTime(1997, 8, 22, 0, 0, 0, 0, DateTimeKind.Utc), + "Lautaro", + "Martínez", + "Serie A", + "Javier", + "Centre-Forward", + 22, + false, + "Inter Milan" + }, + { + new Guid("7cc8d527-56a2-58bd-9528-2618fc139d30"), + "LW", + new DateTime(1988, 2, 15, 0, 0, 0, 0, DateTimeKind.Utc), + "Alejandro", + "Gómez", + "Serie A", + "Darío", + "Left Winger", + 17, + false, + "AC Monza" + }, + { + new Guid("98306555-a466-5d18-804e-dc82175e697b"), + "CB", + new DateTime(1998, 1, 18, 0, 0, 0, 0, DateTimeKind.Utc), + "Lisandro", + "Martínez", + "Premier League", + null, + "Centre-Back", + 25, + false, + "Manchester United" + }, + { + new Guid("9d140400-196f-55d8-86e1-e0b96a375c83"), + "DM", + new DateTime(1994, 6, 29, 0, 0, 0, 0, DateTimeKind.Utc), + "Leandro", + "Paredes", + "Serie A", + "Daniel", + "Defensive Midfield", + 5, + false, + "AS Roma" + }, + { + new Guid("b1306b7b-a3a4-5f7c-90fd-dd5bdbed57ba"), + "RW", + new DateTime(1995, 3, 9, 0, 0, 0, 0, DateTimeKind.Utc), + "Ángel", + "Correa", + "La Liga", + "Martín", + "Right Winger", + 15, + false, + "Atlético Madrid" + }, + { + new Guid("bbd441f7-fcfb-5834-8468-2a9004b64c8c"), + "RB", + new DateTime(1997, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + "Gonzalo", + "Montiel", + "Premier League", + "Ariel", + "Right-Back", + 4, + false, + "Nottingham Forest" + }, + { + new Guid("c62f2ac1-41e8-5d34-b073-2ba0913d0e31"), + "GK", + new DateTime(1992, 5, 20, 0, 0, 0, 0, DateTimeKind.Utc), + "Gerónimo", + "Rulli", + "Eredivisie", + null, + "Goalkeeper", + 12, + false, + "Ajax Amsterdam" + }, + { + new Guid("d3b0e8e8-2c34-531a-b608-b24fed0ef986"), + "CM", + new DateTime(1998, 10, 5, 0, 0, 0, 0, DateTimeKind.Utc), + "Exequiel", + "Palacios", + "Bundesliga", + "Alejandro", + "Central Midfield", + 14, + false, + "Bayer 04 Leverkusen" + }, + { + new Guid("d8bfea25-f189-5d5e-b3a5-ed89329b9f7c"), + "CB", + new DateTime(1991, 6, 27, 0, 0, 0, 0, DateTimeKind.Utc), + "Germán", + "Pezzella", + "La Liga", + "Alejo", + "Centre-Back", + 6, + false, + "Real Betis Balompié" + }, + { + new Guid("dca343a8-12e5-53d6-89a8-916b120a5ee4"), + "LB", + new DateTime(1991, 10, 28, 0, 0, 0, 0, DateTimeKind.Utc), + "Marcos", + "Acuña", + "La Liga", + "Javier", + "Left-Back", + 8, + false, + "Sevilla FC" + }, + { + new Guid("ecec27e8-487b-5622-b116-0855020477ed"), + "AM", + new DateTime(2001, 4, 26, 0, 0, 0, 0, DateTimeKind.Utc), + "Thiago", + "Almada", + "Major League Soccer", + "Ezequiel", + "Attacking Midfield", + 16, + false, + "Atlanta United FC" + } + } + ); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("191c82af-0c51-526a-b903-c3600b61b506") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("5a9cd988-95e6-54c1-bc34-9aa08acca8d0") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("5fdb10e8-38c0-5084-9a3f-b369a960b9c2") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("7941cd7c-4df1-5952-97e8-1e7f5d08e8aa") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("79c96f29-c59f-5f98-96b8-3a5946246624") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("7cc8d527-56a2-58bd-9528-2618fc139d30") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("98306555-a466-5d18-804e-dc82175e697b") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("9d140400-196f-55d8-86e1-e0b96a375c83") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("b1306b7b-a3a4-5f7c-90fd-dd5bdbed57ba") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("bbd441f7-fcfb-5834-8468-2a9004b64c8c") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("c62f2ac1-41e8-5d34-b073-2ba0913d0e31") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("d3b0e8e8-2c34-531a-b608-b24fed0ef986") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("d8bfea25-f189-5d5e-b3a5-ed89329b9f7c") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("dca343a8-12e5-53d6-89a8-916b120a5ee4") + ); + + migrationBuilder.DeleteData( + table: "Players", + keyColumn: "Id", + keyValue: new Guid("ecec27e8-487b-5622-b116-0855020477ed") + ); + } + } +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/PlayerDbContextModelSnapshot.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/PlayerDbContextModelSnapshot.cs new file mode 100644 index 0000000..f831c50 --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/PlayerDbContextModelSnapshot.cs @@ -0,0 +1,426 @@ +// +using System; +using Dotnet.Samples.AspNetCore.WebApi.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Dotnet.Samples.AspNetCore.WebApi.Migrations +{ + [DbContext(typeof(PlayerDbContext))] + partial class PlayerDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.5"); + + modelBuilder.Entity("Dotnet.Samples.AspNetCore.WebApi.Models.Player", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AbbrPosition") + .HasColumnType("TEXT"); + + b.Property("DateOfBirth") + .HasColumnType("TEXT"); + + b.Property("FirstName") + .HasColumnType("TEXT"); + + b.Property("LastName") + .HasColumnType("TEXT"); + + b.Property("League") + .HasColumnType("TEXT"); + + b.Property("MiddleName") + .HasColumnType("TEXT"); + + b.Property("Position") + .HasColumnType("TEXT"); + + b.Property("SquadNumber") + .HasColumnType("INTEGER"); + + b.Property("Starting11") + .HasColumnType("INTEGER"); + + b.Property("Team") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SquadNumber") + .IsUnique(); + + b.ToTable("Players"); + + b.HasData( + new + { + Id = new Guid("01772c59-43f0-5d85-b913-c78e4e281452"), + AbbrPosition = "GK", + DateOfBirth = new DateTime(1992, 9, 1, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Damián", + LastName = "Martínez", + League = "Premier League", + MiddleName = "Emiliano", + Position = "Goalkeeper", + SquadNumber = 23, + Starting11 = true, + Team = "Aston Villa FC" + }, + new + { + Id = new Guid("da31293b-4c7e-5e0f-a168-469ee29ecbc4"), + AbbrPosition = "RB", + DateOfBirth = new DateTime(1998, 4, 5, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Nahuel", + LastName = "Molina", + League = "La Liga", + Position = "Right-Back", + SquadNumber = 26, + Starting11 = true, + Team = "Altético Madrid" + }, + new + { + Id = new Guid("c096c69e-762b-5281-9290-bb9c167a24a0"), + AbbrPosition = "CB", + DateOfBirth = new DateTime(1998, 4, 26, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Cristian", + LastName = "Romero", + League = "Premier League", + MiddleName = "Gabriel", + Position = "Centre-Back", + SquadNumber = 13, + Starting11 = true, + Team = "Tottenham Hotspur" + }, + new + { + Id = new Guid("d5f7dd7a-1dcb-5960-ba27-e34865b63358"), + AbbrPosition = "CB", + DateOfBirth = new DateTime(1988, 2, 11, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Nicolás", + LastName = "Otamendi", + League = "Liga Portugal", + MiddleName = "Hernán Gonzalo", + Position = "Centre-Back", + SquadNumber = 19, + Starting11 = true, + Team = "SL Benfica" + }, + new + { + Id = new Guid("2f6f90a0-9b9d-5023-96d2-a2aaf03143a6"), + AbbrPosition = "LB", + DateOfBirth = new DateTime(1992, 8, 30, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Nicolás", + LastName = "Tagliafico", + League = "Ligue 1", + MiddleName = "Alejandro", + Position = "Left-Back", + SquadNumber = 3, + Starting11 = true, + Team = "Olympique Lyon" + }, + new + { + Id = new Guid("b5b46e79-929e-5ed2-949d-0d167109c022"), + AbbrPosition = "RW", + DateOfBirth = new DateTime(1988, 2, 13, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Ángel", + LastName = "Di María", + League = "Liga Portugal", + MiddleName = "Fabián", + Position = "Right Winger", + SquadNumber = 11, + Starting11 = true, + Team = "SL Benfica" + }, + new + { + Id = new Guid("0293b282-1da8-562e-998e-83849b417a42"), + AbbrPosition = "CM", + DateOfBirth = new DateTime(1994, 5, 23, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Rodrigo", + LastName = "de Paul", + League = "La Liga", + MiddleName = "Javier", + Position = "Central Midfield", + SquadNumber = 7, + Starting11 = true, + Team = "Altético Madrid" + }, + new + { + Id = new Guid("d3ba552a-dac3-588a-b961-1ea7224017fd"), + AbbrPosition = "CM", + DateOfBirth = new DateTime(2001, 1, 16, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Enzo", + LastName = "Fernández", + League = "Liga Portugal", + MiddleName = "Jeremías", + Position = "Central Midfield", + SquadNumber = 24, + Starting11 = true, + Team = "SL Benfica" + }, + new + { + Id = new Guid("9613cae9-16ab-5b54-937e-3135123b9e0d"), + AbbrPosition = "CM", + DateOfBirth = new DateTime(1998, 12, 23, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Alexis", + LastName = "Mac Allister", + League = "Premier League", + Position = "Central Midfield", + SquadNumber = 20, + Starting11 = true, + Team = "Brighton & Hove Albion" + }, + new + { + Id = new Guid("acc433bf-d505-51fe-831e-45eb44c4d43c"), + AbbrPosition = "RW", + DateOfBirth = new DateTime(1987, 6, 23, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Lionel", + LastName = "Messi", + League = "Ligue 1", + MiddleName = "Andrés", + Position = "Right Winger", + SquadNumber = 10, + Starting11 = true, + Team = "Paris Saint-Germain" + }, + new + { + Id = new Guid("38bae91d-8519-55a2-b30a-b9fe38849bfb"), + AbbrPosition = "CF", + DateOfBirth = new DateTime(2000, 1, 30, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Julián", + LastName = "Álvarez", + League = "Premier League", + Position = "Centre-Forward", + SquadNumber = 9, + Starting11 = true, + Team = "Manchester City" + }, + new + { + Id = new Guid("5a9cd988-95e6-54c1-bc34-9aa08acca8d0"), + AbbrPosition = "GK", + DateOfBirth = new DateTime(1986, 10, 16, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Franco", + LastName = "Armani", + League = "Copa de la Liga", + MiddleName = "Daniel", + Position = "Goalkeeper", + SquadNumber = 1, + Starting11 = false, + Team = "River Plate" + }, + new + { + Id = new Guid("c62f2ac1-41e8-5d34-b073-2ba0913d0e31"), + AbbrPosition = "GK", + DateOfBirth = new DateTime(1992, 5, 20, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Gerónimo", + LastName = "Rulli", + League = "Eredivisie", + Position = "Goalkeeper", + SquadNumber = 12, + Starting11 = false, + Team = "Ajax Amsterdam" + }, + new + { + Id = new Guid("5fdb10e8-38c0-5084-9a3f-b369a960b9c2"), + AbbrPosition = "RB", + DateOfBirth = new DateTime(1998, 1, 12, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Juan", + LastName = "Foyth", + League = "La Liga", + MiddleName = "Marcos", + Position = "Right-Back", + SquadNumber = 2, + Starting11 = false, + Team = "Villarreal" + }, + new + { + Id = new Guid("bbd441f7-fcfb-5834-8468-2a9004b64c8c"), + AbbrPosition = "RB", + DateOfBirth = new DateTime(1997, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Gonzalo", + LastName = "Montiel", + League = "Premier League", + MiddleName = "Ariel", + Position = "Right-Back", + SquadNumber = 4, + Starting11 = false, + Team = "Nottingham Forest" + }, + new + { + Id = new Guid("d8bfea25-f189-5d5e-b3a5-ed89329b9f7c"), + AbbrPosition = "CB", + DateOfBirth = new DateTime(1991, 6, 27, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Germán", + LastName = "Pezzella", + League = "La Liga", + MiddleName = "Alejo", + Position = "Centre-Back", + SquadNumber = 6, + Starting11 = false, + Team = "Real Betis Balompié" + }, + new + { + Id = new Guid("dca343a8-12e5-53d6-89a8-916b120a5ee4"), + AbbrPosition = "LB", + DateOfBirth = new DateTime(1991, 10, 28, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Marcos", + LastName = "Acuña", + League = "La Liga", + MiddleName = "Javier", + Position = "Left-Back", + SquadNumber = 8, + Starting11 = false, + Team = "Sevilla FC" + }, + new + { + Id = new Guid("98306555-a466-5d18-804e-dc82175e697b"), + AbbrPosition = "CB", + DateOfBirth = new DateTime(1998, 1, 18, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Lisandro", + LastName = "Martínez", + League = "Premier League", + Position = "Centre-Back", + SquadNumber = 25, + Starting11 = false, + Team = "Manchester United" + }, + new + { + Id = new Guid("9d140400-196f-55d8-86e1-e0b96a375c83"), + AbbrPosition = "DM", + DateOfBirth = new DateTime(1994, 6, 29, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Leandro", + LastName = "Paredes", + League = "Serie A", + MiddleName = "Daniel", + Position = "Defensive Midfield", + SquadNumber = 5, + Starting11 = false, + Team = "AS Roma" + }, + new + { + Id = new Guid("d3b0e8e8-2c34-531a-b608-b24fed0ef986"), + AbbrPosition = "CM", + DateOfBirth = new DateTime(1998, 10, 5, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Exequiel", + LastName = "Palacios", + League = "Bundesliga", + MiddleName = "Alejandro", + Position = "Central Midfield", + SquadNumber = 14, + Starting11 = false, + Team = "Bayer 04 Leverkusen" + }, + new + { + Id = new Guid("7cc8d527-56a2-58bd-9528-2618fc139d30"), + AbbrPosition = "LW", + DateOfBirth = new DateTime(1988, 2, 15, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Alejandro", + LastName = "Gómez", + League = "Serie A", + MiddleName = "Darío", + Position = "Left Winger", + SquadNumber = 17, + Starting11 = false, + Team = "AC Monza" + }, + new + { + Id = new Guid("191c82af-0c51-526a-b903-c3600b61b506"), + AbbrPosition = "DM", + DateOfBirth = new DateTime(1994, 4, 12, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Guido", + LastName = "Rodríguez", + League = "La Liga", + Position = "Defensive Midfield", + SquadNumber = 18, + Starting11 = false, + Team = "Real Betis Balompié" + }, + new + { + Id = new Guid("b1306b7b-a3a4-5f7c-90fd-dd5bdbed57ba"), + AbbrPosition = "RW", + DateOfBirth = new DateTime(1995, 3, 9, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Ángel", + LastName = "Correa", + League = "La Liga", + MiddleName = "Martín", + Position = "Right Winger", + SquadNumber = 15, + Starting11 = false, + Team = "Atlético Madrid" + }, + new + { + Id = new Guid("ecec27e8-487b-5622-b116-0855020477ed"), + AbbrPosition = "AM", + DateOfBirth = new DateTime(2001, 4, 26, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Thiago", + LastName = "Almada", + League = "Major League Soccer", + MiddleName = "Ezequiel", + Position = "Attacking Midfield", + SquadNumber = 16, + Starting11 = false, + Team = "Atlanta United FC" + }, + new + { + Id = new Guid("7941cd7c-4df1-5952-97e8-1e7f5d08e8aa"), + AbbrPosition = "SS", + DateOfBirth = new DateTime(1993, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Paulo", + LastName = "Dybala", + League = "Serie A", + MiddleName = "Exequiel", + Position = "Second Striker", + SquadNumber = 21, + Starting11 = false, + Team = "AS Roma" + }, + new + { + Id = new Guid("79c96f29-c59f-5f98-96b8-3a5946246624"), + AbbrPosition = "CF", + DateOfBirth = new DateTime(1997, 8, 22, 0, 0, 0, 0, DateTimeKind.Utc), + FirstName = "Lautaro", + LastName = "Martínez", + League = "Serie A", + MiddleName = "Javier", + Position = "Centre-Forward", + SquadNumber = 22, + Starting11 = false, + Team = "Inter Milan" + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/ProviderSpecificMigrationsAssembly.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/ProviderSpecificMigrationsAssembly.cs new file mode 100644 index 0000000..aca86ad --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Migrations/ProviderSpecificMigrationsAssembly.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Reflection; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +#pragma warning disable EF1001 // Internal EF Core API usage. +using Microsoft.EntityFrameworkCore.Migrations.Internal; + +namespace Dotnet.Samples.AspNetCore.WebApi.Migrations; + +/// +/// Filters the available migrations to only those belonging to the active database provider's +/// namespace, ensuring SQLite and PostgreSQL each apply their own migration set. +/// +#pragma warning disable CS9107 // currentContext is intentionally captured by both the subclass and the base class +public class ProviderSpecificMigrationsAssembly( + ICurrentDbContext currentContext, + IDbContextOptions options, + IMigrationsIdGenerator idGenerator, + IDiagnosticsLogger logger +) : MigrationsAssembly(currentContext, options, idGenerator, logger) +#pragma warning restore CS9107 +{ + private const string SqliteNamespace = "Dotnet.Samples.AspNetCore.WebApi.Migrations"; + private const string NpgsqlNamespace = "Dotnet.Samples.AspNetCore.WebApi.Migrations.Npgsql"; + + public override IReadOnlyDictionary Migrations + { + get + { + var providerName = currentContext.Context.Database.ProviderName ?? string.Empty; + var targetNamespace = providerName.Contains( + "Sqlite", + StringComparison.OrdinalIgnoreCase + ) + ? SqliteNamespace + : NpgsqlNamespace; + + return new SortedDictionary( + base.Migrations.Where(m => m.Value.Namespace == targetNamespace) + .ToDictionary(m => m.Key, m => m.Value) + ); + } + } +} +#pragma warning restore EF1001 diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Models/Player.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Models/Player.cs new file mode 100644 index 0000000..8b5b105 --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Models/Player.cs @@ -0,0 +1,67 @@ +using System.ComponentModel.DataAnnotations; + +namespace Dotnet.Samples.AspNetCore.WebApi.Models; + +/// +/// Model for Player entity. +/// +/// +/// This class represents the Player entity in the database. +/// +public class Player +{ + /// + /// The unique identifier for the Player. + /// + public Guid Id { get; set; } = Guid.NewGuid(); + + /// + /// The first name of the Player. + /// + public string? FirstName { get; set; } + + /// + /// The middle name of the Player, if any. + /// + public string? MiddleName { get; set; } + + /// + /// The last name of the Player. + /// + public string? LastName { get; set; } + + /// + /// The date of birth of the Player. + /// + public DateTime? DateOfBirth { get; set; } + + /// + /// The squad number assigned to the Player. + /// + public int SquadNumber { get; set; } + + /// + /// The playing position of the Player. + /// + public string? Position { get; set; } + + /// + /// The abbreviated form of the Player's position. + /// + public string? AbbrPosition { get; set; } + + /// + /// The team to which the Player belongs. + /// + public string? Team { get; set; } + + /// + /// The league where the team plays. + /// + public string? League { get; set; } + + /// + /// Indicates whether the Player is in the starting 11. + /// + public bool Starting11 { get; set; } +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Models/PlayerRequestModel.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Models/PlayerRequestModel.cs new file mode 100644 index 0000000..3046dd5 --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Models/PlayerRequestModel.cs @@ -0,0 +1,54 @@ +using System.ComponentModel.DataAnnotations; + +namespace Dotnet.Samples.AspNetCore.WebApi.Models; + +/// +/// Model for Player request. +/// +/// +/// This class is used to receive Player data from the client. +/// The properties are decorated with validation attributes to ensure that +/// the required fields are provided and that the data is in the correct format. +/// +public class PlayerRequestModel +{ + /// + /// The first name of the Player. + /// + public string? FirstName { get; set; } + + /// + /// The middle name of the Player, if any. + /// + public string? MiddleName { get; set; } + + /// + /// The last name of the Player. + /// + public string? LastName { get; set; } + + /// + /// The date of birth of the Player. + /// + public DateTime? DateOfBirth { get; set; } + + /// + /// The squad number assigned to the Player (required). + /// + public required int SquadNumber { get; set; } + + /// + /// The abbreviated form of the Player's position. + /// + public string? AbbrPosition { get; set; } + + /// + /// The team to which the Player belongs. + /// + public string? Team { get; set; } + + /// + /// The league where the team plays. + /// + public string? League { get; set; } +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Models/PlayerResponseModel.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Models/PlayerResponseModel.cs new file mode 100644 index 0000000..3f3c4bf --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Models/PlayerResponseModel.cs @@ -0,0 +1,47 @@ +using System.ComponentModel.DataAnnotations; + +namespace Dotnet.Samples.AspNetCore.WebApi.Models; + +/// +/// Model for Player response. +/// +/// +/// This class is used to send Player data to the client. +/// +public class PlayerResponseModel +{ + /// + /// The full name of the Player (combined first, middle, and last names). + /// + public string? FullName { get; set; } + + /// + /// The formatted birth date of the Player. + /// + public string? Birth { get; set; } + + /// + /// The squad number (dorsal) of the Player. + /// + public int Dorsal { get; set; } + + /// + /// The playing position of the Player. + /// + public string? Position { get; set; } + + /// + /// The club (team) to which the Player belongs. + /// + public string? Club { get; set; } + + /// + /// The league where the club plays. + /// + public string? League { get; set; } + + /// + /// Indicates whether the Player is in the starting 11 (formatted as string). + /// + public string? Starting11 { get; set; } +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Program.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Program.cs new file mode 100644 index 0000000..08e750c --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Program.cs @@ -0,0 +1,129 @@ +using Dotnet.Samples.AspNetCore.WebApi.Data; +using Dotnet.Samples.AspNetCore.WebApi.Extensions; +using Microsoft.EntityFrameworkCore; +using Serilog; + +/* ----------------------------------------------------------------------------- + * Web Application + * Registers all services into the DI container before the application is built. + * Throughout this section, builder.Services refers to the ASP.NET Core + * dependency injection (DI) container — not to be confused with the Services + * subsection below, which registers our own application-level business logic. + * https://learn.microsoft.com/en-us/aspnet/core/fundamentals/startup + * -------------------------------------------------------------------------- */ + +var builder = WebApplication.CreateBuilder(args); + +/* Configurations ----------------------------------------------------------- */ + +builder + .Configuration.SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddEnvironmentVariables(); + +/* Logging ------------------------------------------------------------------ */ + +Log.Logger = new LoggerConfiguration().ReadFrom.Configuration(builder.Configuration).CreateLogger(); +builder.Host.UseSerilog(); + +/* Infrastructure ----------------------------------------------------------- */ + +builder.Services.AddHealthChecks(); +builder.Services.AddCorsDefaultPolicy(builder.Environment); +builder.Services.AddFixedWindowRateLimiter(builder.Configuration); + +if (builder.Environment.IsDevelopment()) +{ + builder.Services.AddSwaggerConfiguration(builder.Configuration); +} + +/* Controllers -------------------------------------------------------------- */ + +builder.Services.AddControllers(); +builder.Services.AddValidators(); + +/* Services (Business Logic) ------------------------------------------------ */ + +builder.Services.RegisterPlayerService(); +builder.Services.AddMemoryCache(); +builder.Services.AddMappings(); + +/* Repositories ------------------------------------------------------------- */ + +builder.Services.RegisterPlayerRepository(); + +/* Data --------------------------------------------------------------------- */ + +builder.Services.AddPlayerDbContext(builder.Environment); + +var app = builder.Build(); + +/* ----------------------------------------------------------------------------- + * Database Migration + * Applies pending EF Core migrations at startup, before the app accepts requests. + * https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/applying#apply-migrations-at-runtime + * -------------------------------------------------------------------------- */ + +await using (var scope = app.Services.CreateAsyncScope()) +{ + var db = scope.ServiceProvider.GetRequiredService(); + await db.Database.MigrateAsync(); +} + +/* ----------------------------------------------------------------------------- + * Middlewares + * Defines the order in which middleware components process each HTTP request. + * https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware + * https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware#middleware-order + * -------------------------------------------------------------------------- */ + +// Replaces the default ASP.NET Core request logging with Serilog's structured +// logging, emitting one log entry per request with timing, status code, and +// other contextual properties. +app.UseSerilogRequestLogging(); + +// Custom middleware that catches unhandled exceptions and returns a consistent +// RFC 7807 Problem Details response instead of exposing a raw stack trace. +app.UseExceptionHandling(); + +// Redirects all plain HTTP requests to HTTPS, enforcing transport security. +app.UseHttpsRedirection(); + +// DisableRateLimiting() exempts the health check endpoint from the global rate +// limiter so that monitoring and orchestration systems can always assess +// liveness and readiness without being throttled. +app.MapHealthChecks("/health").DisableRateLimiting(); + +// Enforces the fixed-window rate limiting policy defined during service +// registration, returning 429 Too Many Requests when the limit is exceeded. +app.UseRateLimiter(); + +if (app.Environment.IsDevelopment()) +{ + // Only active in Development, where AddCorsDefaultPolicy registers a + // permissive wildcard policy for Swagger UI and local frontends. No policy + // exists in Production — the API is assumed to be consumed server-to-server + // or behind a reverse proxy on the same origin, where CORS is not needed. + // Must precede MapControllers so CORS headers are applied before any + // endpoint executes, consistent with the standard middleware pipeline order. + app.UseCors(); + + // Generates the OpenAPI JSON document consumed by Swagger UI. + app.UseSwagger(); + + // Serves the interactive Swagger UI at /swagger, allowing manual + // exploration and testing of the API endpoints during development. + app.UseSwaggerUI(); +} + +// Routes incoming HTTP requests to the matching controller actions. Must come +// after all middleware that needs to run before endpoint execution (CORS, rate +// limiting, etc.). +app.MapControllers(); + +await app.RunAsync(); + +public partial class Program +{ + protected Program() { } +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Properties/launchSettings.json b/src/Dotnet.Samples.AspNetCore.WebApi/Properties/launchSettings.json new file mode 100644 index 0000000..6bafff8 --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Dotnet.Samples.AspNetCore.WebApi": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:56530;http://localhost:56531" + } + } +} \ No newline at end of file diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Repositories/IPlayerRepository.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Repositories/IPlayerRepository.cs new file mode 100644 index 0000000..b082c00 --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Repositories/IPlayerRepository.cs @@ -0,0 +1,21 @@ +// IPlayerRepository.cs + +using Dotnet.Samples.AspNetCore.WebApi.Models; + +namespace Dotnet.Samples.AspNetCore.WebApi.Repositories; + +/// +/// Provides specialized repository operations for Player entities. +/// +public interface IPlayerRepository : IRepository +{ + /// + /// Finds a Player in the repository by their Squad Number. + /// + /// The Squad Number of the Player to retrieve. + /// + /// A Task representing the asynchronous operation,containing the Player + /// if found, or null if no Player with the specified Squad Number exists. + /// + Task FindBySquadNumberAsync(int squadNumber); +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Repositories/IRepository.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Repositories/IRepository.cs new file mode 100644 index 0000000..b65da42 --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Repositories/IRepository.cs @@ -0,0 +1,47 @@ +namespace Dotnet.Samples.AspNetCore.WebApi.Repositories; + +/// +/// Provides generic repository operations for entities of type . +/// +/// The entity type managed by this repository. +public interface IRepository + where T : class +{ + /// + /// Adds a new entity to the repository. + /// + /// The entity to create. + /// A Task representing the asynchronous operation. + Task AddAsync(T entity); + + /// + /// Gets all entities from the repository. + /// + /// A Task representing the asynchronous operation, + /// containing a list of all entities. + Task> GetAllAsync(); + + /// + /// Finds an entity on the repository by its unique identifier. + /// + /// The unique identifier of the entity to retrieve. + /// + /// A ValueTask representing the asynchronous operation, containing the entity if found, + /// or null if no entity with the specified ID exists. + /// + ValueTask FindByIdAsync(Guid id); + + /// + /// Updates an existing entity in the repository. + /// + /// The entity with updated values. + /// A Task representing the asynchronous operation. + Task UpdateAsync(T entity); + + /// + /// Removes an entity from the repository by its unique identifier. + /// + /// The unique identifier of the entity to remove. + /// A Task representing the asynchronous operation. + Task RemoveAsync(Guid id); +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Repositories/PlayerRepository.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Repositories/PlayerRepository.cs new file mode 100644 index 0000000..2863e77 --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Repositories/PlayerRepository.cs @@ -0,0 +1,18 @@ +using Dotnet.Samples.AspNetCore.WebApi.Data; +using Dotnet.Samples.AspNetCore.WebApi.Models; +using Microsoft.EntityFrameworkCore; + +namespace Dotnet.Samples.AspNetCore.WebApi.Repositories; + +public sealed class PlayerRepository(PlayerDbContext dbContext) + : Repository(dbContext), + IPlayerRepository +{ + public async Task FindBySquadNumberAsync(int squadNumber) => + await _dbSet.FirstOrDefaultAsync(p => p.SquadNumber == squadNumber); + + public async Task SquadNumberExistsAsync(int squadNumber) + { + return await dbContext.Players.AnyAsync(p => p.SquadNumber == squadNumber); + } +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Repositories/Repository.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Repositories/Repository.cs new file mode 100644 index 0000000..10d4d25 --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Repositories/Repository.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore; + +namespace Dotnet.Samples.AspNetCore.WebApi.Repositories; + +public class Repository(DbContext dbContext) : IRepository + where T : class +{ + protected readonly DbSet _dbSet = dbContext.Set(); + + public async Task AddAsync(T entity) + { + await _dbSet.AddAsync(entity); + await dbContext.SaveChangesAsync(); + } + + public async Task> GetAllAsync() => await _dbSet.AsNoTracking().ToListAsync(); + + public async ValueTask FindByIdAsync(Guid id) => await _dbSet.FindAsync(id); + + public async Task UpdateAsync(T entity) + { + _dbSet.Update(entity); + await dbContext.SaveChangesAsync(); + } + + public async Task RemoveAsync(Guid id) + { + var entity = await _dbSet.FindAsync(id); + if (entity != null) + { + _dbSet.Remove(entity); + await dbContext.SaveChangesAsync(); + } + } +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Services/IPlayerService.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Services/IPlayerService.cs new file mode 100644 index 0000000..0e70130 --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Services/IPlayerService.cs @@ -0,0 +1,59 @@ +using Dotnet.Samples.AspNetCore.WebApi.Models; + +namespace Dotnet.Samples.AspNetCore.WebApi.Services +{ + /// + /// Interface for managing Player entities in the repository. + /// + public interface IPlayerService + { + /// + /// Adds a new Player to the repository. + /// + /// The Player to create. + /// A Task representing the asynchronous operation, + /// containing the created Player. + public Task CreateAsync(PlayerRequestModel playerRequestModel); + + /// + /// Retrieves all players from the repository. + /// + /// A Task representing the asynchronous operation, + /// containing a list of all players. + public Task> RetrieveAsync(); + + /// + /// Retrieves a Player from the repository by its ID. + /// + /// The ID of the Player to retrieve. + /// + /// A Task representing the asynchronous operation, containing the Player if found, + /// or null if not. + /// + public Task RetrieveByIdAsync(Guid id); + + /// + /// Retrieves a Player from the repository by its Squad Number. + /// + /// The Squad Number of the Player to retrieve. + /// + /// A Task representing the asynchronous operation, containing the Player if found, + /// or null if not. + /// + public Task RetrieveBySquadNumberAsync(int squadNumber); + + /// + /// Updates (entirely) an existing Player in the repository. + /// + /// The Player to update. + /// A Task representing the asynchronous operation. + public Task UpdateAsync(PlayerRequestModel playerRequestModel); + + /// + /// Removes an existing Player from the repository. + /// + /// The Squad Number of the Player to delete. + /// A Task representing the asynchronous operation. + public Task DeleteAsync(int squadNumber); + } +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Services/PlayerService.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Services/PlayerService.cs new file mode 100644 index 0000000..47df663 --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Services/PlayerService.cs @@ -0,0 +1,150 @@ +using AutoMapper; +using Dotnet.Samples.AspNetCore.WebApi.Models; +using Dotnet.Samples.AspNetCore.WebApi.Repositories; +using Microsoft.Extensions.Caching.Memory; + +namespace Dotnet.Samples.AspNetCore.WebApi.Services; + +public class PlayerService( + IPlayerRepository playerRepository, + ILogger logger, + IMemoryCache memoryCache, + IMapper mapper, + IHostEnvironment environment +) : IPlayerService +{ + /// + /// Creates a MemoryCacheEntryOptions instance with Normal priority, + /// SlidingExpiration of 10 minutes and AbsoluteExpiration of 1 hour. + /// + private static readonly MemoryCacheEntryOptions CacheEntryOptions = + new MemoryCacheEntryOptions() + .SetPriority(CacheItemPriority.Normal) + .SetSlidingExpiration(TimeSpan.FromMinutes(10)) + .SetAbsoluteExpiration(TimeSpan.FromHours(1)); + + /// + /// The key used to store the list of Players in the cache. + /// + private static readonly string CacheKey_RetrieveAsync = nameof(RetrieveAsync); + + /* ------------------------------------------------------------------------- + * Create + * ---------------------------------------------------------------------- */ + + public async Task CreateAsync(PlayerRequestModel playerRequestModel) + { + var player = mapper.Map(playerRequestModel); + await playerRepository.AddAsync(player); + logger.LogInformation("Player added to Repository: {Player}", player); + memoryCache.Remove(CacheKey_RetrieveAsync); + logger.LogInformation("Removed objects from Cache with Key: {Key}", CacheKey_RetrieveAsync); + return mapper.Map(player); + } + + /* ------------------------------------------------------------------------- + * Retrieve + * ---------------------------------------------------------------------- */ + + public async Task> RetrieveAsync() + { + if (memoryCache.TryGetValue(CacheKey_RetrieveAsync, out List? cached)) + { + logger.LogInformation("Players retrieved from Cache"); + return cached!; + } + else + { + if (environment.IsDevelopment()) + { + await SimulateRepositoryDelayAsync(); + } + var players = await playerRepository.GetAllAsync(); + logger.LogInformation("Players retrieved from Repository"); + var playerResponseModels = mapper.Map>(players); + using (var cacheEntry = memoryCache.CreateEntry(CacheKey_RetrieveAsync)) + { + logger.LogInformation( + "{Count} entries created in Cache with key: {Key}", + playerResponseModels.Count, + CacheKey_RetrieveAsync + ); + cacheEntry.SetSize(playerResponseModels.Count); + cacheEntry.Value = playerResponseModels; + cacheEntry.SetOptions(CacheEntryOptions); + } + return playerResponseModels; + } + } + + public async Task RetrieveByIdAsync(Guid id) + { + var player = await playerRepository.FindByIdAsync(id); + return player is not null ? mapper.Map(player) : null; + } + + public async Task RetrieveBySquadNumberAsync(int squadNumber) + { + var player = await playerRepository.FindBySquadNumberAsync(squadNumber); + return player is not null ? mapper.Map(player) : null; + } + + /* ------------------------------------------------------------------------- + * Update + * ---------------------------------------------------------------------- */ + + public async Task UpdateAsync(PlayerRequestModel playerRequestModel) + { + if ( + await playerRepository.FindBySquadNumberAsync(playerRequestModel.SquadNumber) + is Player player + ) + { + mapper.Map(playerRequestModel, player); + await playerRepository.UpdateAsync(player); + logger.LogInformation("Player updated in Repository: {Player}", player); + memoryCache.Remove(CacheKey_RetrieveAsync); + logger.LogInformation( + "Removed objects from Cache with Key: {Key}", + CacheKey_RetrieveAsync + ); + } + } + + /* ------------------------------------------------------------------------- + * Delete + * ---------------------------------------------------------------------- */ + + public async Task DeleteAsync(int squadNumber) + { + if (await playerRepository.FindBySquadNumberAsync(squadNumber) is Player player) + { + await playerRepository.RemoveAsync(player.Id); + logger.LogInformation( + "Player with Id {SquadNumber} removed from Repository", + squadNumber + ); + memoryCache.Remove(CacheKey_RetrieveAsync); + logger.LogInformation( + "Removed objects from Cache with Key: {Key}", + CacheKey_RetrieveAsync + ); + } + } + + /// + /// Simulates a delay in the repository call to mimic a long-running operation. + /// This is only used in the Development environment to simulate a delay + /// in the repository call. In production, this method should not be called. + /// + /// A Task representing the asynchronous operation. + private async Task SimulateRepositoryDelayAsync() + { + var milliseconds = new Random().Next(2600, 4200); + logger.LogInformation( + "Simulating a random delay of {Milliseconds} milliseconds...", + milliseconds + ); + await Task.Delay(milliseconds); + } +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Utilities/HttpContextUtilities.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Utilities/HttpContextUtilities.cs new file mode 100644 index 0000000..e76cfce --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Utilities/HttpContextUtilities.cs @@ -0,0 +1,47 @@ +using System.Net; + +namespace Dotnet.Samples.AspNetCore.WebApi.Utilities; + +/// +/// Utility class for HTTP context operations. +/// +public static class HttpContextUtilities +{ + /// + /// This method checks for the "X-Forwarded-For" and "X-Real-IP" headers, + /// which are commonly used by proxies to forward the original client IP address. + /// If these headers are not present or the IP address cannot be parsed, + /// it falls back to the remote IP address from the connection. + /// If no valid IP address can be determined, it returns "unknown". + /// + /// The HTTP context. + /// The client IP address or "unknown" if not available. + public static string ExtractIpAddress(HttpContext httpContext) + { + ArgumentNullException.ThrowIfNull(httpContext); + + var headers = httpContext.Request.Headers; + IPAddress? ipAddress; + + if (headers.TryGetValue("X-Forwarded-For", out var xForwardedFor)) + { + var clientIp = xForwardedFor + .ToString() + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .FirstOrDefault(); + + if (!string.IsNullOrWhiteSpace(clientIp) && IPAddress.TryParse(clientIp, out ipAddress)) + return ipAddress.ToString(); + } + + if ( + headers.TryGetValue("X-Real-IP", out var xRealIp) + && IPAddress.TryParse(xRealIp.ToString(), out ipAddress) + ) + { + return ipAddress.ToString(); + } + + return httpContext.Connection.RemoteIpAddress?.ToString() ?? $"unknown-{Guid.NewGuid()}"; + } +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Utilities/PlayerData.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Utilities/PlayerData.cs new file mode 100644 index 0000000..83b7262 --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Utilities/PlayerData.cs @@ -0,0 +1,917 @@ +using System.Text.Json; +using Dotnet.Samples.AspNetCore.WebApi.Enums; +using Dotnet.Samples.AspNetCore.WebApi.Models; + +namespace Dotnet.Samples.AspNetCore.WebApi.Utilities; + +/// +/// Provides static player data for database seeding and testing. +/// Single source of truth for all player definitions. +/// +public static class PlayerData +{ + /// + /// Returns the starting 11 players without IDs (for EF Core auto-increment). + /// Used for database migrations and seeding. + /// + /// List of 11 Player entities representing the starting lineup. + public static List MakeStarting11() + { + return + [ + new() + { + FirstName = "Damián", + MiddleName = "Emiliano", + LastName = "Martínez", + DateOfBirth = new DateTime(1992, 9, 1, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 23, + Position = Position.Goalkeeper.Text, + AbbrPosition = Position.Goalkeeper.Abbr, + Team = "Aston Villa FC", + League = "Premier League", + Starting11 = true, + }, + new() + { + FirstName = "Nahuel", + LastName = "Molina", + DateOfBirth = new DateTime(1998, 4, 5, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 26, + Position = Position.RightBack.Text, + AbbrPosition = Position.RightBack.Abbr, + Team = "Altético Madrid", + League = "La Liga", + Starting11 = true, + }, + new() + { + FirstName = "Cristian", + MiddleName = "Gabriel", + LastName = "Romero", + DateOfBirth = new DateTime(1998, 4, 26, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 13, + Position = Position.CentreBack.Text, + AbbrPosition = Position.CentreBack.Abbr, + Team = "Tottenham Hotspur", + League = "Premier League", + Starting11 = true, + }, + new() + { + FirstName = "Nicolás", + MiddleName = "Hernán Gonzalo", + LastName = "Otamendi", + DateOfBirth = new DateTime(1988, 2, 11, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 19, + Position = Position.CentreBack.Text, + AbbrPosition = Position.CentreBack.Abbr, + Team = "SL Benfica", + League = "Liga Portugal", + Starting11 = true, + }, + new() + { + FirstName = "Nicolás", + MiddleName = "Alejandro", + LastName = "Tagliafico", + DateOfBirth = new DateTime(1992, 8, 30, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 3, + Position = Position.LeftBack.Text, + AbbrPosition = Position.LeftBack.Abbr, + Team = "Olympique Lyon", + League = "Ligue 1", + Starting11 = true, + }, + new() + { + FirstName = "Ángel", + MiddleName = "Fabián", + LastName = "Di María", + DateOfBirth = new DateTime(1988, 2, 13, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 11, + Position = Position.RightWinger.Text, + AbbrPosition = Position.RightWinger.Abbr, + Team = "SL Benfica", + League = "Liga Portugal", + Starting11 = true, + }, + new() + { + FirstName = "Rodrigo", + MiddleName = "Javier", + LastName = "de Paul", + DateOfBirth = new DateTime(1994, 5, 23, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 7, + Position = Position.CentralMidfield.Text, + AbbrPosition = Position.CentralMidfield.Abbr, + Team = "Altético Madrid", + League = "La Liga", + Starting11 = true, + }, + new() + { + FirstName = "Enzo", + MiddleName = "Jeremías", + LastName = "Fernández", + DateOfBirth = new DateTime(2001, 1, 16, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 24, + Position = Position.CentralMidfield.Text, + AbbrPosition = Position.CentralMidfield.Abbr, + Team = "SL Benfica", + League = "Liga Portugal", + Starting11 = true, + }, + new() + { + FirstName = "Alexis", + LastName = "Mac Allister", + DateOfBirth = new DateTime(1998, 12, 23, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 20, + Position = Position.CentralMidfield.Text, + AbbrPosition = Position.CentralMidfield.Abbr, + Team = "Brighton & Hove Albion", + League = "Premier League", + Starting11 = true, + }, + new() + { + FirstName = "Lionel", + MiddleName = "Andrés", + LastName = "Messi", + DateOfBirth = new DateTime(1987, 6, 23, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 10, + Position = Position.RightWinger.Text, + AbbrPosition = Position.RightWinger.Abbr, + Team = "Paris Saint-Germain", + League = "Ligue 1", + Starting11 = true, + }, + new() + { + FirstName = "Julián", + LastName = "Álvarez", + DateOfBirth = new DateTime(2000, 1, 30, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 9, + Position = Position.CentreForward.Text, + AbbrPosition = Position.CentreForward.Abbr, + Team = "Manchester City", + League = "Premier League", + Starting11 = true, + }, + ]; + } + + /// + /// Create a predefined starting eleven of players where each player has a fixed GUID identifier and full profile data. + /// + /// A list of 11 Player instances representing the starting lineup; each entry includes a predefined Id (Guid) and populated fields such as name, date of birth, squad number, position (and abbreviation), team, league, and Starting11 set to true. + public static List MakeStarting11WithId() + { + return + [ + new() + { + Id = Guid.Parse("01772c59-43f0-5d85-b913-c78e4e281452"), + FirstName = "Damián", + MiddleName = "Emiliano", + LastName = "Martínez", + DateOfBirth = new DateTime(1992, 9, 1, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 23, + Position = Position.Goalkeeper.Text, + AbbrPosition = Position.Goalkeeper.Abbr, + Team = "Aston Villa FC", + League = "Premier League", + Starting11 = true, + }, + new() + { + Id = Guid.Parse("da31293b-4c7e-5e0f-a168-469ee29ecbc4"), + FirstName = "Nahuel", + LastName = "Molina", + DateOfBirth = new DateTime(1998, 4, 5, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 26, + Position = Position.RightBack.Text, + AbbrPosition = Position.RightBack.Abbr, + Team = "Altético Madrid", + League = "La Liga", + Starting11 = true, + }, + new() + { + Id = Guid.Parse("c096c69e-762b-5281-9290-bb9c167a24a0"), + FirstName = "Cristian", + MiddleName = "Gabriel", + LastName = "Romero", + DateOfBirth = new DateTime(1998, 4, 26, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 13, + Position = Position.CentreBack.Text, + AbbrPosition = Position.CentreBack.Abbr, + Team = "Tottenham Hotspur", + League = "Premier League", + Starting11 = true, + }, + new() + { + Id = Guid.Parse("d5f7dd7a-1dcb-5960-ba27-e34865b63358"), + FirstName = "Nicolás", + MiddleName = "Hernán Gonzalo", + LastName = "Otamendi", + DateOfBirth = new DateTime(1988, 2, 11, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 19, + Position = Position.CentreBack.Text, + AbbrPosition = Position.CentreBack.Abbr, + Team = "SL Benfica", + League = "Liga Portugal", + Starting11 = true, + }, + new() + { + Id = Guid.Parse("2f6f90a0-9b9d-5023-96d2-a2aaf03143a6"), + FirstName = "Nicolás", + MiddleName = "Alejandro", + LastName = "Tagliafico", + DateOfBirth = new DateTime(1992, 8, 30, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 3, + Position = Position.LeftBack.Text, + AbbrPosition = Position.LeftBack.Abbr, + Team = "Olympique Lyon", + League = "Ligue 1", + Starting11 = true, + }, + new() + { + Id = Guid.Parse("b5b46e79-929e-5ed2-949d-0d167109c022"), + FirstName = "Ángel", + MiddleName = "Fabián", + LastName = "Di María", + DateOfBirth = new DateTime(1988, 2, 13, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 11, + Position = Position.RightWinger.Text, + AbbrPosition = Position.RightWinger.Abbr, + Team = "SL Benfica", + League = "Liga Portugal", + Starting11 = true, + }, + new() + { + Id = Guid.Parse("0293b282-1da8-562e-998e-83849b417a42"), + FirstName = "Rodrigo", + MiddleName = "Javier", + LastName = "de Paul", + DateOfBirth = new DateTime(1994, 5, 23, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 7, + Position = Position.CentralMidfield.Text, + AbbrPosition = Position.CentralMidfield.Abbr, + Team = "Altético Madrid", + League = "La Liga", + Starting11 = true, + }, + new() + { + Id = Guid.Parse("d3ba552a-dac3-588a-b961-1ea7224017fd"), + FirstName = "Enzo", + MiddleName = "Jeremías", + LastName = "Fernández", + DateOfBirth = new DateTime(2001, 1, 16, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 24, + Position = Position.CentralMidfield.Text, + AbbrPosition = Position.CentralMidfield.Abbr, + Team = "SL Benfica", + League = "Liga Portugal", + Starting11 = true, + }, + new() + { + Id = Guid.Parse("9613cae9-16ab-5b54-937e-3135123b9e0d"), + FirstName = "Alexis", + LastName = "Mac Allister", + DateOfBirth = new DateTime(1998, 12, 23, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 20, + Position = Position.CentralMidfield.Text, + AbbrPosition = Position.CentralMidfield.Abbr, + Team = "Brighton & Hove Albion", + League = "Premier League", + Starting11 = true, + }, + new() + { + Id = Guid.Parse("acc433bf-d505-51fe-831e-45eb44c4d43c"), + FirstName = "Lionel", + MiddleName = "Andrés", + LastName = "Messi", + DateOfBirth = new DateTime(1987, 6, 23, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 10, + Position = Position.RightWinger.Text, + AbbrPosition = Position.RightWinger.Abbr, + Team = "Paris Saint-Germain", + League = "Ligue 1", + Starting11 = true, + }, + new() + { + Id = Guid.Parse("38bae91d-8519-55a2-b30a-b9fe38849bfb"), + FirstName = "Julián", + LastName = "Álvarez", + DateOfBirth = new DateTime(2000, 1, 30, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 9, + Position = Position.CentreForward.Text, + AbbrPosition = Position.CentreForward.Abbr, + Team = "Manchester City", + League = "Premier League", + Starting11 = true, + }, + ]; + } + + /// + /// Create a predefined list of 15 substitute players with full profile data but without Id values. + /// + /// A list of 15 Player instances representing substitute players; each entry includes populated fields such as name, date of birth, squad number, position (and abbreviation), team, league, and Starting11 set to false. Id values are not assigned. + public static List GetSubstitutes() + { + return + [ + new() + { + FirstName = "Franco", + MiddleName = "Daniel", + LastName = "Armani", + DateOfBirth = new DateTime(1986, 10, 16, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 1, + Position = Position.Goalkeeper.Text, + AbbrPosition = Position.Goalkeeper.Abbr, + Team = "River Plate", + League = "Copa de la Liga", + Starting11 = false, + }, + new() + { + FirstName = "Gerónimo", + LastName = "Rulli", + DateOfBirth = new DateTime(1992, 5, 20, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 12, + Position = Position.Goalkeeper.Text, + AbbrPosition = Position.Goalkeeper.Abbr, + Team = "Ajax Amsterdam", + League = "Eredivisie", + Starting11 = false, + }, + new() + { + FirstName = "Juan", + MiddleName = "Marcos", + LastName = "Foyth", + DateOfBirth = new DateTime(1998, 1, 12, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 2, + Position = Position.RightBack.Text, + AbbrPosition = Position.RightBack.Abbr, + Team = "Villarreal", + League = "La Liga", + Starting11 = false, + }, + new() + { + FirstName = "Gonzalo", + MiddleName = "Ariel", + LastName = "Montiel", + DateOfBirth = new DateTime(1997, 1, 1, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 4, + Position = Position.RightBack.Text, + AbbrPosition = Position.RightBack.Abbr, + Team = "Nottingham Forest", + League = "Premier League", + Starting11 = false, + }, + new() + { + FirstName = "Germán", + MiddleName = "Alejo", + LastName = "Pezzella", + DateOfBirth = new DateTime(1991, 6, 27, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 6, + Position = Position.CentreBack.Text, + AbbrPosition = Position.CentreBack.Abbr, + Team = "Real Betis Balompié", + League = "La Liga", + Starting11 = false, + }, + new() + { + FirstName = "Marcos", + MiddleName = "Javier", + LastName = "Acuña", + DateOfBirth = new DateTime(1991, 10, 28, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 8, + Position = Position.LeftBack.Text, + AbbrPosition = Position.LeftBack.Abbr, + Team = "Sevilla FC", + League = "La Liga", + Starting11 = false, + }, + new() + { + FirstName = "Lisandro", + LastName = "Martínez", + DateOfBirth = new DateTime(1998, 1, 18, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 25, + Position = Position.CentreBack.Text, + AbbrPosition = Position.CentreBack.Abbr, + Team = "Manchester United", + League = "Premier League", + Starting11 = false, + }, + new() + { + FirstName = "Leandro", + MiddleName = "Daniel", + LastName = "Paredes", + DateOfBirth = new DateTime(1994, 6, 29, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 5, + Position = Position.DefensiveMidfield.Text, + AbbrPosition = Position.DefensiveMidfield.Abbr, + Team = "AS Roma", + League = "Serie A", + Starting11 = false, + }, + new() + { + FirstName = "Exequiel", + MiddleName = "Alejandro", + LastName = "Palacios", + DateOfBirth = new DateTime(1998, 10, 5, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 14, + Position = Position.CentralMidfield.Text, + AbbrPosition = Position.CentralMidfield.Abbr, + Team = "Bayer 04 Leverkusen", + League = "Bundesliga", + Starting11 = false, + }, + new() + { + FirstName = "Alejandro", + MiddleName = "Darío", + LastName = "Gómez", + DateOfBirth = new DateTime(1988, 2, 15, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 17, + Position = Position.LeftWinger.Text, + AbbrPosition = Position.LeftWinger.Abbr, + Team = "AC Monza", + League = "Serie A", + Starting11 = false, + }, + new() + { + FirstName = "Guido", + LastName = "Rodríguez", + DateOfBirth = new DateTime(1994, 4, 12, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 18, + Position = Position.DefensiveMidfield.Text, + AbbrPosition = Position.DefensiveMidfield.Abbr, + Team = "Real Betis Balompié", + League = "La Liga", + Starting11 = false, + }, + new() + { + FirstName = "Ángel", + MiddleName = "Martín", + LastName = "Correa", + DateOfBirth = new DateTime(1995, 3, 9, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 15, + Position = Position.RightWinger.Text, + AbbrPosition = Position.RightWinger.Abbr, + Team = "Atlético Madrid", + League = "La Liga", + Starting11 = false, + }, + new() + { + FirstName = "Thiago", + MiddleName = "Ezequiel", + LastName = "Almada", + DateOfBirth = new DateTime(2001, 4, 26, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 16, + Position = Position.AttackingMidfield.Text, + AbbrPosition = Position.AttackingMidfield.Abbr, + Team = "Atlanta United FC", + League = "Major League Soccer", + Starting11 = false, + }, + new() + { + FirstName = "Paulo", + MiddleName = "Exequiel", + LastName = "Dybala", + DateOfBirth = new DateTime(1993, 11, 15, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 21, + Position = Position.SecondStriker.Text, + AbbrPosition = Position.SecondStriker.Abbr, + Team = "AS Roma", + League = "Serie A", + Starting11 = false, + }, + new() + { + FirstName = "Lautaro", + MiddleName = "Javier", + LastName = "Martínez", + DateOfBirth = new DateTime(1997, 8, 22, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 22, + Position = Position.CentreForward.Text, + AbbrPosition = Position.CentreForward.Abbr, + Team = "Inter Milan", + League = "Serie A", + Starting11 = false, + }, + new() + { + FirstName = "Giovani", + LastName = "Lo Celso", + DateOfBirth = new DateTime(1996, 4, 9, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 27, + Position = Position.CentralMidfield.Text, + AbbrPosition = Position.CentralMidfield.Abbr, + Team = "Real Betis Balompié", + League = "La Liga", + Starting11 = false, + }, + ]; + } + + /// + /// Create a predefined list of 15 substitute players where each player has a fixed GUID identifier and full profile data. + /// + /// A list of 15 Player instances representing substitute players (squads 1–26); each entry includes a predefined Id (Guid) and populated fields such as name, date of birth, squad number, position (and abbreviation), team, league, and Starting11 set to false. Lo Celso (squad 27) is intentionally excluded — his squad number falls outside the seeded range so he can serve as the canonical Create/Delete fixture without conflicting with seeded data. + public static List GetSubstitutesWithId() + { + return + [ + new() + { + Id = Guid.Parse("5a9cd988-95e6-54c1-bc34-9aa08acca8d0"), + FirstName = "Franco", + MiddleName = "Daniel", + LastName = "Armani", + DateOfBirth = new DateTime(1986, 10, 16, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 1, + Position = Position.Goalkeeper.Text, + AbbrPosition = Position.Goalkeeper.Abbr, + Team = "River Plate", + League = "Copa de la Liga", + Starting11 = false, + }, + new() + { + Id = Guid.Parse("c62f2ac1-41e8-5d34-b073-2ba0913d0e31"), + FirstName = "Gerónimo", + LastName = "Rulli", + DateOfBirth = new DateTime(1992, 5, 20, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 12, + Position = Position.Goalkeeper.Text, + AbbrPosition = Position.Goalkeeper.Abbr, + Team = "Ajax Amsterdam", + League = "Eredivisie", + Starting11 = false, + }, + new() + { + Id = Guid.Parse("5fdb10e8-38c0-5084-9a3f-b369a960b9c2"), + FirstName = "Juan", + MiddleName = "Marcos", + LastName = "Foyth", + DateOfBirth = new DateTime(1998, 1, 12, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 2, + Position = Position.RightBack.Text, + AbbrPosition = Position.RightBack.Abbr, + Team = "Villarreal", + League = "La Liga", + Starting11 = false, + }, + new() + { + Id = Guid.Parse("bbd441f7-fcfb-5834-8468-2a9004b64c8c"), + FirstName = "Gonzalo", + MiddleName = "Ariel", + LastName = "Montiel", + DateOfBirth = new DateTime(1997, 1, 1, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 4, + Position = Position.RightBack.Text, + AbbrPosition = Position.RightBack.Abbr, + Team = "Nottingham Forest", + League = "Premier League", + Starting11 = false, + }, + new() + { + Id = Guid.Parse("d8bfea25-f189-5d5e-b3a5-ed89329b9f7c"), + FirstName = "Germán", + MiddleName = "Alejo", + LastName = "Pezzella", + DateOfBirth = new DateTime(1991, 6, 27, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 6, + Position = Position.CentreBack.Text, + AbbrPosition = Position.CentreBack.Abbr, + Team = "Real Betis Balompié", + League = "La Liga", + Starting11 = false, + }, + new() + { + Id = Guid.Parse("dca343a8-12e5-53d6-89a8-916b120a5ee4"), + FirstName = "Marcos", + MiddleName = "Javier", + LastName = "Acuña", + DateOfBirth = new DateTime(1991, 10, 28, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 8, + Position = Position.LeftBack.Text, + AbbrPosition = Position.LeftBack.Abbr, + Team = "Sevilla FC", + League = "La Liga", + Starting11 = false, + }, + new() + { + Id = Guid.Parse("98306555-a466-5d18-804e-dc82175e697b"), + FirstName = "Lisandro", + LastName = "Martínez", + DateOfBirth = new DateTime(1998, 1, 18, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 25, + Position = Position.CentreBack.Text, + AbbrPosition = Position.CentreBack.Abbr, + Team = "Manchester United", + League = "Premier League", + Starting11 = false, + }, + new() + { + Id = Guid.Parse("9d140400-196f-55d8-86e1-e0b96a375c83"), + FirstName = "Leandro", + MiddleName = "Daniel", + LastName = "Paredes", + DateOfBirth = new DateTime(1994, 6, 29, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 5, + Position = Position.DefensiveMidfield.Text, + AbbrPosition = Position.DefensiveMidfield.Abbr, + Team = "AS Roma", + League = "Serie A", + Starting11 = false, + }, + new() + { + Id = Guid.Parse("d3b0e8e8-2c34-531a-b608-b24fed0ef986"), + FirstName = "Exequiel", + MiddleName = "Alejandro", + LastName = "Palacios", + DateOfBirth = new DateTime(1998, 10, 5, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 14, + Position = Position.CentralMidfield.Text, + AbbrPosition = Position.CentralMidfield.Abbr, + Team = "Bayer 04 Leverkusen", + League = "Bundesliga", + Starting11 = false, + }, + new() + { + Id = Guid.Parse("7cc8d527-56a2-58bd-9528-2618fc139d30"), + FirstName = "Alejandro", + MiddleName = "Darío", + LastName = "Gómez", + DateOfBirth = new DateTime(1988, 2, 15, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 17, + Position = Position.LeftWinger.Text, + AbbrPosition = Position.LeftWinger.Abbr, + Team = "AC Monza", + League = "Serie A", + Starting11 = false, + }, + new() + { + Id = Guid.Parse("191c82af-0c51-526a-b903-c3600b61b506"), + FirstName = "Guido", + LastName = "Rodríguez", + DateOfBirth = new DateTime(1994, 4, 12, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 18, + Position = Position.DefensiveMidfield.Text, + AbbrPosition = Position.DefensiveMidfield.Abbr, + Team = "Real Betis Balompié", + League = "La Liga", + Starting11 = false, + }, + new() + { + Id = Guid.Parse("b1306b7b-a3a4-5f7c-90fd-dd5bdbed57ba"), + FirstName = "Ángel", + MiddleName = "Martín", + LastName = "Correa", + DateOfBirth = new DateTime(1995, 3, 9, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 15, + Position = Position.RightWinger.Text, + AbbrPosition = Position.RightWinger.Abbr, + Team = "Atlético Madrid", + League = "La Liga", + Starting11 = false, + }, + new() + { + Id = Guid.Parse("ecec27e8-487b-5622-b116-0855020477ed"), + FirstName = "Thiago", + MiddleName = "Ezequiel", + LastName = "Almada", + DateOfBirth = new DateTime(2001, 4, 26, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 16, + Position = Position.AttackingMidfield.Text, + AbbrPosition = Position.AttackingMidfield.Abbr, + Team = "Atlanta United FC", + League = "Major League Soccer", + Starting11 = false, + }, + new() + { + Id = Guid.Parse("7941cd7c-4df1-5952-97e8-1e7f5d08e8aa"), + FirstName = "Paulo", + MiddleName = "Exequiel", + LastName = "Dybala", + DateOfBirth = new DateTime(1993, 11, 15, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 21, + Position = Position.SecondStriker.Text, + AbbrPosition = Position.SecondStriker.Abbr, + Team = "AS Roma", + League = "Serie A", + Starting11 = false, + }, + new() + { + Id = Guid.Parse("79c96f29-c59f-5f98-96b8-3a5946246624"), + FirstName = "Lautaro", + MiddleName = "Javier", + LastName = "Martínez", + DateOfBirth = new DateTime(1997, 8, 22, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 22, + Position = Position.CentreForward.Text, + AbbrPosition = Position.CentreForward.Abbr, + Team = "Inter Milan", + League = "Serie A", + Starting11 = false, + }, + ]; + } + + /// + /// The purpose of this method is to demonstrate the capabilities of + /// System.Text.Json.JsonSerializer + /// https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/deserialization + /// + /// A list of Players. + public static List MakeStarting11FromDeserializedJson() + { + var players = new List(); + + var json = """ + [{ + "firstName": "Damián", + "middleName": "Emiliano", + "lastName": "Martínez", + "dateOfBirth": "1992-09-01T00:00:00.000Z", + "squadNumber": 23, + "position": "Goalkeeper", + "abbrPosition": "GK", + "team": "Aston Villa FC", + "league": "Premier League", + "starting11": true + }, + { + "firstName": "Nahuel", + "middleName": null, + "lastName": "Molina", + "dateOfBirth": "1998-04-05T00:00:00.000Z", + "squadNumber": 26, + "position": "Right-Back", + "abbrPosition": "RB", + "team": "Atlético Madrid", + "league": "La Liga", + "starting11": true + }, + { + "firstName": "Cristian", + "middleName": "Gabriel", + "lastName": "Romero", + "dateOfBirth": "1998-04-26T00:00:00.000Z", + "squadNumber": 13, + "position": "Centre-Back", + "abbrPosition": "CB", + "team": "Tottenham Hotspur", + "league": "Premier League", + "starting11": true + }, + { + "firstName": "Nicolás", + "middleName": "Hernán Gonzalo", + "lastName": "Otamendi", + "dateOfBirth": "1988-02-11T00:00:00.000Z", + "squadNumber": 19, + "position": "Centre-Back", + "abbrPosition": "CB", + "team": "SL Benfica", + "league": "Liga Portugal", + "starting11": true + }, + { + "firstName": "Nicolás", + "middleName": "Alejandro", + "lastName": "Tagliafico", + "dateOfBirth": "1992-08-30T00:00:00.000Z", + "squadNumber": 3, + "position": "Left-Back", + "abbrPosition": "LB", + "team": "Olympique Lyon", + "league": "Ligue 1", + "starting11": true + }, + { + "firstName": "Ángel", + "middleName": "Fabián", + "lastName": "Di María", + "dateOfBirth": "1988-02-13T00:00:00.000Z", + "squadNumber": 11, + "position": "Right Winger", + "abbrPosition": "RW", + "team": "SL Benfica", + "league": "Liga Portugal", + "starting11": true + }, + { + "firstName": "Rodrigo", + "middleName": "Javier", + "lastName": "de Paul", + "dateOfBirth": "1994-05-23T00:00:00.000Z", + "squadNumber": 7, + "position": "Central Midfield", + "abbrPosition": "CM", + "team": "Atlético Madrid", + "league": "La Liga", + "starting11": true + }, + { + "firstName": "Enzo", + "middleName": "Jeremías", + "lastName": "Fernández", + "dateOfBirth": "2001-01-16T00:00:00.000Z", + "squadNumber": 24, + "position": "Central Midfield", + "abbrPosition": "CM", + "team": "SL Benfica", + "league": "Liga Portugal", + "starting11": true + }, + { + "firstName": "Alexis", + "middleName": null, + "lastName": "Mac Allister", + "dateOfBirth": "1998-12-23T00:00:00.000Z", + "squadNumber": 20, + "position": "Central Midfield", + "abbrPosition": "CM", + "team": "Brighton & Hove Albion", + "league": "Premier League", + "starting11": true + }, + { + "firstName": "Lionel", + "middleName": "Andrés", + "lastName": "Messi", + "dateOfBirth": "1987-06-23T00:00:00.000Z", + "squadNumber": 10, + "position": "Right Winger", + "abbrPosition": "RW", + "team": "Paris Saint-Germain", + "league": "Ligue 1", + "starting11": true + }, + { + "firstName": "Julián", + "middleName": null, + "lastName": "Álvarez", + "dateOfBirth": "2000-01-30T00:00:00.000Z", + "squadNumber": 9, + "position": "Centre-Forward", + "abbrPosition": "CF", + "team": "Manchester City", + "league": "Premier League", + "starting11": true + }] + """; + + var starting11 = JsonSerializer.Deserialize>(json, options); + + if (starting11 != null && starting11.Count > 0) + { + players.AddRange(starting11); + } + + return players; + } + + private static readonly JsonSerializerOptions options = + new() { PropertyNameCaseInsensitive = true, }; +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Utilities/SwaggerUtilities.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Utilities/SwaggerUtilities.cs new file mode 100644 index 0000000..2cdba59 --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Utilities/SwaggerUtilities.cs @@ -0,0 +1,64 @@ +using System.Reflection; +using Microsoft.OpenApi; + +namespace Dotnet.Samples.AspNetCore.WebApi.Utilities; + +/// +/// Utility methods for Swagger/OpenAPI configuration. +/// Contains reusable helper methods that create OpenAPI objects. +/// +public static class SwaggerUtilities +{ + /// + /// Resolves the path to the XML comments file generated from code + /// documentation. + /// This is used to enrich the Swagger UI with method summaries and remarks. + /// + /// Full file path to the XML documentation file. + public static string ConfigureXmlCommentsFilePath() + { + var path = Path.Combine( + AppContext.BaseDirectory, + $"{Assembly.GetExecutingAssembly().GetName().Name}.xml" + ); + + if (!File.Exists(path)) + { + throw new FileNotFoundException("XML comments file not found.", path); + } + return path; + } + + /// + /// Configures the OpenAPI security definition for JWT Bearer authentication. + /// This will show the padlock icon in Swagger UI and allow users to + /// authenticate. + /// + /// An describing the Bearer token format. + public static OpenApiSecurityScheme ConfigureSecurityDefinition() + { + return new OpenApiSecurityScheme + { + Name = "Authorization", + Type = SecuritySchemeType.Http, + Scheme = "bearer", + BearerFormat = "JWT", + In = ParameterLocation.Header, + Description = "Enter your JWT token below. Example: Bearer {token}" + }; + } + + /// + /// Adds a global security requirement to the Swagger spec that applies + /// the Bearer definition. + /// Routes decorated with [Authorize] will be marked as protected in the UI. + /// + /// An referencing the Bearer definition. + public static OpenApiSecurityRequirement ConfigureSecurityRequirement() + { + return new OpenApiSecurityRequirement + { + { new OpenApiSecuritySchemeReference("Bearer", null), new List() } + }; + } +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Validators/PlayerRequestModelValidator.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Validators/PlayerRequestModelValidator.cs new file mode 100644 index 0000000..0d36726 --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/Validators/PlayerRequestModelValidator.cs @@ -0,0 +1,135 @@ +using Dotnet.Samples.AspNetCore.WebApi.Enums; +using Dotnet.Samples.AspNetCore.WebApi.Models; +using Dotnet.Samples.AspNetCore.WebApi.Repositories; +using FluentValidation; + +namespace Dotnet.Samples.AspNetCore.WebApi.Validators; + +/// +/// Validator for PlayerRequestModel. +/// This class uses FluentValidation to define validation rules for the +/// PlayerRequestModel. +/// +/// +/// Rules are organized into CRUD-named rule sets to make their intent explicit. +/// This prevents BeUniqueSquadNumber from running on PUT requests, where +/// the player's squad number already exists in the database by definition. +/// +/// +/// +/// "Create" — used by POST /players; includes all rules plus +/// the uniqueness check for SquadNumber. +/// +/// +/// "Update" — used by PUT /players/squadNumber/{n}; same +/// rules, but BeUniqueSquadNumber is intentionally omitted. +/// +/// +/// +/// Controllers pass opts.IncludeRuleSets("Create") or +/// opts.IncludeRuleSets("Update") so that only the appropriate rule +/// set runs for each operation. +/// +public class PlayerRequestModelValidator : AbstractValidator +{ + private readonly IPlayerRepository _playerRepository; + + public PlayerRequestModelValidator( + IPlayerRepository playerRepository, + TimeProvider? timeProvider = null + ) + { + _playerRepository = playerRepository; + var clock = timeProvider ?? TimeProvider.System; + + // "Create" rule set — POST /players + // Includes BeUniqueSquadNumber to prevent duplicate squad numbers on insert. + RuleSet( + "Create", + () => + { + RuleFor(player => player.FirstName) + .NotEmpty() + .WithMessage("FirstName is required."); + + RuleFor(player => player.LastName).NotEmpty().WithMessage("LastName is required."); + + RuleFor(player => player.SquadNumber) + .NotEmpty() + .WithMessage("SquadNumber is required.") + .GreaterThan(0) + .WithMessage("SquadNumber must be greater than 0.") + .MustAsync(BeUniqueSquadNumber) + .WithMessage("SquadNumber must be unique."); + + RuleFor(player => player.AbbrPosition) + .NotEmpty() + .WithMessage("AbbrPosition is required.") + .Must(Position.IsValidAbbr) + .WithMessage("AbbrPosition is invalid."); + + When( + player => player.DateOfBirth.HasValue, + () => + { + RuleFor(player => player.DateOfBirth) + .Must(date => date!.Value.Date < clock.GetUtcNow().Date) + .WithMessage("DateOfBirth must be a date in the past.") + .Must(date => + date!.Value.Date + >= new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc) + ) + .WithMessage("DateOfBirth must be on or after January 1, 1900."); + } + ); + } + ); + + // "Update" rule set — PUT /players/squadNumber/{n} + // BeUniqueSquadNumber is intentionally omitted: on PUT the player being + // updated already exists in the database, so the check would always fail. + RuleSet( + "Update", + () => + { + RuleFor(player => player.FirstName) + .NotEmpty() + .WithMessage("FirstName is required."); + + RuleFor(player => player.LastName).NotEmpty().WithMessage("LastName is required."); + + RuleFor(player => player.SquadNumber) + .NotEmpty() + .WithMessage("SquadNumber is required.") + .GreaterThan(0) + .WithMessage("SquadNumber must be greater than 0."); + + RuleFor(player => player.AbbrPosition) + .NotEmpty() + .WithMessage("AbbrPosition is required.") + .Must(Position.IsValidAbbr) + .WithMessage("AbbrPosition is invalid."); + + When( + player => player.DateOfBirth.HasValue, + () => + { + RuleFor(player => player.DateOfBirth) + .Must(date => date!.Value.Date < clock.GetUtcNow().Date) + .WithMessage("DateOfBirth must be a date in the past.") + .Must(date => + date!.Value.Date + >= new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc) + ) + .WithMessage("DateOfBirth must be on or after January 1, 1900."); + } + ); + } + ); + } + + private async Task BeUniqueSquadNumber( + int squadNumber, + CancellationToken cancellationToken + ) => (await _playerRepository.FindBySquadNumberAsync(squadNumber)) is null; +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/appsettings.Development.json b/src/Dotnet.Samples.AspNetCore.WebApi/appsettings.Development.json new file mode 100644 index 0000000..c443cfa --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/appsettings.Development.json @@ -0,0 +1,47 @@ +{ + "Serilog": { + "Using": ["Serilog.Sinks.Console", "Serilog.Sinks.File"], + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft.EntityFrameworkCore.Database.Command": "Information" + } + }, + "WriteTo": [ + { + "Name": "Console", + "Args": { + "theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console", + "outputTemplate": "{Timestamp:HH:mm:ss.fff zzz} [{Level}] {Message:lj}{NewLine}{Exception}" + } + }, + { + "Name": "File", + "Args": { + "path": "logs/log-.log", + "rollingInterval": "Day", + "outputTemplate": "{Timestamp:HH:mm:ss.fff zzz} [{Level}] {Message:lj}{NewLine}{Exception}" + } + } + ], + "Enrich": ["FromLogContext"] + }, + "OpenApiInfo": { + "Version": "1.0.0", + "Title": "Dotnet.Samples.AspNetCore.WebApi", + "Description": "🧪 Proof of Concept for a Web API (Async) made with .NET 8 (LTS) and ASP.NET Core 8.0", + "Contact": { + "Name": "GitHub", + "Url": "https://github.com/nanotaboada/Dotnet.Samples.AspNetCore.WebApi" + }, + "License": { + "Name": "MIT License", + "Url": "https://opensource.org/license/mit" + } + }, + "RateLimiter": { + "PermitLimit": 60, + "WindowSeconds": 60, + "QueueLimit": 0 + } +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/appsettings.json b/src/Dotnet.Samples.AspNetCore.WebApi/appsettings.json new file mode 100644 index 0000000..10cbb7e --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/appsettings.json @@ -0,0 +1,47 @@ +{ + "Serilog": { + "Using": ["Serilog.Sinks.Console", "Serilog.Sinks.File"], + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft.EntityFrameworkCore.Database.Command": "Warning" + } + }, + "WriteTo": [ + { + "Name": "Console", + "Args": { + "theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console", + "outputTemplate": "{Timestamp:HH:mm:ss.fff zzz} [{Level}] {Message:lj}{NewLine}{Exception}" + } + }, + { + "Name": "File", + "Args": { + "path": "logs/log-.log", + "rollingInterval": "Day", + "outputTemplate": "{Timestamp:HH:mm:ss.fff zzz} [{Level}] {Message:lj}{NewLine}{Exception}" + } + } + ], + "Enrich": ["FromLogContext"] + }, + "OpenApiInfo": { + "Version": "1.0.0", + "Title": "Dotnet.Samples.AspNetCore.WebApi", + "Description": "🧪 Proof of Concept for a Web API (Async) made with .NET 8 (LTS) and ASP.NET Core 8.0", + "Contact": { + "Name": "GitHub", + "Url": "https://github.com/nanotaboada/Dotnet.Samples.AspNetCore.WebApi" + }, + "License": { + "Name": "MIT License", + "Url": "https://opensource.org/license/mit" + } + }, + "RateLimiter": { + "PermitLimit": 60, + "WindowSeconds": 60, + "QueueLimit": 0 + } +} diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/packages.lock.json b/src/Dotnet.Samples.AspNetCore.WebApi/packages.lock.json new file mode 100644 index 0000000..1ccd7cc --- /dev/null +++ b/src/Dotnet.Samples.AspNetCore.WebApi/packages.lock.json @@ -0,0 +1,1289 @@ +{ + "version": 1, + "dependencies": { + "net10.0": { + "AutoMapper": { + "type": "Direct", + "requested": "[16.1.1, 17.0.0)", + "resolved": "16.1.1", + "contentHash": "VNEky8JA15ci+oIDRGHITOGOpV4dILsf8pnn24QhDl2urtqgJ2IXiS/V2EtGU17P/+f6OeFQPJETaZXV9QOIZg==", + "dependencies": { + "Microsoft.IdentityModel.JsonWebTokens": "8.14.0" + } + }, + "FluentValidation": { + "type": "Direct", + "requested": "[12.1.1, )", + "resolved": "12.1.1", + "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" + }, + "FluentValidation.DependencyInjectionExtensions": { + "type": "Direct", + "requested": "[12.1.1, )", + "resolved": "12.1.1", + "contentHash": "D0VXh4dtjjX2aQizuaa0g6R8X3U1JaVqJPfGCvLwZX9t/O2h7tkpbitbadQMfwcgSPdDbI2vDxuwRMv/Uf9dHA==", + "dependencies": { + "FluentValidation": "12.1.1" + } + }, + "Microsoft.AspNetCore.OpenApi": { + "type": "Direct", + "requested": "[10.0.9, )", + "resolved": "10.0.9", + "contentHash": "1ihb8FO9cGgEK1/m3CTtT/SfnynwmiZib0W2pcDVj3KSWk/Sca4VOXEtaptKQc582zpFrzTFiwkGRCglt6H+WQ==", + "dependencies": { + "Microsoft.OpenApi": "2.0.0" + } + }, + "Microsoft.EntityFrameworkCore.Design": { + "type": "Direct", + "requested": "[10.0.9, )", + "resolved": "10.0.9", + "contentHash": "NxgqhpB89S6BViMku9B476KopBumtk0IQ4/iVn2GeeUBW9SyzLvWwOR0zPgcooAtRlR0UVzXkaUs71gYHSAbHg==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.Build.Framework": "18.0.2", + "Microsoft.CodeAnalysis.CSharp": "5.0.0", + "Microsoft.CodeAnalysis.CSharp.Workspaces": "5.0.0", + "Microsoft.CodeAnalysis.Workspaces.MSBuild": "5.0.0", + "Microsoft.EntityFrameworkCore.Relational": "10.0.9", + "Microsoft.Extensions.DependencyModel": "10.0.9", + "Mono.TextTemplating": "3.0.0", + "Newtonsoft.Json": "13.0.3" + } + }, + "Microsoft.EntityFrameworkCore.Sqlite": { + "type": "Direct", + "requested": "[10.0.9, )", + "resolved": "10.0.9", + "contentHash": "HH9/nnRF/YmRrc3hUlgXjMBYKH5kFmd5UWC81l9U0ySQhwHTcgvDPSewB8DyQHzFJzNGgG7VFK6ynC6+XQz9WA==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.9", + "Microsoft.Extensions.DependencyModel": "10.0.9", + "SQLitePCLRaw.bundle_e_sqlite3": "2.1.11", + "SQLitePCLRaw.core": "2.1.11" + } + }, + "Microsoft.VisualStudio.Web.CodeGeneration.Design": { + "type": "Direct", + "requested": "[10.0.2, )", + "resolved": "10.0.2", + "contentHash": "pAO/U3tr7wlUJ5QkAuO+AI0qNm7BJGoT4FaLqIAe8fIZW0Ezn2/d0bLIMKkLzWDu+wFHf/s+HlAhqQMyhtP9Cg==", + "dependencies": { + "Humanizer": "2.14.1", + "Microsoft.AspNetCore.Razor.Language": "6.0.24", + "Microsoft.Build": "17.11.48", + "Microsoft.CodeAnalysis.CSharp": "5.0.0", + "Microsoft.CodeAnalysis.CSharp.Features": "5.0.0", + "Microsoft.CodeAnalysis.CSharp.Workspaces": "5.0.0", + "Microsoft.CodeAnalysis.Common": "5.0.0", + "Microsoft.CodeAnalysis.Features": "5.0.0", + "Microsoft.CodeAnalysis.Razor": "6.0.24", + "Microsoft.CodeAnalysis.Workspaces.Common": "5.0.0", + "Microsoft.CodeAnalysis.Workspaces.MSBuild": "5.0.0", + "Microsoft.DotNet.Scaffolding.Shared": "10.0.2", + "Microsoft.EntityFrameworkCore": "10.0.2", + "Microsoft.EntityFrameworkCore.Design": "10.0.2", + "Microsoft.Extensions.DependencyModel": "10.0.2", + "Microsoft.VisualStudio.Web.CodeGenerators.Mvc": "10.0.2", + "Mono.TextTemplating": "3.0.0", + "Newtonsoft.Json": "13.0.3", + "NuGet.Packaging": "6.12.1", + "NuGet.ProjectModel": "6.12.1", + "System.Security.Cryptography.ProtectedData": "9.0.6" + } + }, + "Npgsql.EntityFrameworkCore.PostgreSQL": { + "type": "Direct", + "requested": "[10.0.2, )", + "resolved": "10.0.2", + "contentHash": "PsNYgPOSW41Xx19gin7y4EdZAPteWr9Cb01XkdObxOsPzi+mgBupBEN7J7+erXFsROPOILM7MlIoO9QzL8+LGQ==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[10.0.4, 11.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.4, 11.0.0)", + "Npgsql": "10.0.3" + } + }, + "Serilog.AspNetCore": { + "type": "Direct", + "requested": "[10.0.0, )", + "resolved": "10.0.0", + "contentHash": "a/cNa1mY4On1oJlfGG1wAvxjp5g7OEzk/Jf/nm7NF9cWoE7KlZw1GldrifUBWm9oKibHkR7Lg/l5jy3y7ACR8w==", + "dependencies": { + "Serilog": "4.3.0", + "Serilog.Extensions.Hosting": "10.0.0", + "Serilog.Formatting.Compact": "3.0.0", + "Serilog.Settings.Configuration": "10.0.0", + "Serilog.Sinks.Console": "6.1.1", + "Serilog.Sinks.Debug": "3.0.0", + "Serilog.Sinks.File": "7.0.0" + } + }, + "Serilog.Settings.Configuration": { + "type": "Direct", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "ayFE7h66mqMqzwfPrzDCMbWU27FdNC2bkCG+jnkeHFZTBRh+yWdr4aa/2WuX7c8RmqxGPMW2UqoJ3fw9hK3QhA==", + "dependencies": { + "Microsoft.Extensions.DependencyModel": "10.0.0", + "Serilog": "4.3.0" + } + }, + "Serilog.Sinks.Console": { + "type": "Direct", + "requested": "[6.1.1, )", + "resolved": "6.1.1", + "contentHash": "8jbqgjUyZlfCuSTaJk6lOca465OndqOz3KZP6Cryt/IqZYybyBu7GP0fE/AXBzrrQB3EBmQntBFAvMVz1COvAA==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Sinks.File": { + "type": "Direct", + "requested": "[7.0.0, )", + "resolved": "7.0.0", + "contentHash": "fKL7mXv7qaiNBUC71ssvn/dU0k9t0o45+qm2XgKAlSt19xF+ijjxyA3R6HmCgfKEKwfcfkwWjayuQtRueZFkYw==", + "dependencies": { + "Serilog": "4.2.0" + } + }, + "Swashbuckle.AspNetCore": { + "type": "Direct", + "requested": "[10.2.3, )", + "resolved": "10.2.3", + "contentHash": "8KNh1RWvofdU6DVLyBs4Z/OpUMnmf8oNvJQc0QxpwySRbi42bwLfdVMMrXZWANg5U5KQGQq1xW6r/hlcqw99tQ==", + "dependencies": { + "Microsoft.Extensions.ApiDescription.Server": "10.0.0", + "Swashbuckle.AspNetCore.Swagger": "10.2.3", + "Swashbuckle.AspNetCore.SwaggerGen": "10.2.3", + "Swashbuckle.AspNetCore.SwaggerUI": "10.2.3" + } + }, + "Humanizer": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "/FUTD3cEceAAmJSCPN9+J+VhGwmL/C12jvwlyM1DFXShEMsBzvLzLqSrJ2rb+k/W2znKw7JyflZgZpyE+tI7lA==", + "dependencies": { + "Humanizer.Core.af": "2.14.1", + "Humanizer.Core.ar": "2.14.1", + "Humanizer.Core.az": "2.14.1", + "Humanizer.Core.bg": "2.14.1", + "Humanizer.Core.bn-BD": "2.14.1", + "Humanizer.Core.cs": "2.14.1", + "Humanizer.Core.da": "2.14.1", + "Humanizer.Core.de": "2.14.1", + "Humanizer.Core.el": "2.14.1", + "Humanizer.Core.es": "2.14.1", + "Humanizer.Core.fa": "2.14.1", + "Humanizer.Core.fi-FI": "2.14.1", + "Humanizer.Core.fr": "2.14.1", + "Humanizer.Core.fr-BE": "2.14.1", + "Humanizer.Core.he": "2.14.1", + "Humanizer.Core.hr": "2.14.1", + "Humanizer.Core.hu": "2.14.1", + "Humanizer.Core.hy": "2.14.1", + "Humanizer.Core.id": "2.14.1", + "Humanizer.Core.is": "2.14.1", + "Humanizer.Core.it": "2.14.1", + "Humanizer.Core.ja": "2.14.1", + "Humanizer.Core.ko-KR": "2.14.1", + "Humanizer.Core.ku": "2.14.1", + "Humanizer.Core.lv": "2.14.1", + "Humanizer.Core.ms-MY": "2.14.1", + "Humanizer.Core.mt": "2.14.1", + "Humanizer.Core.nb": "2.14.1", + "Humanizer.Core.nb-NO": "2.14.1", + "Humanizer.Core.nl": "2.14.1", + "Humanizer.Core.pl": "2.14.1", + "Humanizer.Core.pt": "2.14.1", + "Humanizer.Core.ro": "2.14.1", + "Humanizer.Core.ru": "2.14.1", + "Humanizer.Core.sk": "2.14.1", + "Humanizer.Core.sl": "2.14.1", + "Humanizer.Core.sr": "2.14.1", + "Humanizer.Core.sr-Latn": "2.14.1", + "Humanizer.Core.sv": "2.14.1", + "Humanizer.Core.th-TH": "2.14.1", + "Humanizer.Core.tr": "2.14.1", + "Humanizer.Core.uk": "2.14.1", + "Humanizer.Core.uz-Cyrl-UZ": "2.14.1", + "Humanizer.Core.uz-Latn-UZ": "2.14.1", + "Humanizer.Core.vi": "2.14.1", + "Humanizer.Core.zh-CN": "2.14.1", + "Humanizer.Core.zh-Hans": "2.14.1", + "Humanizer.Core.zh-Hant": "2.14.1" + } + }, + "Humanizer.Core": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" + }, + "Humanizer.Core.af": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "BoQHyu5le+xxKOw+/AUM7CLXneM/Bh3++0qh1u0+D95n6f9eGt9kNc8LcAHLIOwId7Sd5hiAaaav0Nimj3peNw==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.ar": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "3d1V10LDtmqg5bZjWkA/EkmGFeSfNBcyCH+TiHcHP+HGQQmRq3eBaLcLnOJbVQVn3Z6Ak8GOte4RX4kVCxQlFA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.az": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "8Z/tp9PdHr/K2Stve2Qs/7uqWPWLUK9D8sOZDNzyv42e20bSoJkHFn7SFoxhmaoVLJwku2jp6P7HuwrfkrP18Q==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.bg": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "S+hIEHicrOcbV2TBtyoPp1AVIGsBzlarOGThhQYCnP6QzEYo/5imtok6LMmhZeTnBFoKhM8yJqRfvJ5yqVQKSQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.bn-BD": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "U3bfj90tnUDRKlL1ZFlzhCHoVgpTcqUlTQxjvGCaFKb+734TTu3nkHUWVZltA1E/swTvimo/aXLtkxnLFrc0EQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.cs": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "jWrQkiCTy3L2u1T86cFkgijX6k7hoB0pdcFMWYaSZnm6rvG/XJE40tfhYyKhYYgIc1x9P2GO5AC7xXvFnFdqMQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.da": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "5o0rJyE/2wWUUphC79rgYDnif/21MKTTx9LIzRVz9cjCIVFrJ2bDyR2gapvI9D6fjoyvD1NAfkN18SHBsO8S9g==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.de": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "9JD/p+rqjb8f5RdZ3aEJqbjMYkbk4VFii2QDnnOdNo6ywEfg/A5YeOQ55CaBJmy7KvV4tOK4+qHJnX/tg3Z54A==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.el": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "Xmv6sTL5mqjOWGGpqY7bvbfK5RngaUHSa8fYDGSLyxY9mGdNbDcasnRnMOvi0SxJS9gAqBCn21Xi90n2SHZbFA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.es": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "e//OIAeMB7pjBV1HqqI4pM2Bcw3Jwgpyz9G5Fi4c+RJvhqFwztoWxW57PzTnNJE2lbhGGLQZihFZjsbTUsbczA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.fa": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "nzDOj1x0NgjXMjsQxrET21t1FbdoRYujzbmZoR8u8ou5CBWY1UNca0j6n/PEJR/iUbt4IxstpszRy41wL/BrpA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.fi-FI": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "Vnxxx4LUhp3AzowYi6lZLAA9Lh8UqkdwRh4IE2qDXiVpbo08rSbokATaEzFS+o+/jCNZBmoyyyph3vgmcSzhhQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.fr": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "2p4g0BYNzFS3u9SOIDByp2VClYKO0K1ecDV4BkB9EYdEPWfFODYnF+8CH8LpUrpxL2TuWo2fiFx/4Jcmrnkbpg==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.fr-BE": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "o6R3SerxCRn5Ij8nCihDNMGXlaJ/1AqefteAssgmU2qXYlSAGdhxmnrQAXZUDlE4YWt/XQ6VkNLtH7oMqsSPFQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.he": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "FPsAhy7Iw6hb+ZitLgYC26xNcgGAHXb0V823yFAzcyoL5ozM+DCJtYfDPYiOpsJhEZmKFTM9No0jUn1M89WGvg==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.hr": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "chnaD89yOlST142AMkAKLuzRcV5df3yyhDyRU5rypDiqrq2HN8y1UR3h1IicEAEtXLoOEQyjSAkAQ6QuXkn7aw==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.hu": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "hAfnaoF9LTGU/CmFdbnvugN4tIs8ppevVMe3e5bD24+tuKsggMc5hYta9aiydI8JH9JnuVmxvNI4DJee1tK05A==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.hy": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "sVIKxOiSBUb4gStRHo9XwwAg9w7TNvAXbjy176gyTtaTiZkcjr9aCPziUlYAF07oNz6SdwdC2mwJBGgvZ0Sl2g==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.id": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "4Zl3GTvk3a49Ia/WDNQ97eCupjjQRs2iCIZEQdmkiqyaLWttfb+cYXDMGthP42nufUL0SRsvBctN67oSpnXtsg==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.is": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "R67A9j/nNgcWzU7gZy1AJ07ABSLvogRbqOWvfRDn4q6hNdbg/mjGjZBp4qCTPnB2mHQQTCKo3oeCUayBCNIBCw==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.it": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "jYxGeN4XIKHVND02FZ+Woir3CUTyBhLsqxu9iqR/9BISArkMf1Px6i5pRZnvq4fc5Zn1qw71GKKoCaHDJBsLFw==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.ja": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "TM3ablFNoYx4cYJybmRgpDioHpiKSD7q0QtMrmpsqwtiiEsdW5zz/q4PolwAczFnvrKpN6nBXdjnPPKVet93ng==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.ko-KR": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "CtvwvK941k/U0r8PGdEuBEMdW6jv/rBiA9tUhakC7Zd2rA/HCnDcbr1DiNZ+/tRshnhzxy/qwmpY8h4qcAYCtQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.ku": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "vHmzXcVMe+LNrF9txpdHzpG7XJX65SiN9GQd/Zkt6gsGIIEeECHrkwCN5Jnlkddw2M/b0HS4SNxdR1GrSn7uCA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.lv": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "E1/KUVnYBS1bdOTMNDD7LV/jdoZv/fbWTLPtvwdMtSdqLyRTllv6PGM9xVQoFDYlpvVGtEl/09glCojPHw8ffA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.ms-MY": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "vX8oq9HnYmAF7bek4aGgGFJficHDRTLgp/EOiPv9mBZq0i4SA96qVMYSjJ2YTaxs7Eljqit7pfpE2nmBhY5Fnw==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.mt": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "pEgTBzUI9hzemF7xrIZigl44LidTUhNu4x/P6M9sAwZjkUF0mMkbpxKkaasOql7lLafKrnszs0xFfaxQyzeuZQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.nb": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "mbs3m6JJq53ssLqVPxNfqSdTxAcZN3njlG8yhJVx83XVedpTe1ECK9aCa8FKVOXv93Gl+yRHF82Hw9T9LWv2hw==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.nb-NO": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "AsJxrrVYmIMbKDGe8W6Z6//wKv9dhWH7RsTcEHSr4tQt/80pcNvLi0hgD3fqfTtg0tWKtgch2cLf4prorEV+5A==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.nl": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "24b0OUdzJxfoqiHPCtYnR5Y4l/s4Oh7KW7uDp+qX25NMAHLCGog2eRfA7p2kRJp8LvnynwwQxm2p534V9m55wQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.pl": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "17mJNYaBssENVZyQHduiq+bvdXS0nhZJGEXtPKoMhKv3GD//WO0mEfd9wjEBsWCSmWI7bjRqhCidxzN+YtJmsg==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.pt": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "8HB8qavcVp2la1GJX6t+G9nDYtylPKzyhxr9LAooIei9MnQvNsjEiIE4QvHoeDZ4weuQ9CsPg1c211XUMVEZ4A==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.ro": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "psXNOcA6R8fSHoQYhpBTtTTYiOk8OBoN3PKCEDgsJKIyeY5xuK81IBdGi77qGZMu/OwBRQjQCBMtPJb0f4O1+A==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.ru": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "zm245xUWrajSN2t9H7BTf84/2APbUkKlUJpcdgsvTdAysr1ag9fi1APu6JEok39RRBXDfNRVZHawQ/U8X0pSvQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.sk": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "Ncw24Vf3ioRnbU4MsMFHafkyYi8JOnTqvK741GftlQvAbULBoTz2+e7JByOaasqeSi0KfTXeegJO+5Wk1c0Mbw==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.sl": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "l8sUy4ciAIbVThWNL0atzTS2HWtv8qJrsGWNlqrEKmPwA4SdKolSqnTes9V89fyZTc2Q43jK8fgzVE2C7t009A==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.sr": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "rnNvhpkOrWEymy7R/MiFv7uef8YO5HuXDyvojZ7JpijHWA5dXuVXooCOiA/3E93fYa3pxDuG2OQe4M/olXbQ7w==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.sr-Latn": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "nuy/ykpk974F8ItoQMS00kJPr2dFNjOSjgzCwfysbu7+gjqHmbLcYs7G4kshLwdA4AsVncxp99LYeJgoh1JF5g==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.sv": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "E53+tpAG0RCp+cSSI7TfBPC+NnsEqUuoSV0sU+rWRXWr9MbRWx1+Zj02XMojqjGzHjjOrBFBBio6m74seFl0AA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.th-TH": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "eSevlJtvs1r4vQarNPfZ2kKDp/xMhuD00tVVzRXkSh1IAZbBJI/x2ydxUOwfK9bEwEp+YjvL1Djx2+kw7ziu7g==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.tr": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "rQ8N+o7yFcFqdbtu1mmbrXFi8TQ+uy+fVH9OPI0CI3Cu1om5hUU/GOMC3hXsTCI6d79y4XX+0HbnD7FT5khegA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.uk": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "2uEfujwXKNm6bdpukaLtEJD+04uUtQD65nSGCetA1fYNizItEaIBUboNfr3GzJxSMQotNwGVM3+nSn8jTd0VSg==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.uz-Cyrl-UZ": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "TD3ME2sprAvFqk9tkWrvSKx5XxEMlAn1sjk+cYClSWZlIMhQQ2Bp/w0VjX1Kc5oeKjxRAnR7vFcLUFLiZIDk9Q==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.uz-Latn-UZ": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "/kHAoF4g0GahnugZiEMpaHlxb+W6jCEbWIdsq9/I1k48ULOsl/J0pxZj93lXC3omGzVF1BTVIeAtv5fW06Phsg==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.vi": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "rsQNh9rmHMBtnsUUlJbShMsIMGflZtPmrMM6JNDw20nhsvqfrdcoDD8cMnLAbuSovtc3dP+swRmLQzKmXDTVPA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.zh-CN": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "uH2dWhrgugkCjDmduLdAFO9w1Mo0q07EuvM0QiIZCVm6FMCu/lGv2fpMu4GX+4HLZ6h5T2Pg9FIdDLCPN2a67w==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.zh-Hans": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "WH6IhJ8V1UBG7rZXQk3dZUoP2gsi8a0WkL8xL0sN6WGiv695s8nVcmab9tWz20ySQbuzp0UkSxUQFi5jJHIpOQ==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Humanizer.Core.zh-Hant": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "VIXB7HCUC34OoaGnO3HJVtSv2/wljPhjV7eKH4+TFPgQdJj2lvHNKY41Dtg0Bphu7X5UaXFR4zrYYyo+GNOjbA==", + "dependencies": { + "Humanizer.Core": "[2.14.1]" + } + }, + "Microsoft.AspNetCore.Razor.Language": { + "type": "Transitive", + "resolved": "6.0.24", + "contentHash": "kBL6ljTREp/3fk8EKN27mrPy3WTqWUjiqCkKFlCKHUKRO3/9rAasKizX3vPWy4ZTcNsIPmVWUHwjDFmiW4MyNA==" + }, + "Microsoft.Build": { + "type": "Transitive", + "resolved": "17.11.48", + "contentHash": "g8Kn575mNAKcuFotV3C7xvF+IbxuHennl67LH2shL2au1U6UqwReTDygCHyU04+koc2Yn7fHIbVQaC08HqEWow==", + "dependencies": { + "Microsoft.Build.Framework": "17.11.48", + "Microsoft.NET.StringTools": "17.11.48", + "System.Configuration.ConfigurationManager": "8.0.0", + "System.Reflection.MetadataLoadContext": "8.0.0" + } + }, + "Microsoft.Build.Framework": { + "type": "Transitive", + "resolved": "18.0.2", + "contentHash": "sOSb+0J4G/jCBW/YqmRuL0eOMXgfw1KQLdC9TkbvfA5xs7uNm+PBQXJCOzSJGXtZcZrtXozcwxPmUiRUbmd7FA==" + }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "3.11.0", + "contentHash": "v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "ZXRAdvH6GiDeHRyd3q/km8Z44RoM6FBWHd+gen/la81mVnAdHTEsEkO5J0TCNXBymAcx5UYKt5TvgKBhaLJEow==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0" + } + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "5DSyJ9bk+ATuDy7fp2Zt0mJStDVKbBoiz1DyfAwSa+k4H4IwykAUcV3URelw5b8/iVbfSaOwkwmPUZH6opZKCw==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Common": "[5.0.0]" + } + }, + "Microsoft.CodeAnalysis.CSharp.Features": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "Hbf3tvBghC+7S9+LuJXajgPVhjwdZO9+U4FPSTFuKbeLAunwpcbxZdJ+GXf7XJQ9bBJvBlzIDupHjMAgE5i1uQ==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.CSharp": "[5.0.0]", + "Microsoft.CodeAnalysis.CSharp.Workspaces": "[5.0.0]", + "Microsoft.CodeAnalysis.Common": "[5.0.0]", + "Microsoft.CodeAnalysis.Elfie": "1.0.0", + "Microsoft.CodeAnalysis.Features": "[5.0.0]", + "Microsoft.CodeAnalysis.Workspaces.Common": "[5.0.0]", + "Microsoft.DiaSymReader": "2.0.0", + "System.Composition": "9.0.0", + "System.Configuration.ConfigurationManager": "9.0.0", + "System.Security.Cryptography.ProtectedData": "9.0.0" + } + }, + "Microsoft.CodeAnalysis.CSharp.Workspaces": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "Al/Q8B+yO8odSqGVpSvrShMFDvlQdIBU//F3E6Rb0YdiLSALE9wh/pvozPNnfmh5HDnvU+mkmSjpz4hQO++jaA==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.CSharp": "[5.0.0]", + "Microsoft.CodeAnalysis.Common": "[5.0.0]", + "Microsoft.CodeAnalysis.Workspaces.Common": "[5.0.0]", + "System.Composition": "9.0.0" + } + }, + "Microsoft.CodeAnalysis.Elfie": { + "type": "Transitive", + "resolved": "1.0.0", + "contentHash": "r12elUp4MRjdnRfxEP+xqVSUUfG3yIJTBEJGwbfvF5oU4m0jb9HC0gFG28V/dAkYGMkRmHVi3qvrnBLQSw9X3Q==", + "dependencies": { + "System.Configuration.ConfigurationManager": "4.5.0", + "System.Data.DataSetExtensions": "4.5.0" + } + }, + "Microsoft.CodeAnalysis.Features": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "2SFKBDQFHiqwUS4JncpuXyNDhlo8x4EHLCE6IIb22ZfPxFRd1baLP9PA9WY4xJVdmxgHOok7XngDdIY2MZ69JQ==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Common": "[5.0.0]", + "Microsoft.CodeAnalysis.Elfie": "1.0.0", + "Microsoft.CodeAnalysis.Scripting.Common": "[5.0.0]", + "Microsoft.CodeAnalysis.Workspaces.Common": "[5.0.0]", + "Microsoft.DiaSymReader": "2.0.0", + "System.Composition": "9.0.0", + "System.Configuration.ConfigurationManager": "9.0.0", + "System.Security.Cryptography.ProtectedData": "9.0.0" + } + }, + "Microsoft.CodeAnalysis.Razor": { + "type": "Transitive", + "resolved": "6.0.24", + "contentHash": "xIAjR6l/1PO2ILT6/lOGYfe8OzMqfqxh1lxFuM4Exluwc2sQhJw0kS7pEyJ0DE/UMYu6Jcdc53DmjOxQUDT2Pg==", + "dependencies": { + "Microsoft.AspNetCore.Razor.Language": "6.0.24", + "Microsoft.CodeAnalysis.CSharp": "4.0.0", + "Microsoft.CodeAnalysis.Common": "4.0.0" + } + }, + "Microsoft.CodeAnalysis.Scripting.Common": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "XTulByMNxqXGCgMeODUoG2h4oK4/nLv1BcawRVcjv+UZHMpoaymtdaq3cJqlNrEvYEcbU48g5swJ3RhY1m3fBg==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Common": "[5.0.0]" + } + }, + "Microsoft.CodeAnalysis.Workspaces.Common": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "ZbUmIvT6lqTNKiv06Jl5wf0MTMi1vQ1oH7ou4CLcs2C/no/L7EhP3T8y3XXvn9VbqMcJaJnEsNA1jwYUMgc5jg==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Common": "[5.0.0]", + "System.Composition": "9.0.0" + } + }, + "Microsoft.CodeAnalysis.Workspaces.MSBuild": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "/G+LVoAGMz6Ae8nm+PGLxSw+F5RjYx/J7irbTO5uKAPw1bxHyQJLc/YOnpDxt+EpPtYxvC9wvBsg/kETZp1F9Q==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.Build.Framework": "17.11.31", + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Workspaces.Common": "[5.0.0]", + "Microsoft.VisualStudio.SolutionPersistence": "1.0.52", + "Newtonsoft.Json": "13.0.3", + "System.Composition": "9.0.0" + } + }, + "Microsoft.Data.Sqlite.Core": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "iZrONyMKPjxfVZnUktqO30QjzNwAGH+AxM61s8lKQnVhgbQ3bn0hiXI129ZmVicEbIcwljyy2OVsIYUR51ZHKQ==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.11" + } + }, + "Microsoft.DiaSymReader": { + "type": "Transitive", + "resolved": "2.0.0", + "contentHash": "QcZrCETsBJqy/vQpFtJc+jSXQ0K5sucQ6NUFbTNVHD4vfZZOwjZ/3sBzczkC4DityhD3AVO/+K/+9ioLs1AgRA==" + }, + "Microsoft.DotNet.Scaffolding.Shared": { + "type": "Transitive", + "resolved": "10.0.2", + "contentHash": "jDtDiBUsSCRWKZWC1VIjr0IlybvkrpySpufNLp8s6ZaCdSGoYOIGb13Mv7fnSfksHrLrUFPVtcfS6PznX8iBsg==", + "dependencies": { + "Humanizer": "2.14.1", + "Microsoft.CodeAnalysis.CSharp": "5.0.0", + "Microsoft.CodeAnalysis.CSharp.Features": "5.0.0", + "Microsoft.CodeAnalysis.CSharp.Workspaces": "5.0.0", + "Microsoft.CodeAnalysis.Common": "5.0.0", + "Microsoft.CodeAnalysis.Features": "5.0.0", + "Microsoft.CodeAnalysis.Workspaces.Common": "5.0.0", + "Microsoft.Extensions.DependencyModel": "10.0.2", + "Mono.TextTemplating": "3.0.0", + "Newtonsoft.Json": "13.0.3", + "NuGet.Packaging": "6.12.1", + "NuGet.ProjectModel": "6.12.1", + "System.Security.Cryptography.ProtectedData": "9.0.6" + } + }, + "Microsoft.EntityFrameworkCore": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "tu85SRzOT021V7EQlViCiAE7TqldVn469Y6lt5TEn/+XC4/MeNCHgMRSxqYuWqvF4zAQZUhCmtNEZuM3ss4LeA==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Abstractions": "10.0.9", + "Microsoft.EntityFrameworkCore.Analyzers": "10.0.9" + } + }, + "Microsoft.EntityFrameworkCore.Abstractions": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "GRMaiPkqYna/gCsyDffYDWmefGPC3hDrdMw+2rrGcQwhs6uZOsaMQXMJnoXQ35tx9SkBV2ieRRU9N/jLOO6BZw==" + }, + "Microsoft.EntityFrameworkCore.Analyzers": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "aiEFB+C5EsZGqxvMPazE07hbWsp4iPaufJpanGt5O+lrwv7mJLrqma5haVIgFAPCyhQkmk75XSCEubT1zUjxtA==" + }, + "Microsoft.EntityFrameworkCore.Relational": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "dEoyYKgiaZHHgOFm1WMWm1sFEsEuhPWufX4L9PekKtqd/RaIcPjkCjvbrVvJtApErb5wPSJhYvnTlxhH+p9h2g==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "10.0.9" + } + }, + "Microsoft.EntityFrameworkCore.Sqlite.Core": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "3bPEmACAaPJJSw+m5XwTSi3yZnVtaifa4d8gLsNMzW0Qu28jS5kADSfgJRBlq49RJ1K098VCzEDRJwM8gE6f2w==", + "dependencies": { + "Microsoft.Data.Sqlite.Core": "10.0.9", + "Microsoft.EntityFrameworkCore.Relational": "10.0.9", + "Microsoft.Extensions.DependencyModel": "10.0.9", + "SQLitePCLRaw.core": "2.1.11" + } + }, + "Microsoft.Extensions.ApiDescription.Server": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "NCWCGiwRwje8773yzPQhvucYnnfeR+ZoB1VRIrIMp4uaeUNw7jvEPHij3HIbwCDuNCrNcphA00KSAR9yD9qmbg==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "SCDTQ6HubnRvTUjR7dgMKHZvNoCb03t44ttHL8trlFTGgfDteWn/0nRdOxDhcI+lTWhKgd/flCVJEtAOPhSLNg==" + }, + "Microsoft.IdentityModel.Abstractions": { + "type": "Transitive", + "resolved": "8.14.0", + "contentHash": "iwbCpSjD3ehfTwBhtSNEtKPK0ICun6ov7Ibx6ISNA9bfwIyzI2Siwyi9eJFCJBwxowK9xcA1mj+jBWiigeqgcQ==" + }, + "Microsoft.IdentityModel.JsonWebTokens": { + "type": "Transitive", + "resolved": "8.14.0", + "contentHash": "4jOpiA4THdtpLyMdAb24dtj7+6GmvhOhxf5XHLYWmPKF8ApEnApal1UnJsKO4HxUWRXDA6C4WQVfYyqsRhpNpQ==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.14.0" + } + }, + "Microsoft.IdentityModel.Logging": { + "type": "Transitive", + "resolved": "8.14.0", + "contentHash": "eqqnemdW38CKZEHS6diA50BV94QICozDZEvSrsvN3SJXUFwVB9gy+/oz76gldP7nZliA16IglXjXTCTdmU/Ejg==", + "dependencies": { + "Microsoft.IdentityModel.Abstractions": "8.14.0" + } + }, + "Microsoft.IdentityModel.Tokens": { + "type": "Transitive", + "resolved": "8.14.0", + "contentHash": "lKIZiBiGd36k02TCdMHp1KlNWisyIvQxcYJvIkz7P4gSQ9zi8dgh6S5Grj8NNG7HWYIPfQymGyoZ6JB5d1Lo1g==", + "dependencies": { + "Microsoft.IdentityModel.Logging": "8.14.0" + } + }, + "Microsoft.NET.StringTools": { + "type": "Transitive", + "resolved": "17.11.48", + "contentHash": "0IQo089IGBEC4jgtishauZMVr9ZxOWNiGKeDvyzZlvw7p2r253lJh6IJCLLFWXvZnVrVO5mnsYIPamxFPzM08w==" + }, + "Microsoft.OpenApi": { + "type": "Transitive", + "resolved": "2.7.5", + "contentHash": "0FA67RSnRM4tcBKqiqVu/HPdZ9+QOKbmeRjxRUGTCjPU4C0bmUhd97Dso7Yild5P7nOV6GxJ2xrK0Kv/O9xp0w==" + }, + "Microsoft.VisualStudio.SolutionPersistence": { + "type": "Transitive", + "resolved": "1.0.52", + "contentHash": "oNv2JtYXhpdJrX63nibx1JT3uCESOBQ1LAk7Dtz/sr0+laW0KRM6eKp4CZ3MHDR2siIkKsY8MmUkeP5DKkQQ5w==" + }, + "Microsoft.VisualStudio.Web.CodeGeneration": { + "type": "Transitive", + "resolved": "10.0.2", + "contentHash": "LhwZTguLVNS7STgweJz/Akd1HSDHzoQBiAukcJ0t3k8QIwo5RON6UDle/w8YDnmTdSDDwZ/3BVdvPPpkrq22Ng==", + "dependencies": { + "Humanizer": "2.14.1", + "Microsoft.AspNetCore.Razor.Language": "6.0.24", + "Microsoft.Build": "17.11.48", + "Microsoft.CodeAnalysis.CSharp": "5.0.0", + "Microsoft.CodeAnalysis.CSharp.Features": "5.0.0", + "Microsoft.CodeAnalysis.CSharp.Workspaces": "5.0.0", + "Microsoft.CodeAnalysis.Common": "5.0.0", + "Microsoft.CodeAnalysis.Features": "5.0.0", + "Microsoft.CodeAnalysis.Razor": "6.0.24", + "Microsoft.CodeAnalysis.Workspaces.Common": "5.0.0", + "Microsoft.CodeAnalysis.Workspaces.MSBuild": "5.0.0", + "Microsoft.EntityFrameworkCore": "10.0.2", + "Microsoft.EntityFrameworkCore.Design": "10.0.2", + "Microsoft.Extensions.DependencyModel": "10.0.2", + "Microsoft.VisualStudio.Web.CodeGeneration.EntityFrameworkCore": "10.0.2", + "Mono.TextTemplating": "3.0.0", + "Newtonsoft.Json": "13.0.3", + "NuGet.Packaging": "6.12.1", + "NuGet.ProjectModel": "6.12.1", + "System.Security.Cryptography.ProtectedData": "9.0.6" + } + }, + "Microsoft.VisualStudio.Web.CodeGeneration.Core": { + "type": "Transitive", + "resolved": "10.0.2", + "contentHash": "MnjMLuWnW5Um7CFzfW9Wt1x7RvTCkz8GRM8vEiTxsSQgqvzFcKIGIrQSfu+193g3TKeRH7joulWFr0ionrXcZA==", + "dependencies": { + "Humanizer": "2.14.1", + "Microsoft.AspNetCore.Razor.Language": "6.0.24", + "Microsoft.Build": "17.11.48", + "Microsoft.CodeAnalysis.CSharp": "5.0.0", + "Microsoft.CodeAnalysis.CSharp.Features": "5.0.0", + "Microsoft.CodeAnalysis.CSharp.Workspaces": "5.0.0", + "Microsoft.CodeAnalysis.Common": "5.0.0", + "Microsoft.CodeAnalysis.Features": "5.0.0", + "Microsoft.CodeAnalysis.Razor": "6.0.24", + "Microsoft.CodeAnalysis.Workspaces.Common": "5.0.0", + "Microsoft.Extensions.DependencyModel": "10.0.2", + "Microsoft.VisualStudio.Web.CodeGeneration.Templating": "10.0.2", + "Mono.TextTemplating": "3.0.0", + "Newtonsoft.Json": "13.0.3", + "NuGet.Packaging": "6.12.1", + "NuGet.ProjectModel": "6.12.1", + "System.Security.Cryptography.ProtectedData": "9.0.6" + } + }, + "Microsoft.VisualStudio.Web.CodeGeneration.EntityFrameworkCore": { + "type": "Transitive", + "resolved": "10.0.2", + "contentHash": "NLA44g44cU6god5zxNPycqYFuttIAFpc4pZpPFO9HtV9smm9fYqhMIvfRlsiVKNXelI8AwtDUPJbsCXvwCyOgw==", + "dependencies": { + "Humanizer": "2.14.1", + "Microsoft.AspNetCore.Razor.Language": "6.0.24", + "Microsoft.Build": "17.11.48", + "Microsoft.CodeAnalysis.CSharp": "5.0.0", + "Microsoft.CodeAnalysis.CSharp.Features": "5.0.0", + "Microsoft.CodeAnalysis.CSharp.Workspaces": "5.0.0", + "Microsoft.CodeAnalysis.Common": "5.0.0", + "Microsoft.CodeAnalysis.Features": "5.0.0", + "Microsoft.CodeAnalysis.Razor": "6.0.24", + "Microsoft.CodeAnalysis.Workspaces.Common": "5.0.0", + "Microsoft.CodeAnalysis.Workspaces.MSBuild": "5.0.0", + "Microsoft.DotNet.Scaffolding.Shared": "10.0.2", + "Microsoft.EntityFrameworkCore": "10.0.2", + "Microsoft.EntityFrameworkCore.Design": "10.0.2", + "Microsoft.Extensions.DependencyModel": "10.0.2", + "Microsoft.VisualStudio.Web.CodeGeneration.Core": "10.0.2", + "Mono.TextTemplating": "3.0.0", + "Newtonsoft.Json": "13.0.3", + "NuGet.Packaging": "6.12.1", + "NuGet.ProjectModel": "6.12.1", + "System.Security.Cryptography.ProtectedData": "9.0.6" + } + }, + "Microsoft.VisualStudio.Web.CodeGeneration.Templating": { + "type": "Transitive", + "resolved": "10.0.2", + "contentHash": "9avhpmX70AhFtLgR8e3fBxJ6ynQuhDbDVZ6OkHkDYD4AT0VT39AXL1wXlB5f36HuDQ9T9RS5yc+JN8iyKlMoOg==", + "dependencies": { + "Humanizer": "2.14.1", + "Microsoft.AspNetCore.Razor.Language": "6.0.24", + "Microsoft.Build": "17.11.48", + "Microsoft.CodeAnalysis.CSharp": "5.0.0", + "Microsoft.CodeAnalysis.CSharp.Features": "5.0.0", + "Microsoft.CodeAnalysis.CSharp.Workspaces": "5.0.0", + "Microsoft.CodeAnalysis.Common": "5.0.0", + "Microsoft.CodeAnalysis.Features": "5.0.0", + "Microsoft.CodeAnalysis.Razor": "6.0.24", + "Microsoft.CodeAnalysis.Workspaces.Common": "5.0.0", + "Microsoft.Extensions.DependencyModel": "10.0.2", + "Microsoft.VisualStudio.Web.CodeGeneration.Utils": "10.0.2", + "Mono.TextTemplating": "3.0.0", + "Newtonsoft.Json": "13.0.3", + "NuGet.Packaging": "6.12.1", + "NuGet.ProjectModel": "6.12.1", + "System.Security.Cryptography.ProtectedData": "9.0.6" + } + }, + "Microsoft.VisualStudio.Web.CodeGeneration.Utils": { + "type": "Transitive", + "resolved": "10.0.2", + "contentHash": "FmiduaP4N+4EfST18OWHcx//g6Yv3wcpf8dTWbaZZ3ZVjOWoEEnIeCr1C4Yo9w/6MO0HnZB6Z0YScU5hi2yzjA==", + "dependencies": { + "Humanizer": "2.14.1", + "Microsoft.Build": "17.11.48", + "Microsoft.CodeAnalysis.CSharp": "5.0.0", + "Microsoft.CodeAnalysis.CSharp.Features": "5.0.0", + "Microsoft.CodeAnalysis.CSharp.Workspaces": "5.0.0", + "Microsoft.CodeAnalysis.Common": "5.0.0", + "Microsoft.CodeAnalysis.Features": "5.0.0", + "Microsoft.CodeAnalysis.Workspaces.Common": "5.0.0", + "Microsoft.DotNet.Scaffolding.Shared": "10.0.2", + "Microsoft.Extensions.DependencyModel": "10.0.2", + "Mono.TextTemplating": "3.0.0", + "Newtonsoft.Json": "13.0.3", + "NuGet.Packaging": "6.12.1", + "NuGet.ProjectModel": "6.12.1", + "System.Security.Cryptography.ProtectedData": "9.0.6" + } + }, + "Microsoft.VisualStudio.Web.CodeGenerators.Mvc": { + "type": "Transitive", + "resolved": "10.0.2", + "contentHash": "JQcdTUracjvpN2zzHLhpGuqWqeZJzV08q0us960GRi4EN+3H8/CvE4D6+PzS+mD2eapf6i4DjPQljg9oi+R0+A==", + "dependencies": { + "Humanizer": "2.14.1", + "Microsoft.AspNetCore.Razor.Language": "6.0.24", + "Microsoft.Build": "17.11.48", + "Microsoft.CodeAnalysis.CSharp": "5.0.0", + "Microsoft.CodeAnalysis.CSharp.Features": "5.0.0", + "Microsoft.CodeAnalysis.CSharp.Workspaces": "5.0.0", + "Microsoft.CodeAnalysis.Common": "5.0.0", + "Microsoft.CodeAnalysis.Features": "5.0.0", + "Microsoft.CodeAnalysis.Razor": "6.0.24", + "Microsoft.CodeAnalysis.Workspaces.Common": "5.0.0", + "Microsoft.CodeAnalysis.Workspaces.MSBuild": "5.0.0", + "Microsoft.DotNet.Scaffolding.Shared": "10.0.2", + "Microsoft.EntityFrameworkCore": "10.0.2", + "Microsoft.EntityFrameworkCore.Design": "10.0.2", + "Microsoft.Extensions.DependencyModel": "10.0.2", + "Microsoft.VisualStudio.Web.CodeGeneration": "10.0.2", + "Mono.TextTemplating": "3.0.0", + "Newtonsoft.Json": "13.0.3", + "NuGet.Packaging": "6.12.1", + "NuGet.ProjectModel": "6.12.1", + "System.Security.Cryptography.ProtectedData": "9.0.6" + } + }, + "Mono.TextTemplating": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "YqueG52R/Xej4VVbKuRIodjiAhV0HR/XVbLbNrJhCZnzjnSjgMJ/dCdV0akQQxavX6hp/LC6rqLGLcXeQYU7XA==", + "dependencies": { + "System.CodeDom": "6.0.0" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "Npgsql": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "7nb5YzXuvWWJxB0J8DiyL3we+X4FOctZrt0fIBnucOIaIevFEEwGQVZKtiu9olXdlNAK1eNgqSral6r/jlhI4w==" + }, + "NuGet.Common": { + "type": "Transitive", + "resolved": "6.12.1", + "contentHash": "nk8nTdhQl4x2VaAQUvefI7DDYAuBDlE+OZZRffm50Qx5dUAEq8wkc5JIqrN2lTEohObHPI/SXyG2UFdMQkrdyg==", + "dependencies": { + "NuGet.Frameworks": "6.12.1" + } + }, + "NuGet.Configuration": { + "type": "Transitive", + "resolved": "6.12.1", + "contentHash": "IRwlY1379ZgJ0oEJvjD+lDuOhJ5S1fsU5n/bEC5/i0+N9bo2WIMDAdaQ/qIdyK/gMJ/YWS+++GSX6rN7luqEvg==", + "dependencies": { + "NuGet.Common": "6.12.1", + "System.Security.Cryptography.ProtectedData": "4.4.0" + } + }, + "NuGet.DependencyResolver.Core": { + "type": "Transitive", + "resolved": "6.12.1", + "contentHash": "Nn4+pXW1qxGcq0OEQBJBh/FjKvWhzD49+kRzuOC9SkRZOrHsTaA12EsQ1mEE4rMUfqviaosmPEtvodJ4eKCmIw==", + "dependencies": { + "NuGet.Configuration": "6.12.1", + "NuGet.LibraryModel": "6.12.1", + "NuGet.Protocol": "6.12.1" + } + }, + "NuGet.Frameworks": { + "type": "Transitive", + "resolved": "6.12.1", + "contentHash": "kPaRD5RJC0ByUg+yGX6bDz5XHMI7OYmQwP8kbtef+vZ+csj/VDb5Bwas4ChxwhoAbI8lEvwP5/3aViQPpgNBow==" + }, + "NuGet.LibraryModel": { + "type": "Transitive", + "resolved": "6.12.1", + "contentHash": "IqX7Ze7qSbhx22lsdnMbLJpcqrmwGyQbyNk9JSsZ/u5p6PIaZyeWsvHHy+oXU8fuRmmKfHBnZEsceaCAw6Xy5A==", + "dependencies": { + "NuGet.Common": "6.12.1", + "NuGet.Versioning": "6.12.1" + } + }, + "NuGet.Packaging": { + "type": "Transitive", + "resolved": "6.12.1", + "contentHash": "6s5NO3VNX6fIx6GwuWZtIsal9W1xkelYd3Vg2KUAg1zGqnKC3wB5IZlombvVGVGcwyl/A+iDvpUwSvgeDoB3wA==", + "dependencies": { + "Newtonsoft.Json": "13.0.3", + "NuGet.Configuration": "6.12.1", + "NuGet.Versioning": "6.12.1", + "System.Security.Cryptography.Pkcs": "6.0.4" + } + }, + "NuGet.ProjectModel": { + "type": "Transitive", + "resolved": "6.12.1", + "contentHash": "nzpVqooG0qumADw5t8YMg9e+ezVqUhhoGvoihVmu5O9gJDqzkRYswQzyIVsE24dirBQabW0X0Sv53f6ZLJCv+g==", + "dependencies": { + "NuGet.DependencyResolver.Core": "6.12.1" + } + }, + "NuGet.Protocol": { + "type": "Transitive", + "resolved": "6.12.1", + "contentHash": "VBN7OtG/Y9Rnj1WT3G8X88ZHu5Pq+yzca5Z6OI/FWXcENVAQkUl0ml6Cv8ghOqYyiuvnObGDV9oWLD/bIuVtDw==", + "dependencies": { + "NuGet.Packaging": "6.12.1" + } + }, + "NuGet.Versioning": { + "type": "Transitive", + "resolved": "6.12.1", + "contentHash": "fJ6rFYANDnohFsdpaY79FvrJxI6murmoOxXz6nZlf819F48+IBKMnAIg3oIBRtZq5y498ObMtKnro5IitvizUg==" + }, + "Serilog": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "+cDryFR0GRhsGOnZSKwaDzRRl4MupvJ42FhCE4zhQRVanX0Jpg6WuCBk59OVhVDPmab1bB+nRykAnykYELA9qQ==" + }, + "Serilog.Extensions.Hosting": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "E7juuIc+gzoGxgzFooFgAV8g9BfiSXNKsUok9NmEpyAXg2odkcPsMa/Yo4axkJRlh0se7mkYQ1GXDaBemR+b6w==", + "dependencies": { + "Serilog": "4.3.0", + "Serilog.Extensions.Logging": "10.0.0" + } + }, + "Serilog.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "vx0kABKl2dWbBhhqAfTOk53/i8aV/5VaT3a6il9gn72Wqs2pM7EK2OB6No6xdqK2IaY6Zf9gdjLuK9BVa2rT+Q==", + "dependencies": { + "Serilog": "4.2.0" + } + }, + "Serilog.Formatting.Compact": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Sinks.Debug": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "4BzXcdrgRX7wde9PmHuYd9U6YqycCC28hhpKonK7hx0wb19eiuRj16fPcPSVp0o/Y1ipJuNLYQ00R3q2Zs8FDA==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "SQLitePCLRaw.bundle_e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.11", + "contentHash": "DC4nA7yWnf4UZdgJDF+9Mus4/cb0Y3Sfgi3gDnAoKNAIBwzkskNAbNbyu+u4atT0ruVlZNJfwZmwiEwE5oz9LQ==", + "dependencies": { + "SQLitePCLRaw.lib.e_sqlite3": "2.1.11", + "SQLitePCLRaw.provider.e_sqlite3": "2.1.11" + } + }, + "SQLitePCLRaw.core": { + "type": "Transitive", + "resolved": "2.1.11", + "contentHash": "PK0GLFkfhZzLQeR3PJf71FmhtHox+U3vcY6ZtswoMjrefkB9k6ErNJEnwXqc5KgXDSjige2XXrezqS39gkpQKA==" + }, + "SQLitePCLRaw.lib.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.11", + "contentHash": "Ev2ytaXiOlWZ4b3R67GZBsemTINslLD1DCJr2xiacpn4tbapu0Q4dHEzSvZSMnVWeE5nlObU3VZN2p81q3XOYQ==" + }, + "SQLitePCLRaw.provider.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.11", + "contentHash": "Y/0ZkR+r0Cg3DQFuCl1RBnv/tmxpIZRU3HUvelPw6MVaKHwYYR8YNvgs0vuNuXCMvlyJ+Fh88U1D4tah1tt6qw==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.11" + } + }, + "Swashbuckle.AspNetCore.Swagger": { + "type": "Transitive", + "resolved": "10.2.3", + "contentHash": "1jUUs3WQnrS0FUtaZPLSy1yYMEwS1zlvDmvQ2/eldPHUANX0LJSLVZecCMgSMdeGiRqeaRrIXLtSz++TCiTMww==", + "dependencies": { + "Microsoft.OpenApi": "2.7.5" + } + }, + "Swashbuckle.AspNetCore.SwaggerGen": { + "type": "Transitive", + "resolved": "10.2.3", + "contentHash": "y7t4coDRAeFYChmvlMRiH2OjbiRrm9AVIDgt17fQfs3x9PVAI5PiwWYOhg+4F13R4Q36WDc9lqfoOnNa3tNbGg==", + "dependencies": { + "Swashbuckle.AspNetCore.Swagger": "10.2.3" + } + }, + "Swashbuckle.AspNetCore.SwaggerUI": { + "type": "Transitive", + "resolved": "10.2.3", + "contentHash": "nthWONRs/FJ4yyG206g1cC52WEG8EqrjuMWjGdR+5XG7lbjFto6NqcI9EMICgVFom/UivIjUVwI76ZHbHwTPfQ==" + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" + }, + "System.Composition": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "3Djj70fFTraOarSKmRnmRy/zm4YurICm+kiCtI0dYRqGJnLX6nJ+G3WYuFJ173cAPax/gh96REcbNiVqcrypFQ==", + "dependencies": { + "System.Composition.AttributedModel": "9.0.0", + "System.Composition.Convention": "9.0.0", + "System.Composition.Hosting": "9.0.0", + "System.Composition.Runtime": "9.0.0", + "System.Composition.TypedParts": "9.0.0" + } + }, + "System.Composition.AttributedModel": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "iri00l/zIX9g4lHMY+Nz0qV1n40+jFYAmgsaiNn16xvt2RDwlqByNG4wgblagnDYxm3YSQQ0jLlC/7Xlk9CzyA==" + }, + "System.Composition.Convention": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "+vuqVP6xpi582XIjJi6OCsIxuoTZfR0M7WWufk3uGDeCl3wGW6KnpylUJ3iiXdPByPE0vR5TjJgR6hDLez4FQg==", + "dependencies": { + "System.Composition.AttributedModel": "9.0.0" + } + }, + "System.Composition.Hosting": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "OFqSeFeJYr7kHxDfaViGM1ymk7d4JxK//VSoNF9Ux0gpqkLsauDZpu89kTHHNdCWfSljbFcvAafGyBoY094btQ==", + "dependencies": { + "System.Composition.Runtime": "9.0.0" + } + }, + "System.Composition.Runtime": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "w1HOlQY1zsOWYussjFGZCEYF2UZXgvoYnS94NIu2CBnAGMbXFAX8PY8c92KwUItPmowal68jnVLBCzdrWLeEKA==" + }, + "System.Composition.TypedParts": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "aRZlojCCGEHDKqh43jaDgaVpYETsgd7Nx4g1zwLKMtv4iTo0627715ajEFNpEEBTgLmvZuv8K0EVxc3sM4NWJA==", + "dependencies": { + "System.Composition.AttributedModel": "9.0.0", + "System.Composition.Hosting": "9.0.0", + "System.Composition.Runtime": "9.0.0" + } + }, + "System.Configuration.ConfigurationManager": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "PdkuMrwDhXoKFo/JxISIi9E8L+QGn9Iquj2OKDWHB6Y/HnUOuBouF7uS3R4Hw3FoNmwwMo6hWgazQdyHIIs27A==", + "dependencies": { + "System.Security.Cryptography.ProtectedData": "9.0.0" + } + }, + "System.Data.DataSetExtensions": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "221clPs1445HkTBZPL+K9sDBdJRB8UN8rgjO3ztB0CQ26z//fmJXtlsr6whGatscsKGBrhJl5bwJuKSA8mwFOw==" + }, + "System.Reflection.MetadataLoadContext": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "SZxrQ4sQYnIcdwiO3G/lHZopbPYQ2lW0ioT4JezgccWUrKaKbHLJbAGZaDfkYjWcta1pWssAo3MOXLsR0ie4tQ==" + }, + "System.Security.Cryptography.Pkcs": { + "type": "Transitive", + "resolved": "6.0.4", + "contentHash": "LGbXi1oUJ9QgCNGXRO9ndzBL/GZgANcsURpMhNR8uO+rca47SZmciS3RSQUvlQRwK3QHZSHNOXzoMUASKA+Anw==" + }, + "System.Security.Cryptography.ProtectedData": { + "type": "Transitive", + "resolved": "9.0.6", + "contentHash": "yErfw/3pZkJE/VKza/Cm5idTpIKOy/vsmVi59Ta5SruPVtubzxb8CtnE8tyUpzs5pr0Y28GUFfSVzAhCLN3F/Q==" + } + } + } +} \ No newline at end of file diff --git a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Dotnet.Samples.AspNetCore.WebApi.Tests.csproj b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Dotnet.Samples.AspNetCore.WebApi.Tests.csproj new file mode 100644 index 0000000..5b1c14c --- /dev/null +++ b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Dotnet.Samples.AspNetCore.WebApi.Tests.csproj @@ -0,0 +1,29 @@ + + + + net10.0 + enable + enable + false + true + + + + + + + + + + + + + + + + + + en-US + + + diff --git a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Integration/PlayerRepositoryTests.cs b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Integration/PlayerRepositoryTests.cs new file mode 100644 index 0000000..614990d --- /dev/null +++ b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Integration/PlayerRepositoryTests.cs @@ -0,0 +1,186 @@ +using System.Data.Common; +using Dotnet.Samples.AspNetCore.WebApi.Data; +using Dotnet.Samples.AspNetCore.WebApi.Models; +using Dotnet.Samples.AspNetCore.WebApi.Repositories; +using Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities; +using FluentAssertions; + +namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Integration; + +/// +/// Integration tests for and . +/// Each test runs against an in-memory SQLite database with the full EF Core migration +/// chain applied via , which also validates +/// that the migration chain itself is healthy as a side effect. +/// +public class PlayerRepositoryTests : IAsyncLifetime +{ + private DbConnection _connection = default!; + private PlayerDbContext _dbContext = default!; + private PlayerRepository _repository = default!; + + public async Task InitializeAsync() + { + var (connection, options) = DatabaseFakes.CreateSqliteConnection(); + _connection = connection; + _dbContext = DatabaseFakes.CreateDbContext(options); + await _dbContext.MigrateAsync(); + _repository = new PlayerRepository(_dbContext); + } + + public async Task DisposeAsync() + { + await _dbContext.DisposeAsync(); + await _connection.DisposeAsync(); + } + + /* ------------------------------------------------------------------------- + * GetAllAsync + * ---------------------------------------------------------------------- */ + + [Fact] + [Trait("Category", "Integration")] + public async Task GetAllAsync_WhenCalled_ReturnsAllSeededPlayers() + { + // Act + var players = await _repository.GetAllAsync(); + + // Assert + players.Should().HaveCount(26); + _dbContext.ChangeTracker.Entries().Should().BeEmpty(); + } + + /* ------------------------------------------------------------------------- + * FindByIdAsync + * ---------------------------------------------------------------------- */ + + [Fact] + [Trait("Category", "Integration")] + public async Task FindByIdAsync_ExistingId_ReturnsPlayer() + { + // Arrange — resolve a real ID from the seeded database + var seeded = await _repository.GetAllAsync(); + var existingId = seeded[0].Id; + + // Act + var player = await _repository.FindByIdAsync(existingId); + + // Assert + player.Should().NotBeNull(); + player!.Id.Should().Be(existingId); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task FindByIdAsync_UnknownId_ReturnsNull() + { + // Act + var player = await _repository.FindByIdAsync(Guid.NewGuid()); + + // Assert + player.Should().BeNull(); + } + + /* ------------------------------------------------------------------------- + * RemoveAsync + * ---------------------------------------------------------------------- */ + + [Fact] + [Trait("Category", "Integration")] + public async Task RemoveAsync_ExistingEntity_RemovesFromDatabase() + { + // Arrange + var seeded = await _repository.GetAllAsync(); + var existingId = seeded[0].Id; + + // Act + await _repository.RemoveAsync(existingId); + + // Assert + var player = await _repository.FindByIdAsync(existingId); + player.Should().BeNull(); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task RemoveAsync_UnknownId_NoExceptionThrown() + { + // Arrange + var countBefore = (await _repository.GetAllAsync()).Count; + + // Act + var act = async () => await _repository.RemoveAsync(Guid.NewGuid()); + + // Assert + await act.Should().NotThrowAsync(); + var countAfter = (await _repository.GetAllAsync()).Count; + countAfter.Should().Be(countBefore); + } + + /* ------------------------------------------------------------------------- + * FindBySquadNumberAsync + * ---------------------------------------------------------------------- */ + + [Fact] + [Trait("Category", "Integration")] + public async Task FindBySquadNumberAsync_ExistingSquadNumber_ReturnsPlayer() + { + // Arrange + var expected = PlayerFakes.MakeFromStarting11(23); + + // Act + var player = await _repository.FindBySquadNumberAsync(expected.SquadNumber); + + // Assert + player.Should().NotBeNull(); + player!.SquadNumber.Should().Be(expected.SquadNumber); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task FindBySquadNumberAsync_UnknownSquadNumber_ReturnsNull() + { + // Arrange — derive a squad number guaranteed not to exist in the seeded data + var seeded = await _repository.GetAllAsync(); + var unknownSquadNumber = seeded.Max(p => p.SquadNumber) + 1; + + // Act + var player = await _repository.FindBySquadNumberAsync(unknownSquadNumber); + + // Assert + player.Should().BeNull(); + } + + /* ------------------------------------------------------------------------- + * SquadNumberExistsAsync + * ---------------------------------------------------------------------- */ + + [Fact] + [Trait("Category", "Integration")] + public async Task SquadNumberExistsAsync_ExistingSquadNumber_ReturnsTrue() + { + // Arrange + var expected = PlayerFakes.MakeFromStarting11(23); + + // Act + var exists = await _repository.SquadNumberExistsAsync(expected.SquadNumber); + + // Assert + exists.Should().BeTrue(); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task SquadNumberExistsAsync_UnknownSquadNumber_ReturnsFalse() + { + // Arrange — derive a squad number guaranteed not to exist in the seeded data + var seeded = await _repository.GetAllAsync(); + var unknownSquadNumber = seeded.Max(p => p.SquadNumber) + 1; + + // Act + var exists = await _repository.SquadNumberExistsAsync(unknownSquadNumber); + + // Assert + exists.Should().BeFalse(); + } +} diff --git a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Integration/PlayerWebApplicationTests.cs b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Integration/PlayerWebApplicationTests.cs new file mode 100644 index 0000000..57558e4 --- /dev/null +++ b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Integration/PlayerWebApplicationTests.cs @@ -0,0 +1,345 @@ +using System.Data.Common; +using System.Net; +using System.Net.Http.Json; +using Dotnet.Samples.AspNetCore.WebApi.Data; +using Dotnet.Samples.AspNetCore.WebApi.Migrations; +using Dotnet.Samples.AspNetCore.WebApi.Models; +using Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities; +using FluentAssertions; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.Extensions.DependencyInjection; + +namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Integration; + +/// +/// Integration tests for the player endpoints exposed by the web application. +/// Each test exercises the full ASP.NET Core request pipeline — routing, +/// middleware, validation, serialization — via +/// backed by an in-memory SQLite database. The factory is created fresh per test +/// (via ) to ensure state isolation. +/// +public class PlayerWebApplicationTests : IAsyncLifetime +{ + private WebApplicationFactory _factory = default!; + private WebApplicationFactory _authorizedFactory = default!; + private HttpClient _client = default!; + private HttpClient _authorizedClient = default!; + private DbConnection _connection = default!; + + public Task InitializeAsync() + { + var (connection, _) = DatabaseFakes.CreateSqliteConnection(); + _connection = connection; + + _factory = new WebApplicationFactory().WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(services => + { + // AddDbContextPool registers three service types. All three must be + // removed before re-registering with AddDbContext; leaving any one + // causes startup to fail when the pool tries to resolve its options. + var descriptors = services + .Where(d => + d.ServiceType == typeof(DbContextOptions) + || d.ServiceType == typeof(PlayerDbContext) + || ( + d.ServiceType.IsGenericType + && d.ServiceType.GetGenericArguments().Length > 0 + && d.ServiceType.GetGenericArguments()[0] == typeof(PlayerDbContext) + ) + ) + .ToList(); + + foreach (var descriptor in descriptors) + services.Remove(descriptor); + + services.AddDbContext(options => + options + .UseSqlite(_connection) + .ReplaceService() + ); + }); + }); + + _client = _factory.CreateClient(); + + // Derived factory that layers authentication on top of the base factory. + // Used only by tests targeting endpoints protected with [Authorize]. + // Keeping auth out of the base factory means unauthenticated tests will + // fail if an endpoint accidentally becomes protected. + _authorizedFactory = _factory.WithWebHostBuilder(builder => + builder.ConfigureTestServices(services => + services + .AddAuthentication(TestAuthHandler.SchemeName) + .AddScheme( + TestAuthHandler.SchemeName, + _ => { } + ) + ) + ); + + _authorizedClient = _authorizedFactory.CreateClient(); + return Task.CompletedTask; + } + + public async Task DisposeAsync() + { + _authorizedClient.Dispose(); + _client.Dispose(); + await _authorizedFactory.DisposeAsync(); + await _factory.DisposeAsync(); + await _connection.DisposeAsync(); + } + + /* ------------------------------------------------------------------------- + * GET /players + * ---------------------------------------------------------------------- */ + + [Fact] + [Trait("Category", "Integration")] + public async Task Get_Players_Existing_Returns200Ok() + { + // Act + var response = await _client.GetAsync("/players"); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + var players = await response.Content.ReadFromJsonAsync>(); + players.Should().HaveCount(26); + } + + /* ------------------------------------------------------------------------- + * GET /players/{id:Guid} + * ---------------------------------------------------------------------- */ + + [Fact] + [Trait("Category", "Integration")] + public async Task Get_PlayerById_Existing_Returns200Ok() + { + // Arrange — resolve a real ID from the seeded database + await using var scope = _authorizedFactory.Services.CreateAsyncScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var existingId = (await db.Players.FirstAsync()).Id; + + // Act + var response = await _authorizedClient.GetAsync($"/players/{existingId}"); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + var player = await response.Content.ReadFromJsonAsync(); + player.Should().NotBeNull(); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task Get_PlayerById_Unknown_Returns404NotFound() + { + // Act + var response = await _authorizedClient.GetAsync($"/players/{Guid.NewGuid()}"); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.NotFound); + var problem = await response.Content.ReadFromJsonAsync(); + problem!.Status.Should().Be(StatusCodes.Status404NotFound); + problem.Title.Should().Be("Not Found"); + } + + /* ------------------------------------------------------------------------- + * GET /players/squadNumber/{squadNumber:int} + * ---------------------------------------------------------------------- */ + + [Fact] + [Trait("Category", "Integration")] + public async Task Get_PlayerBySquadNumber_Existing_Returns200Ok() + { + // Act + var response = await _client.GetAsync("/players/squadNumber/23"); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + var player = await response.Content.ReadFromJsonAsync(); + player.Should().NotBeNull(); + player!.Dorsal.Should().Be(23); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task Get_PlayerBySquadNumber_Unknown_Returns404NotFound() + { + // Act + var response = await _client.GetAsync("/players/squadNumber/999"); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.NotFound); + var problem = await response.Content.ReadFromJsonAsync(); + problem!.Status.Should().Be(StatusCodes.Status404NotFound); + problem.Title.Should().Be("Not Found"); + } + + /* ------------------------------------------------------------------------- + * POST /players + * ---------------------------------------------------------------------- */ + + [Fact] + [Trait("Category", "Integration")] + public async Task Post_Players_Nonexistent_Returns201Created() + { + // Arrange — squad number 27 (Lo Celso) is not in the seeded data + var request = PlayerFakes.MakeRequestModelForCreate(); + + // Act + var response = await _client.PostAsJsonAsync("/players", request); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.Created); + var player = await response.Content.ReadFromJsonAsync(); + player.Should().NotBeNull(); + player!.Dorsal.Should().Be(request.SquadNumber); + } + + // Note: the controller's 409 Conflict branch (squad number already exists) is + // unreachable via the HTTP pipeline. The "Create" validation rule set includes + // BeUniqueSquadNumber, which returns a 422 validation error (Unprocessable Entity) + // before the controller's own duplicate check ever runs. The 409 path is covered + // by the unit test Post_Players_Existing_Returns409Conflict, where validation is + // mocked to pass so the controller logic can be exercised in isolation. + + [Fact] + [Trait("Category", "Integration")] + public async Task Post_Players_ValidationError_Returns422UnprocessableEntity() + { + // Arrange — SquadNumber 0 is the int default, fails NotEmpty + var request = PlayerFakes.MakeRequestModelForCreate(); + request.SquadNumber = 0; + + // Act + var response = await _client.PostAsJsonAsync("/players", request); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + var problem = await response.Content.ReadFromJsonAsync(); + problem!.Status.Should().Be(StatusCodes.Status422UnprocessableEntity); + } + + /* ------------------------------------------------------------------------- + * PUT /players/squadNumber/{squadNumber:int} + * ---------------------------------------------------------------------- */ + + [Fact] + [Trait("Category", "Integration")] + public async Task Put_PlayerBySquadNumber_Existing_Returns204NoContent() + { + // Arrange + var request = PlayerFakes.MakeRequestModelForUpdate(23); + + // Act + var response = await _client.PutAsJsonAsync("/players/squadNumber/23", request); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.NoContent); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task Put_PlayerBySquadNumber_Unknown_Returns404NotFound() + { + // Arrange — squad number 999 does not exist; body matches route to avoid mismatch error + var request = PlayerFakes.MakeRequestModelForUpdate(23); + request.SquadNumber = 999; + + // Act + var response = await _client.PutAsJsonAsync("/players/squadNumber/999", request); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.NotFound); + var problem = await response.Content.ReadFromJsonAsync(); + problem!.Status.Should().Be(StatusCodes.Status404NotFound); + problem.Title.Should().Be("Not Found"); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task Put_PlayerBySquadNumber_ValidationError_Returns422UnprocessableEntity() + { + // Arrange — SquadNumber -1 fails GreaterThan(0) + var request = PlayerFakes.MakeRequestModelForUpdate(23); + request.SquadNumber = -1; + + // Act + var response = await _client.PutAsJsonAsync("/players/squadNumber/23", request); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + var problem = await response.Content.ReadFromJsonAsync(); + problem!.Status.Should().Be(StatusCodes.Status422UnprocessableEntity); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task Put_PlayerBySquadNumber_SquadNumberMismatch_Returns400BadRequest() + { + // Arrange — body squad number (99) differs from route (23); both are valid values + // so validation passes and the mismatch guard in the controller fires instead + var request = PlayerFakes.MakeRequestModelForUpdate(23); + request.SquadNumber = 99; + + // Act + var response = await _client.PutAsJsonAsync("/players/squadNumber/23", request); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + var problem = await response.Content.ReadFromJsonAsync(); + problem!.Status.Should().Be(StatusCodes.Status400BadRequest); + problem.Title.Should().Be("Bad Request"); + } + + /* ------------------------------------------------------------------------- + * DELETE /players/squadNumber/{squadNumber:int} + * ---------------------------------------------------------------------- */ + + [Fact] + [Trait("Category", "Integration")] + public async Task Delete_PlayerBySquadNumber_Existing_Returns204NoContent() + { + // Act + var response = await _client.DeleteAsync("/players/squadNumber/23"); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.NoContent); + } + + [Fact] + [Trait("Category", "Integration")] + public async Task Delete_PlayerBySquadNumber_Unknown_Returns404NotFound() + { + // Act + var response = await _client.DeleteAsync("/players/squadNumber/999"); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.NotFound); + var problem = await response.Content.ReadFromJsonAsync(); + problem!.Status.Should().Be(StatusCodes.Status404NotFound); + problem.Title.Should().Be("Not Found"); + } + + /* ------------------------------------------------------------------------- + * GET /health + * ---------------------------------------------------------------------- */ + + [Fact] + [Trait("Category", "Integration")] + public async Task Get_Health_Healthy_Returns200Ok() + { + // Act + var response = await _client.GetAsync("/health"); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + } +} diff --git a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerControllerTests.cs b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerControllerTests.cs new file mode 100644 index 0000000..f0a138c --- /dev/null +++ b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerControllerTests.cs @@ -0,0 +1,544 @@ +using Dotnet.Samples.AspNetCore.WebApi.Controllers; +using Dotnet.Samples.AspNetCore.WebApi.Models; +using Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities; +using FluentAssertions; +using FluentValidation; +using FluentValidation.Results; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; +using Moq; + +namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Unit; + +public class PlayerControllerTests : IDisposable +{ + private bool _disposed; + + public PlayerControllerTests() + { + Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development"); + } + + /* ------------------------------------------------------------------------- + * HTTP POST + * ---------------------------------------------------------------------- */ + + [Fact] + [Trait("Category", "Unit")] + public async Task Post_Players_ValidationError_Returns422UnprocessableEntity() + { + // Arrange + var request = PlayerFakes.MakeRequestModelForCreate(); + var (service, logger, validator) = PlayerMocks.InitControllerMocks(); + validator + .Setup(validator => + validator.ValidateAsync( + It.IsAny(), + It.IsAny() + ) + ) + .ReturnsAsync( + new ValidationResult( + new List + { + new("SquadNumber", "SquadNumber must be greater than 0."), + } + ) + ); + + var controller = new PlayerController(service.Object, logger.Object, validator.Object); + + // Act + var result = await controller.PostAsync(request); + + // Assert + service.Verify(service => service.RetrieveBySquadNumberAsync(It.IsAny()), Times.Never); + service.Verify(service => service.CreateAsync(It.IsAny()), Times.Never); + validator.Verify( + validator => + validator.ValidateAsync( + It.IsAny(), + It.IsAny() + ), + Times.Once + ); + var httpResult = result.Should().BeOfType().Subject; + httpResult.StatusCode.Should().Be(StatusCodes.Status422UnprocessableEntity); + var problemDetails = httpResult + .ProblemDetails.Should() + .BeOfType() + .Subject; + problemDetails.Status.Should().Be(StatusCodes.Status422UnprocessableEntity); + problemDetails + .Errors.Should() + .ContainKey("SquadNumber") + .WhoseValue.Should() + .Contain("SquadNumber must be greater than 0."); + } + + [Fact] + [Trait("Category", "Unit")] + public async Task Post_Players_Existing_Returns409Conflict() + { + // Arrange + var request = PlayerFakes.MakeRequestModelForCreate(); + var response = PlayerFakes.MakeResponseModelForCreate(); + var (service, logger, validator) = PlayerMocks.InitControllerMocks(); + service + .Setup(service => service.RetrieveBySquadNumberAsync(request.SquadNumber)) + .ReturnsAsync(response); + validator + .Setup(validator => + validator.ValidateAsync( + It.IsAny(), + It.IsAny() + ) + ) + .ReturnsAsync(new ValidationResult(new List { })); + + var controller = new PlayerController(service.Object, logger.Object, validator.Object); + + // Act + var result = await controller.PostAsync(request); + + // Assert + service.Verify(service => service.RetrieveBySquadNumberAsync(It.IsAny()), Times.Once); + service.Verify(service => service.CreateAsync(It.IsAny()), Times.Never); + validator.Verify( + validator => + validator.ValidateAsync( + It.IsAny(), + It.IsAny() + ), + Times.Once + ); + var httpResult = result.Should().BeOfType>().Subject; + httpResult.StatusCode.Should().Be(StatusCodes.Status409Conflict); + } + + [Fact] + [Trait("Category", "Unit")] + public async Task Post_Players_Nonexistent_Returns201Created() + { + // Arrange + var request = PlayerFakes.MakeRequestModelForCreate(); + var response = PlayerFakes.MakeResponseModelForCreate(); + var (service, logger, validator) = PlayerMocks.InitControllerMocks(); + service + .Setup(service => service.RetrieveBySquadNumberAsync(request.SquadNumber)) + .ReturnsAsync(null as PlayerResponseModel); + service.Setup(service => service.CreateAsync(request)).ReturnsAsync(response); + validator + .Setup(validator => + validator.ValidateAsync( + It.IsAny(), + It.IsAny() + ) + ) + .ReturnsAsync(new ValidationResult(new List { })); + + var controller = new PlayerController(service.Object, logger.Object, validator.Object) + { + Url = PlayerMocks.SetupUrlHelperMock().Object, + }; + + // Act + var result = await controller.PostAsync(request); + + // Assert + service.Verify(service => service.RetrieveBySquadNumberAsync(It.IsAny()), Times.Once); + service.Verify(service => service.CreateAsync(It.IsAny()), Times.Once); + validator.Verify( + validator => + validator.ValidateAsync( + It.IsAny(), + It.IsAny() + ), + Times.Once + ); + var httpResult = result.Should().BeOfType>().Subject; + httpResult.StatusCode.Should().Be(StatusCodes.Status201Created); + httpResult.Value.Should().BeEquivalentTo(response); + httpResult.RouteName.Should().Be("RetrieveBySquadNumber"); + httpResult.RouteValues.Should().NotBeNull().And.ContainKey("squadNumber"); + httpResult.RouteValues!["squadNumber"].Should().Be(response.Dorsal); + } + + /* ------------------------------------------------------------------------- + * HTTP GET + * ---------------------------------------------------------------------- */ + + [Fact] + [Trait("Category", "Unit")] + public async Task Get_Players_Existing_ReturnsPlayers() + { + // Arrange + var response = PlayerFakes.MakeResponseModelsForRetrieve(); + var (service, logger, validator) = PlayerMocks.InitControllerMocks(); + service.Setup(service => service.RetrieveAsync()).ReturnsAsync(response); + + var controller = new PlayerController(service.Object, logger.Object, validator.Object); + + // Act + var result = await controller.GetAsync(); + + // Assert + service.Verify(service => service.RetrieveAsync(), Times.Once); + var httpResult = result.Should().BeOfType>>().Subject; + httpResult.StatusCode.Should().Be(StatusCodes.Status200OK); + httpResult.Value.Should().NotBeNull().And.BeOfType>(); + httpResult.Value.Should().BeEquivalentTo(response); + } + + [Fact] + [Trait("Category", "Unit")] + public async Task Get_Players_Empty_Returns200OkWithEmptyList() + { + // Arrange + var (service, logger, validator) = PlayerMocks.InitControllerMocks(); + service.Setup(service => service.RetrieveAsync()).ReturnsAsync([]); + + var controller = new PlayerController(service.Object, logger.Object, validator.Object); + + // Act + var result = await controller.GetAsync(); + + // Assert + service.Verify(service => service.RetrieveAsync(), Times.Once); + var httpResult = result.Should().BeOfType>>().Subject; + httpResult.StatusCode.Should().Be(StatusCodes.Status200OK); + httpResult.Value.Should().NotBeNull().And.BeEmpty(); + } + + [Fact] + [Trait("Category", "Unit")] + public async Task Get_PlayerById_Unknown_Returns404NotFound() + { + // Arrange + var id = Guid.NewGuid(); + var (service, logger, validator) = PlayerMocks.InitControllerMocks(); + service + .Setup(service => service.RetrieveByIdAsync(id)) + .ReturnsAsync(null as PlayerResponseModel); + + var controller = new PlayerController(service.Object, logger.Object, validator.Object); + + // Act + var result = await controller.GetByIdAsync(id); + + // Assert + service.Verify(service => service.RetrieveByIdAsync(It.IsAny()), Times.Once); + var httpResult = result.Should().BeOfType().Subject; + httpResult.StatusCode.Should().Be(StatusCodes.Status404NotFound); + } + + [Fact] + [Trait("Category", "Unit")] + public async Task Get_PlayerById_Existing_Returns200OK() + { + // Arrange + var id = Guid.NewGuid(); + var response = PlayerFakes.MakeResponseModelForRetrieve(10); + var (service, logger, validator) = PlayerMocks.InitControllerMocks(); + service.Setup(service => service.RetrieveByIdAsync(id)).ReturnsAsync(response); + + var controller = new PlayerController(service.Object, logger.Object, validator.Object); + + // Act + var result = await controller.GetByIdAsync(id); + + // Assert + service.Verify(service => service.RetrieveByIdAsync(It.IsAny()), Times.Once); + var httpResult = result.Should().BeOfType>().Subject; + httpResult.StatusCode.Should().Be(StatusCodes.Status200OK); + httpResult.Value.Should().NotBeNull().And.BeOfType(); + httpResult.Value.Should().BeEquivalentTo(response); + } + + [Fact] + [Trait("Category", "Unit")] + public async Task Get_PlayerBySquadNumber_Unknown_Returns404NotFound() + { + // Arrange + var squadNumber = 999; + var (service, logger, validator) = PlayerMocks.InitControllerMocks(); + service + .Setup(service => service.RetrieveBySquadNumberAsync(squadNumber)) + .ReturnsAsync(null as PlayerResponseModel); + + var controller = new PlayerController(service.Object, logger.Object, validator.Object); + + // Act + var result = await controller.GetBySquadNumberAsync(squadNumber); + + // Assert + service.Verify(service => service.RetrieveBySquadNumberAsync(It.IsAny()), Times.Once); + var httpResult = result.Should().BeOfType().Subject; + httpResult.StatusCode.Should().Be(StatusCodes.Status404NotFound); + } + + [Fact] + [Trait("Category", "Unit")] + public async Task Get_PlayerBySquadNumber_Existing_Returns200OK() + { + // Arrange + var squadNumber = 10; + var response = PlayerFakes.MakeResponseModelForRetrieve(squadNumber); + var (service, logger, validator) = PlayerMocks.InitControllerMocks(); + service + .Setup(service => service.RetrieveBySquadNumberAsync(squadNumber)) + .ReturnsAsync(response); + + var controller = new PlayerController(service.Object, logger.Object, validator.Object); + + // Act + var result = await controller.GetBySquadNumberAsync(squadNumber); + + // Assert + service.Verify(service => service.RetrieveBySquadNumberAsync(It.IsAny()), Times.Once); + var httpResult = result.Should().BeOfType>().Subject; + httpResult.StatusCode.Should().Be(StatusCodes.Status200OK); + httpResult.Value.Should().NotBeNull().And.BeOfType(); + httpResult.Value.Should().BeEquivalentTo(response); + } + + /* ------------------------------------------------------------------------- + * HTTP PUT + * ---------------------------------------------------------------------- */ + + [Fact] + [Trait("Category", "Unit")] + public async Task Put_PlayerBySquadNumber_ValidationError_Returns422UnprocessableEntity() + { + // Arrange + var squadNumber = 20; + var request = PlayerFakes.MakeRequestModelForUpdate(squadNumber); + request.SquadNumber = -999; // Invalid Squad Number + var (service, logger, validator) = PlayerMocks.InitControllerMocks(); + + var controller = new PlayerController(service.Object, logger.Object, validator.Object); + + validator + .Setup(validator => + validator.ValidateAsync( + It.IsAny(), + It.IsAny() + ) + ) + .ReturnsAsync( + new ValidationResult( + new List + { + new("SquadNumber", "SquadNumber must be greater than 0."), + } + ) + ); + + // Act + var result = await controller.PutAsync(squadNumber, request); + + // Assert + service.Verify(service => service.RetrieveBySquadNumberAsync(It.IsAny()), Times.Never); + service.Verify(service => service.UpdateAsync(It.IsAny()), Times.Never); + validator.Verify( + validator => + validator.ValidateAsync( + It.IsAny(), + It.IsAny() + ), + Times.Once + ); + var httpResult = result.Should().BeOfType().Subject; + httpResult.StatusCode.Should().Be(StatusCodes.Status422UnprocessableEntity); + var problemDetails = httpResult + .ProblemDetails.Should() + .BeOfType() + .Subject; + problemDetails.Status.Should().Be(StatusCodes.Status422UnprocessableEntity); + problemDetails + .Errors.Should() + .ContainKey("SquadNumber") + .WhoseValue.Should() + .Contain("SquadNumber must be greater than 0."); + } + + [Fact] + [Trait("Category", "Unit")] + public async Task Put_PlayerBySquadNumber_Unknown_Returns404NotFound() + { + // Arrange + var squadNumber = 999; + var (service, logger, validator) = PlayerMocks.InitControllerMocks(); + service + .Setup(service => service.RetrieveBySquadNumberAsync(squadNumber)) + .ReturnsAsync(null as PlayerResponseModel); + validator + .Setup(validator => + validator.ValidateAsync( + It.IsAny(), + It.IsAny() + ) + ) + .ReturnsAsync(new ValidationResult(new List { })); + + var controller = new PlayerController(service.Object, logger.Object, validator.Object); + + // Act + var result = await controller.PutAsync( + squadNumber, + new PlayerRequestModel { SquadNumber = squadNumber } + ); + + // Assert + service.Verify(service => service.RetrieveBySquadNumberAsync(It.IsAny()), Times.Once); + service.Verify(service => service.UpdateAsync(It.IsAny()), Times.Never); + validator.Verify( + validator => + validator.ValidateAsync( + It.IsAny(), + It.IsAny() + ), + Times.Once + ); + var httpResult = result.Should().BeOfType().Subject; + httpResult.StatusCode.Should().Be(StatusCodes.Status404NotFound); + } + + [Fact] + [Trait("Category", "Unit")] + public async Task Put_PlayerBySquadNumber_SquadNumberMismatch_Returns400BadRequest() + { + // Arrange + var squadNumber = 23; + var request = PlayerFakes.MakeRequestModelForUpdate(squadNumber); + request.SquadNumber = 99; // Mismatched squad number + var response = PlayerFakes.MakeResponseModelForUpdate(squadNumber); + var (service, logger, validator) = PlayerMocks.InitControllerMocks(); + service + .Setup(service => service.RetrieveBySquadNumberAsync(squadNumber)) + .ReturnsAsync(response); + validator + .Setup(validator => + validator.ValidateAsync( + It.IsAny(), + It.IsAny() + ) + ) + .ReturnsAsync(new ValidationResult(new List { })); + + var controller = new PlayerController(service.Object, logger.Object, validator.Object); + + // Act + var result = await controller.PutAsync(squadNumber, request); + + // Assert + service.Verify(service => service.RetrieveBySquadNumberAsync(It.IsAny()), Times.Never); + service.Verify(service => service.UpdateAsync(It.IsAny()), Times.Never); + var httpResult = result.Should().BeOfType().Subject; + httpResult.StatusCode.Should().Be(StatusCodes.Status400BadRequest); + } + + [Fact] + [Trait("Category", "Unit")] + public async Task Put_PlayerBySquadNumber_Existing_Returns204NoContent() + { + // Arrange + var squadNumber = 23; + var request = PlayerFakes.MakeRequestModelForUpdate(squadNumber); + var response = PlayerFakes.MakeResponseModelForUpdate(squadNumber); + request.FirstName = "Emiliano"; + request.MiddleName = string.Empty; + var (service, logger, validator) = PlayerMocks.InitControllerMocks(); + service + .Setup(service => service.RetrieveBySquadNumberAsync(squadNumber)) + .ReturnsAsync(response); + service.Setup(service => service.UpdateAsync(request)); + validator + .Setup(validator => + validator.ValidateAsync( + It.IsAny(), + It.IsAny() + ) + ) + .ReturnsAsync(new ValidationResult(new List { })); + + var controller = new PlayerController(service.Object, logger.Object, validator.Object); + + // Act + var result = await controller.PutAsync(squadNumber, request); + + // Assert + service.Verify(service => service.RetrieveBySquadNumberAsync(It.IsAny()), Times.Once); + service.Verify(service => service.UpdateAsync(It.IsAny()), Times.Once); + var httpResult = result.Should().BeOfType().Subject; + httpResult.StatusCode.Should().Be(StatusCodes.Status204NoContent); + } + + /* ------------------------------------------------------------------------- + * HTTP DELETE + * ---------------------------------------------------------------------- */ + + [Fact] + [Trait("Category", "Unit")] + public async Task Delete_PlayerBySquadNumber_Unknown_Returns404NotFound() + { + // Arrange + var squadNumber = 999; + var (service, logger, validator) = PlayerMocks.InitControllerMocks(); + service + .Setup(service => service.RetrieveBySquadNumberAsync(squadNumber)) + .ReturnsAsync(null as PlayerResponseModel); + + var controller = new PlayerController(service.Object, logger.Object, validator.Object); + + // Act + var result = await controller.DeleteAsync(squadNumber); + + // Assert + service.Verify(service => service.RetrieveBySquadNumberAsync(It.IsAny()), Times.Once); + service.Verify(service => service.DeleteAsync(It.IsAny()), Times.Never); + var httpResult = result.Should().BeOfType().Subject; + httpResult.StatusCode.Should().Be(StatusCodes.Status404NotFound); + } + + [Fact] + [Trait("Category", "Unit")] + public async Task Delete_PlayerBySquadNumber_Existing_Returns204NoContent() + { + // Arrange + var response = PlayerFakes.MakeResponseModelForCreate(); + var squadNumber = response.Dorsal; + var (service, logger, validator) = PlayerMocks.InitControllerMocks(); + service + .Setup(service => service.RetrieveBySquadNumberAsync(squadNumber)) + .ReturnsAsync(response); + service.Setup(service => service.DeleteAsync(squadNumber)); + + var controller = new PlayerController(service.Object, logger.Object, validator.Object); + + // Act + var result = await controller.DeleteAsync(squadNumber); + + // Assert + service.Verify(service => service.RetrieveBySquadNumberAsync(It.IsAny()), Times.Once); + service.Verify(service => service.DeleteAsync(It.IsAny()), Times.Once); + var httpResult = result.Should().BeOfType().Subject; + httpResult.StatusCode.Should().Be(StatusCodes.Status204NoContent); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", null); + _disposed = true; + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } +} diff --git a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerServiceTests.cs b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerServiceTests.cs new file mode 100644 index 0000000..6eef65b --- /dev/null +++ b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerServiceTests.cs @@ -0,0 +1,400 @@ +using System.Diagnostics; +using Dotnet.Samples.AspNetCore.WebApi.Models; +using Dotnet.Samples.AspNetCore.WebApi.Services; +using Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities; +using FluentAssertions; +using Moq; + +namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Unit; + +public class PlayerServiceTests +{ + /* ------------------------------------------------------------------------- + * Create + * ---------------------------------------------------------------------- */ + + [Fact] + [Trait("Category", "Unit")] + public async Task CreateAsync_WhenCalled_AddsPlayerAndRemovesCache() + { + // Arrange + var request = PlayerFakes.MakeRequestModelForCreate(); + var response = PlayerFakes.MakeResponseModelForCreate(); + var (repository, logger, memoryCache, mapper, environment) = PlayerMocks.InitServiceMocks(); + mapper.Setup(mapper => mapper.Map(request)).Returns(response); + + var service = new PlayerService( + repository.Object, + logger.Object, + memoryCache.Object, + mapper.Object, + environment.Object + ); + + // Act + await service.CreateAsync(request); + + // Assert + repository.Verify(repository => repository.AddAsync(It.IsAny()), Times.Once); + memoryCache.Verify(cache => cache.Remove(It.IsAny()), Times.Once); + mapper.Verify( + mapper => mapper.Map(It.IsAny()), + Times.Once + ); + } + + /* ------------------------------------------------------------------------- + * Retrieve + * ---------------------------------------------------------------------- */ + + [Fact] + [Trait("Category", "Unit")] + public async Task RetrieveAsync_CacheMiss_QueriesRepositoryAndCachesResult() + { + // Arrange + var value = It.IsAny(); + var players = PlayerFakes.MakeStarting11(); + var response = PlayerFakes.MakeResponseModelsForRetrieve(); + var (repository, logger, memoryCache, mapper, environment) = PlayerMocks.InitServiceMocks(); + repository.Setup(repository => repository.GetAllAsync()).ReturnsAsync(players); + mapper.Setup(mapper => mapper.Map>(players)).Returns(response); + + var service = new PlayerService( + repository.Object, + logger.Object, + memoryCache.Object, + mapper.Object, + environment.Object + ); + + // Act + var result = await service.RetrieveAsync(); + + // Assert + repository.Verify(repository => repository.GetAllAsync(), Times.Once); + memoryCache.Verify(cache => cache.TryGetValue(It.IsAny(), out value), Times.Once); + memoryCache.Verify(cache => cache.CreateEntry(It.IsAny()), Times.Once); + mapper.Verify( + mapper => mapper.Map>(It.IsAny>()), + Times.Once + ); + result.Should().BeEquivalentTo(response); + } + + [Fact] + [Trait("Category", "Unit")] + public async Task RetrieveAsync_CacheHit_ReturnsBeforeQueryingRepository() + { + // Arrange + var value = It.IsAny(); + var players = PlayerFakes.MakeStarting11(); + var response = PlayerFakes.MakeResponseModelsForRetrieve(); + var (repository, logger, memoryCache, mapper, environment) = PlayerMocks.InitServiceMocks(); + repository.Setup(repository => repository.GetAllAsync()).ReturnsAsync(players); + mapper.Setup(mapper => mapper.Map>(players)).Returns(response); + + var service = new PlayerService( + repository.Object, + logger.Object, + memoryCache.Object, + mapper.Object, + environment.Object + ); + + // Act + var first = await ExecutionTimeAsync(() => service.RetrieveAsync()); + var second = await ExecutionTimeAsync(() => service.RetrieveAsync()); + + // Assert + memoryCache.Verify( + cache => cache.TryGetValue(It.IsAny(), out value), + Times.Exactly(2) // first + second + ); + memoryCache.Verify(cache => cache.CreateEntry(It.IsAny()), Times.Once); // first only + repository.Verify(repository => repository.GetAllAsync(), Times.Once); // first only + mapper.Verify( + mapper => mapper.Map>(It.IsAny>()), + Times.Once // first only + ); + second.Should().BeLessThan(first); + } + + [Fact] + [Trait("Category", "Unit")] + public async Task RetrieveByIdAsync_PlayerNotFound_ReturnsNull() + { + // Arrange + var id = Guid.NewGuid(); + var (repository, logger, memoryCache, mapper, environment) = PlayerMocks.InitServiceMocks(); + repository.Setup(repository => repository.FindByIdAsync(id)).ReturnsAsync(null as Player); + + var service = new PlayerService( + repository.Object, + logger.Object, + memoryCache.Object, + mapper.Object, + environment.Object + ); + + // Act + var result = await service.RetrieveByIdAsync(id); + + // Assert + repository.Verify(repository => repository.FindByIdAsync(It.IsAny()), Times.Once); + mapper.Verify(mapper => mapper.Map(It.IsAny()), Times.Never); + result.Should().BeNull(); + } + + [Fact] + [Trait("Category", "Unit")] + public async Task RetrieveByIdAsync_PlayerFound_ReturnsMappedResponseModel() + { + // Arrange + var id = Guid.NewGuid(); + var squadNumber = 10; + var player = PlayerFakes.MakeFromStarting11(squadNumber); + var response = PlayerFakes.MakeResponseModelForRetrieve(squadNumber); + var (repository, logger, memoryCache, mapper, environment) = PlayerMocks.InitServiceMocks(); + repository.Setup(repository => repository.FindByIdAsync(id)).ReturnsAsync(player); + mapper.Setup(mapper => mapper.Map(player)).Returns(response); + + var service = new PlayerService( + repository.Object, + logger.Object, + memoryCache.Object, + mapper.Object, + environment.Object + ); + + // Act + var result = await service.RetrieveByIdAsync(id); + + // Assert + repository.Verify(repository => repository.FindByIdAsync(It.IsAny()), Times.Once); + mapper.Verify(mapper => mapper.Map(It.IsAny()), Times.Once); + result.Should().BeOfType(); + result.Should().BeEquivalentTo(response); + } + + [Fact] + [Trait("Category", "Unit")] + public async Task RetrieveBySquadNumberAsync_PlayerNotFound_ReturnsNull() + { + // Arrange + var squadNumber = 999; + var (repository, logger, memoryCache, mapper, environment) = PlayerMocks.InitServiceMocks(); + repository + .Setup(repository => repository.FindBySquadNumberAsync(squadNumber)) + .ReturnsAsync(null as Player); + + var service = new PlayerService( + repository.Object, + logger.Object, + memoryCache.Object, + mapper.Object, + environment.Object + ); + + // Act + var result = await service.RetrieveBySquadNumberAsync(squadNumber); + + // Assert + repository.Verify( + repository => repository.FindBySquadNumberAsync(It.IsAny()), + Times.Once + ); + mapper.Verify(mapper => mapper.Map(It.IsAny()), Times.Never); + result.Should().BeNull(); + } + + [Fact] + [Trait("Category", "Unit")] + public async Task RetrieveBySquadNumberAsync_PlayerFound_ReturnsMappedResponseModel() + { + // Arrange + var squadNumber = 10; + var player = PlayerFakes.MakeFromStarting11(squadNumber); + var response = PlayerFakes.MakeResponseModelForRetrieve(squadNumber); + var (repository, logger, memoryCache, mapper, environment) = PlayerMocks.InitServiceMocks(); + repository + .Setup(repository => repository.FindBySquadNumberAsync(squadNumber)) + .ReturnsAsync(player); + mapper.Setup(mapper => mapper.Map(player)).Returns(response); + + var service = new PlayerService( + repository.Object, + logger.Object, + memoryCache.Object, + mapper.Object, + environment.Object + ); + + // Act + var result = await service.RetrieveBySquadNumberAsync(squadNumber); + + // Assert + repository.Verify( + repository => repository.FindBySquadNumberAsync(It.IsAny()), + Times.Once + ); + mapper.Verify(mapper => mapper.Map(It.IsAny()), Times.Once); + result.Should().BeOfType(); + result.Should().BeEquivalentTo(response); + } + + /* ------------------------------------------------------------------------- + * Update + * ---------------------------------------------------------------------- */ + + [Fact] + [Trait("Category", "Unit")] + public async Task UpdateAsync_PlayerFound_UpdatesRepositoryAndRemovesCache() + { + // Arrange + var squadNumber = 23; + var player = PlayerFakes.MakeFromStarting11(squadNumber); + var request = PlayerFakes.MakeRequestModelForUpdate(squadNumber); + var (repository, logger, memoryCache, mapper, environment) = PlayerMocks.InitServiceMocks(); + repository + .Setup(repository => repository.FindBySquadNumberAsync(squadNumber)) + .ReturnsAsync(player); + + var service = new PlayerService( + repository.Object, + logger.Object, + memoryCache.Object, + mapper.Object, + environment.Object + ); + + // Act + await service.UpdateAsync(request); + + // Assert + repository.Verify( + repository => repository.FindBySquadNumberAsync(It.IsAny()), + Times.Once + ); + repository.Verify(repository => repository.UpdateAsync(It.IsAny()), Times.Once); + memoryCache.Verify(cache => cache.Remove(It.IsAny()), Times.Once); + mapper.Verify( + mapper => mapper.Map(It.IsAny(), It.IsAny()), + Times.Once + ); + } + + [Fact] + [Trait("Category", "Unit")] + public async Task UpdateAsync_PlayerNotFound_DoesNotUpdateRepository() + { + // Arrange + var squadNumber = 999; + var request = PlayerFakes.MakeRequestModelForCreate(); + request.SquadNumber = squadNumber; + var (repository, logger, memoryCache, mapper, environment) = PlayerMocks.InitServiceMocks(); + repository + .Setup(repository => repository.FindBySquadNumberAsync(squadNumber)) + .ReturnsAsync(null as Player); + + var service = new PlayerService( + repository.Object, + logger.Object, + memoryCache.Object, + mapper.Object, + environment.Object + ); + + // Act + await service.UpdateAsync(request); + + // Assert + repository.Verify( + repository => repository.FindBySquadNumberAsync(It.IsAny()), + Times.Once + ); + repository.Verify(repository => repository.UpdateAsync(It.IsAny()), Times.Never); + memoryCache.Verify(cache => cache.Remove(It.IsAny()), Times.Never); + mapper.Verify( + mapper => mapper.Map(It.IsAny(), It.IsAny()), + Times.Never + ); + } + + /* ------------------------------------------------------------------------- + * Delete + * ---------------------------------------------------------------------- */ + + [Fact] + [Trait("Category", "Unit")] + public async Task DeleteAsync_PlayerFound_RemovesFromRepositoryAndRemovesCache() + { + // Arrange + var squadNumber = 26; + var player = PlayerFakes.MakeFromStarting11(squadNumber); + var (repository, logger, memoryCache, mapper, environment) = PlayerMocks.InitServiceMocks(); + repository + .Setup(repository => repository.FindBySquadNumberAsync(squadNumber)) + .ReturnsAsync(player); + + var service = new PlayerService( + repository.Object, + logger.Object, + memoryCache.Object, + mapper.Object, + environment.Object + ); + + // Act + await service.DeleteAsync(squadNumber); + + // Assert + repository.Verify( + repository => repository.FindBySquadNumberAsync(It.IsAny()), + Times.Once + ); + repository.Verify(repository => repository.RemoveAsync(It.IsAny()), Times.Once); + memoryCache.Verify(cache => cache.Remove(It.IsAny()), Times.Once); + } + + [Fact] + [Trait("Category", "Unit")] + public async Task DeleteAsync_PlayerNotFound_DoesNotRemoveFromRepository() + { + // Arrange + var squadNumber = 999; + var (repository, logger, memoryCache, mapper, environment) = PlayerMocks.InitServiceMocks(); + repository + .Setup(repository => repository.FindBySquadNumberAsync(squadNumber)) + .ReturnsAsync(null as Player); + + var service = new PlayerService( + repository.Object, + logger.Object, + memoryCache.Object, + mapper.Object, + environment.Object + ); + + // Act + await service.DeleteAsync(squadNumber); + + // Assert + repository.Verify( + repository => repository.FindBySquadNumberAsync(It.IsAny()), + Times.Once + ); + repository.Verify(repository => repository.RemoveAsync(It.IsAny()), Times.Never); + memoryCache.Verify(cache => cache.Remove(It.IsAny()), Times.Never); + } + + private static async Task ExecutionTimeAsync(Func awaitable) + { + var stopwatch = new Stopwatch(); + + stopwatch.Start(); + await awaitable(); + stopwatch.Stop(); + + return stopwatch.ElapsedMilliseconds; + } +} diff --git a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerValidatorTests.cs b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerValidatorTests.cs new file mode 100644 index 0000000..4be8017 --- /dev/null +++ b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerValidatorTests.cs @@ -0,0 +1,381 @@ +using Dotnet.Samples.AspNetCore.WebApi.Models; +using Dotnet.Samples.AspNetCore.WebApi.Repositories; +using Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities; +using Dotnet.Samples.AspNetCore.WebApi.Validators; +using FluentAssertions; +using FluentValidation; +using Moq; + +namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Unit; + +public class PlayerValidatorTests +{ + private sealed class FakeTimeProvider(DateTimeOffset utcNow) : TimeProvider + { + public override DateTimeOffset GetUtcNow() => utcNow; + } + + private static PlayerRequestModelValidator CreateValidator( + Mock? repositoryMock = null, + TimeProvider? timeProvider = null + ) + { + var mock = repositoryMock ?? new Mock(); + return new PlayerRequestModelValidator(mock.Object, timeProvider); + } + + /* ------------------------------------------------------------------------- + * Valid request + * ---------------------------------------------------------------------- */ + + [Fact] + [Trait("Category", "Unit")] + public async Task ValidateAsync_ValidRequest_ReturnsNoErrors() + { + // Arrange + var request = PlayerFakes.MakeRequestModelForCreate(); + var repositoryMock = new Mock(); + repositoryMock + .Setup(repository => repository.FindBySquadNumberAsync(request.SquadNumber)) + .ReturnsAsync(null as Player); + var validator = CreateValidator(repositoryMock); + + // Act + var result = await validator.ValidateAsync( + request, + options => options.IncludeRuleSets("Create") + ); + + // Assert + result.IsValid.Should().BeTrue(); + result.Errors.Should().BeEmpty(); + } + + /* ------------------------------------------------------------------------- + * FirstName + * ---------------------------------------------------------------------- */ + + [Fact] + [Trait("Category", "Unit")] + public async Task ValidateAsync_FirstNameEmpty_ReturnsValidationError() + { + // Arrange + var request = PlayerFakes.MakeRequestModelForCreate(); + request.FirstName = string.Empty; + var validator = CreateValidator(); + + // Act + var result = await validator.ValidateAsync( + request, + options => options.IncludeRuleSets("Create") + ); + + // Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(error => error.PropertyName == "FirstName"); + } + + /* ------------------------------------------------------------------------- + * LastName + * ---------------------------------------------------------------------- */ + + [Fact] + [Trait("Category", "Unit")] + public async Task ValidateAsync_LastNameEmpty_ReturnsValidationError() + { + // Arrange + var request = PlayerFakes.MakeRequestModelForCreate(); + request.LastName = string.Empty; + var validator = CreateValidator(); + + // Act + var result = await validator.ValidateAsync( + request, + options => options.IncludeRuleSets("Create") + ); + + // Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(error => error.PropertyName == "LastName"); + } + + /* ------------------------------------------------------------------------- + * SquadNumber + * ---------------------------------------------------------------------- */ + + [Fact] + [Trait("Category", "Unit")] + public async Task ValidateAsync_SquadNumberNotGreaterThanZero_ReturnsValidationError() + { + // Arrange + var request = PlayerFakes.MakeRequestModelForCreate(); + request.SquadNumber = 0; + var validator = CreateValidator(); + + // Act + var result = await validator.ValidateAsync( + request, + options => options.IncludeRuleSets("Create") + ); + + // Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(error => error.PropertyName == "SquadNumber"); + } + + [Fact] + [Trait("Category", "Unit")] + public async Task ValidateAsync_SquadNumberNegative_ReturnsValidationError() + { + // Arrange + var request = PlayerFakes.MakeRequestModelForCreate(); + request.SquadNumber = -5; + var validator = CreateValidator(); + + // Act + var result = await validator.ValidateAsync( + request, + options => options.IncludeRuleSets("Create") + ); + + // Assert + result.IsValid.Should().BeFalse(); + result + .Errors.Should() + .Contain(error => + error.PropertyName == "SquadNumber" && error.ErrorMessage.Contains("greater than 0") + ); + } + + [Fact] + [Trait("Category", "Unit")] + public async Task ValidateAsync_SquadNumberNotUnique_ReturnsValidationError() + { + // Arrange + var request = PlayerFakes.MakeRequestModelForCreate(); + var existingPlayer = PlayerFakes.MakeNew(); + var repositoryMock = new Mock(); + repositoryMock + .Setup(repository => repository.FindBySquadNumberAsync(request.SquadNumber)) + .ReturnsAsync(existingPlayer); + var validator = CreateValidator(repositoryMock); + + // Act + var result = await validator.ValidateAsync( + request, + options => options.IncludeRuleSets("Create") + ); + + // Assert + result.IsValid.Should().BeFalse(); + result + .Errors.Should() + .Contain(error => + error.PropertyName == "SquadNumber" && error.ErrorMessage.Contains("unique") + ); + } + + [Fact] + [Trait("Category", "Unit")] + public async Task ValidateAsync_SquadNumberBelongsToPlayerBeingUpdated_ReturnsNoErrors() + { + // Arrange + // Simulate a PUT request for an existing player: the squad number in the + // body matches the one already in the database. The "Update" rule set must + // not run BeUniqueSquadNumber, otherwise this valid request would be rejected. + var request = PlayerFakes.MakeRequestModelForUpdate(10); + var existingPlayer = PlayerFakes.MakeFromStarting11(10); + var repositoryMock = new Mock(); + repositoryMock + .Setup(repository => repository.FindBySquadNumberAsync(request.SquadNumber)) + .ReturnsAsync(existingPlayer); + var validator = CreateValidator(repositoryMock); + + // Act + var result = await validator.ValidateAsync( + request, + options => options.IncludeRuleSets("Update") + ); + + // Assert + result.IsValid.Should().BeTrue(); + result.Errors.Should().BeEmpty(); + } + + [Fact] + [Trait("Category", "Unit")] + public async Task ValidateAsync_FirstNameEmptyInUpdateRuleSet_ReturnsValidationError() + { + // Arrange + var request = PlayerFakes.MakeRequestModelForUpdate(10); + request.FirstName = string.Empty; + var validator = CreateValidator(); + + // Act + var result = await validator.ValidateAsync( + request, + options => options.IncludeRuleSets("Update") + ); + + // Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(error => error.PropertyName == "FirstName"); + } + + /* ------------------------------------------------------------------------- + * AbbrPosition + * ---------------------------------------------------------------------- */ + + [Fact] + [Trait("Category", "Unit")] + public async Task ValidateAsync_AbbrPositionEmpty_ReturnsValidationError() + { + // Arrange + var request = PlayerFakes.MakeRequestModelForCreate(); + request.AbbrPosition = string.Empty; + var validator = CreateValidator(); + + // Act + var result = await validator.ValidateAsync( + request, + options => options.IncludeRuleSets("Create") + ); + + // Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(error => error.PropertyName == "AbbrPosition"); + } + + [Fact] + [Trait("Category", "Unit")] + public async Task ValidateAsync_AbbrPositionInvalid_ReturnsValidationError() + { + // Arrange + var request = PlayerFakes.MakeRequestModelForCreate(); + request.AbbrPosition = "INVALID"; + var validator = CreateValidator(); + + // Act + var result = await validator.ValidateAsync( + request, + options => options.IncludeRuleSets("Create") + ); + + // Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(error => error.PropertyName == "AbbrPosition"); + } + + /* ------------------------------------------------------------------------- + * DateOfBirth + * ---------------------------------------------------------------------- */ + + [Fact] + [Trait("Category", "Unit")] + public async Task ValidateAsync_DateOfBirthNull_ReturnsNoErrors() + { + // Arrange + var request = PlayerFakes.MakeRequestModelForCreate(); + request.DateOfBirth = null; + var repositoryMock = new Mock(); + repositoryMock + .Setup(repository => repository.FindBySquadNumberAsync(request.SquadNumber)) + .ReturnsAsync(null as Player); + var validator = CreateValidator(repositoryMock); + + // Act + var result = await validator.ValidateAsync( + request, + options => options.IncludeRuleSets("Create") + ); + + // Assert + result.IsValid.Should().BeTrue(); + } + + [Fact] + [Trait("Category", "Unit")] + public async Task ValidateAsync_DateOfBirthInFuture_ReturnsValidationError() + { + // Arrange + var request = PlayerFakes.MakeRequestModelForCreate(); + request.DateOfBirth = DateTime.UtcNow.AddYears(1); + var validator = CreateValidator(); + + // Act + var result = await validator.ValidateAsync( + request, + options => options.IncludeRuleSets("Create") + ); + + // Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(error => error.PropertyName == "DateOfBirth"); + } + + [Fact] + [Trait("Category", "Unit")] + public async Task ValidateAsync_DateOfBirthToday_ReturnsValidationError() + { + // Arrange + var fixedNow = new DateTimeOffset(2000, 1, 1, 0, 0, 0, TimeSpan.Zero); + var timeProvider = new FakeTimeProvider(fixedNow); + var request = PlayerFakes.MakeRequestModelForCreate(); + request.DateOfBirth = fixedNow.Date; // same "today" the validator sees + var validator = CreateValidator(timeProvider: timeProvider); + + // Act + var result = await validator.ValidateAsync( + request, + options => options.IncludeRuleSets("Create") + ); + + // Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(e => e.PropertyName == "DateOfBirth"); + } + + [Fact] + [Trait("Category", "Unit")] + public async Task ValidateAsync_DateOfBirthBeforeYear1900_ReturnsValidationError() + { + // Arrange + var request = PlayerFakes.MakeRequestModelForCreate(); + request.DateOfBirth = new DateTime(1899, 12, 31, 0, 0, 0, DateTimeKind.Utc); + var validator = CreateValidator(); + + // Act + var result = await validator.ValidateAsync( + request, + options => options.IncludeRuleSets("Create") + ); + + // Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(error => error.PropertyName == "DateOfBirth"); + } + + [Fact] + [Trait("Category", "Unit")] + public async Task ValidateAsync_DateOfBirthOnJanuary1st1900_ReturnsNoErrors() + { + // Arrange + var request = PlayerFakes.MakeRequestModelForCreate(); + request.DateOfBirth = new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc); // rule: >= 1900-01-01 + var repositoryMock = new Mock(); + repositoryMock + .Setup(r => r.FindBySquadNumberAsync(request.SquadNumber)) + .ReturnsAsync(null as Player); + var validator = CreateValidator(repositoryMock); + + // Act + var result = await validator.ValidateAsync( + request, + options => options.IncludeRuleSets("Create") + ); + + // Assert + result.IsValid.Should().BeTrue(); + result.Errors.Should().BeEmpty(); + } +} diff --git a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Usings.cs b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Usings.cs new file mode 100644 index 0000000..c802f44 --- /dev/null +++ b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; diff --git a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/DatabaseFakes.cs b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/DatabaseFakes.cs new file mode 100644 index 0000000..fc8bc05 --- /dev/null +++ b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/DatabaseFakes.cs @@ -0,0 +1,59 @@ +using System.Data.Common; +using Dotnet.Samples.AspNetCore.WebApi.Data; +using Dotnet.Samples.AspNetCore.WebApi.Migrations; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities +{ + /// + /// A Fake is a working implementation that's simpler than the real one. + /// It usually has some "real" logic but is not suitable for production + /// (e.g., an in‑memory database instead of a full SQL Server). Fakes are + /// useful when you need behavior that's closer to reality but still want + /// to avoid external dependencies. + /// + public static class DatabaseFakes + { + /// + /// Creates an in-memory SQLite connection and DbContext options for testing. + /// The connection remains open for the lifetime of the test. + /// + /// A tuple containing the SQLite connection and DbContext options. + public static (DbConnection, DbContextOptions) CreateSqliteConnection() + { + var dbConnection = new SqliteConnection("Filename=:memory:"); + dbConnection.Open(); + + var dbContextOptions = new DbContextOptionsBuilder() + .UseSqlite(dbConnection) + .ReplaceService() + .Options; + + return (dbConnection, dbContextOptions); + } + + /// + /// Creates a PlayerDbContext instance with the specified options. + /// + /// The DbContext options to use. + /// A new PlayerDbContext instance. + public static PlayerDbContext CreateDbContext( + DbContextOptions dbContextOptions + ) + { + return new PlayerDbContext(dbContextOptions); + } + + /// + /// Applies all pending EF Core migrations to the test database, including seed data. + /// Extension method for PlayerDbContext. + /// + /// The PlayerDbContext instance. + public static async Task MigrateAsync(this PlayerDbContext context) + { + await context.Database.MigrateAsync(); + } + } +} diff --git a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerFakes.cs b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerFakes.cs new file mode 100644 index 0000000..2c80a3a --- /dev/null +++ b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerFakes.cs @@ -0,0 +1,228 @@ +using Dotnet.Samples.AspNetCore.WebApi.Models; +using Dotnet.Samples.AspNetCore.WebApi.Utilities; + +namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities; + +/// +/// Test data factory for Player entities. +/// Wraps production data from PlayerData with test-specific modifications (e.g., GUID assignment). +/// A Fake is a working implementation that's simpler than the real one. +/// It usually has some "real" logic but is not suitable for production +/// (e.g., an in‑memory database instead of a full SQL Server). Fakes are +/// useful when you need behavior that's closer to reality but still want +/// to avoid external dependencies. +/// +/// +/// Data-state vocabulary used across controller tests: +/// +/// existingPlayer is present in the database. +/// nonexistentPlayer is absent, valid shape for creation (POST scenarios). +/// unknownValid ID format, absent from database (404-by-lookup scenarios). +/// +/// +public static class PlayerFakes +{ + private static string? FormatBirth(DateTime? dateOfBirth) => + dateOfBirth.HasValue ? $"{dateOfBirth.Value:MMMM d, yyyy}" : null; + + /// + /// Returns the starting 11 players with generated GUIDs for in-memory testing. + /// Reuses production player data from PlayerData.MakeStarting11(). + /// + public static List MakeStarting11() + { + var players = PlayerData.MakeStarting11(); + + // Assign GUIDs for in-memory database testing + foreach (var player in players) + { + player.Id = Guid.NewGuid(); + } + + return players; + } + + /// + /// Returns a specific player from the starting 11 by squad number. + /// Reuses production player data from PlayerData.MakeStarting11(). + /// + public static Player MakeFromStarting11(int squadNumber) + { + var player = + PlayerData.MakeStarting11().SingleOrDefault(p => p.SquadNumber == squadNumber) + ?? throw new ArgumentNullException( + $"Player with Squad Number {squadNumber} not found." + ); + + player.Id = Guid.NewGuid(); + return player; + } + + /// + /// Returns a new player (substitute) for testing create operations. + /// Reuses production player data from PlayerData.GetSubstitutes(). + /// + public static Player MakeNew() + { + // Get Giovani Lo Celso (squad number 27) from substitutes + var player = + PlayerData.GetSubstitutes().SingleOrDefault(player => player.SquadNumber == 27) + ?? throw new InvalidOperationException( + "Substitute player with squad number 27 not found." + ); + + player.Id = Guid.NewGuid(); + return player; + } + + /* ------------------------------------------------------------------------- + * Create + * ---------------------------------------------------------------------- */ + + /// + /// Creates a PlayerRequestModel for testing create operations. + /// Uses data from a substitute player (Giovani Lo Celso). + /// + public static PlayerRequestModel MakeRequestModelForCreate() + { + var player = MakeNew(); + + return new PlayerRequestModel() + { + FirstName = player.FirstName, + MiddleName = player.MiddleName, + LastName = player.LastName, + DateOfBirth = player.DateOfBirth, + SquadNumber = player.SquadNumber, + AbbrPosition = player.AbbrPosition, + Team = player.Team, + League = player.League, + }; + } + + /// + /// Creates a PlayerResponseModel for testing create operation responses. + /// Uses data from a substitute player (Giovani Lo Celso). + /// + public static PlayerResponseModel MakeResponseModelForCreate() + { + var player = MakeNew(); + + return new PlayerResponseModel + { + FullName = + $"{player.FirstName} {(string.IsNullOrWhiteSpace(player.MiddleName) ? "" : player.MiddleName + " ")}{player.LastName}".Trim(), + Birth = FormatBirth(player.DateOfBirth), + Dorsal = player.SquadNumber, + Position = player.Position, + Club = player.Team, + League = player.League, + Starting11 = player.Starting11 ? "Yes" : "No", + }; + } + + /* ------------------------------------------------------------------------- + * Retrieve + * ---------------------------------------------------------------------- */ + + /// + /// Creates a PlayerRequestModel for testing retrieve operations. + /// Uses data from the starting 11 based on the specified squad number. + /// + /// The squad number of the player to retrieve. + public static PlayerRequestModel MakeRequestModelForRetrieve(int squadNumber) + { + var player = + PlayerData.MakeStarting11().SingleOrDefault(p => p.SquadNumber == squadNumber) + ?? throw new ArgumentNullException( + $"Player with Squad Number {squadNumber} not found." + ); + + return new PlayerRequestModel + { + FirstName = player.FirstName, + MiddleName = player.MiddleName, + LastName = player.LastName, + DateOfBirth = player.DateOfBirth, + SquadNumber = player.SquadNumber, + AbbrPosition = player.AbbrPosition, + Team = player.Team, + League = player.League, + }; + } + + /// + /// Creates a PlayerResponseModel for testing retrieve operation responses. + /// Uses data from the starting 11 based on the specified squad number. + /// + /// The squad number of the player to retrieve. + public static PlayerResponseModel MakeResponseModelForRetrieve(int squadNumber) + { + var player = + PlayerData.MakeStarting11().SingleOrDefault(p => p.SquadNumber == squadNumber) + ?? throw new ArgumentNullException( + $"Player with Squad Number {squadNumber} not found." + ); + + return new PlayerResponseModel + { + FullName = + $"{player.FirstName} {(string.IsNullOrWhiteSpace(player.MiddleName) ? "" : player.MiddleName + " ")}{player.LastName}".Trim(), + Birth = FormatBirth(player.DateOfBirth), + Dorsal = player.SquadNumber, + Position = player.Position, + Club = player.Team, + League = player.League, + Starting11 = player.Starting11 ? "Yes" : "No", + }; + } + + /// + /// Creates a list of PlayerResponseModel for testing retrieve all operations. + /// Uses all players from the starting 11. + /// + public static List MakeResponseModelsForRetrieve() => + [ + .. PlayerData + .MakeStarting11() + .Select(player => + { + player.Id = Guid.NewGuid(); + return new PlayerResponseModel + { + FullName = + $"{player.FirstName} {(string.IsNullOrWhiteSpace(player.MiddleName) ? "" : player.MiddleName + " ")}{player.LastName}".Trim(), + Birth = FormatBirth(player.DateOfBirth), + Dorsal = player.SquadNumber, + Position = player.Position, + Club = player.Team, + League = player.League, + Starting11 = player.Starting11 ? "Yes" : "No", + }; + }), + ]; + + /* ------------------------------------------------------------------------- + * Update + * ---------------------------------------------------------------------- */ + + /// + /// Creates a PlayerRequestModel for testing update operations. + /// Delegates to MakeRequestModelForRetrieve. + /// + /// The squad number of the player to update. + public static PlayerRequestModel MakeRequestModelForUpdate(int squadNumber) + { + return MakeRequestModelForRetrieve(squadNumber); + } + + /// + /// Creates a PlayerResponseModel for testing update operation responses. + /// Delegates to MakeResponseModelForRetrieve. + /// + /// The squad number of the player to update. + public static PlayerResponseModel MakeResponseModelForUpdate(int squadNumber) + { + return MakeResponseModelForRetrieve(squadNumber); + } +} diff --git a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerMocks.cs b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerMocks.cs new file mode 100644 index 0000000..4b52fde --- /dev/null +++ b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerMocks.cs @@ -0,0 +1,113 @@ +using AutoMapper; +using Dotnet.Samples.AspNetCore.WebApi.Controllers; +using Dotnet.Samples.AspNetCore.WebApi.Models; +using Dotnet.Samples.AspNetCore.WebApi.Repositories; +using Dotnet.Samples.AspNetCore.WebApi.Services; +using FluentValidation; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Moq; + +namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities +{ + /// + /// A Mock is a dynamic test double that not only provides responses but + /// also records interactions. Mocks are pre‑programmed with expectations + /// (like “this method should be called once with these parameters”) and can + /// later verify that the expected calls occurred. Frameworks such as Moq + /// make it easy to set up and verify these interactions. + /// + public static class PlayerMocks + { + /// + /// Initializes mocks for PlayerController dependencies. + /// + /// A tuple containing mocked service, logger, and validator. + public static ( + Mock service, + Mock> logger, + Mock> validator + ) InitControllerMocks() + { + var service = new Mock(); + var logger = new Mock>(); + var validator = new Mock>(); + + return (service, logger, validator); + } + + /// + /// Creates a mock IUrlHelper configured to return any string for Action calls. + /// + /// A configured Mock of IUrlHelper. + public static Mock SetupUrlHelperMock() + { + var mock = new Mock(); + mock.Setup(url => url.Action(It.IsAny())).Returns(It.IsAny()); + + return mock; + } + + /// + /// Initializes mocks for PlayerService dependencies. + /// + /// Optional cache value to return from memory cache. + /// The environment name (defaults to "Development"). + /// A tuple containing mocked repository, logger, cache, mapper, and environment. + public static ( + Mock repository, + Mock> logger, + Mock memoryCache, + Mock mapper, + Mock environment + ) InitServiceMocks(object? cacheValue = null, string environmentName = "Development") + { + var repository = new Mock(); + var logger = new Mock>(); + var memoryCache = SetupMemoryCacheMock(cacheValue ?? It.IsAny()); + var mapper = new Mock(); + var environment = new Mock(); + environment.Setup(env => env.EnvironmentName).Returns(environmentName); + return (repository, logger, memoryCache, mapper, environment); + } + + /// + /// Creates a mock IMemoryCache with configurable behavior. + /// First TryGetValue call returns false, subsequent calls return true with the specified value. + /// + /// The value to return from cache after the first call. + /// A configured Mock of IMemoryCache. + public static Mock SetupMemoryCacheMock(object? value) + { + var cachedValue = false; + var mock = new Mock(); + + mock.Setup(cache => cache.TryGetValue(It.IsAny(), out It.Ref.IsAny!)) + .Callback( + new TryGetValueDelegate( + (object _, out object? output) => + { + output = cachedValue ? value : null; + } + ) + ) + .Returns(() => + { + bool hasValue = cachedValue; + cachedValue = true; // Subsequent invocations will return true + return hasValue; + }); + + mock.Setup(cache => cache.CreateEntry(It.IsAny())) + .Returns(Mock.Of); + mock.Setup(cache => cache.Remove(It.IsAny())); + + return mock; + } + + private delegate void TryGetValueDelegate(object key, out object? value); + } +} diff --git a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerStubs.cs b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerStubs.cs new file mode 100644 index 0000000..18bdb4d --- /dev/null +++ b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerStubs.cs @@ -0,0 +1,31 @@ +using System.Data.Common; +using Dotnet.Samples.AspNetCore.WebApi.Data; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; + +namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities +{ + /// + /// A Stub provides pre‑configured, hard‑coded responses to method calls. + /// Its purpose is simply to supply data to the system under test without + /// any behavior verification. For example, a Stub for a repository might + /// always return a specific user regardless of the input. + /// + public static class PlayerStubs + { + /// + /// Creates a ModelStateDictionary with a single validation error. + /// Used for testing validation failure scenarios. + /// + /// The property name or key for the error. + /// The error message. + /// A ModelStateDictionary containing the specified error. + public static ModelStateDictionary CreateModelError(string key, string errorMessage) + { + var modelStateDictionary = new ModelStateDictionary(); + modelStateDictionary.AddModelError(key, errorMessage); + return modelStateDictionary; + } + } +} diff --git a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/TestAuthHandler.cs b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/TestAuthHandler.cs new file mode 100644 index 0000000..87d06ec --- /dev/null +++ b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/TestAuthHandler.cs @@ -0,0 +1,30 @@ +using System.Security.Claims; +using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities; + +/// +/// A test authentication handler that automatically authenticates every request, +/// bypassing the real authentication scheme. Used in integration tests to exercise +/// endpoints protected with [Authorize] without requiring a real token. +/// +internal sealed class TestAuthHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder +) : AuthenticationHandler(options, logger, encoder) +{ + public const string SchemeName = "Test"; + + protected override Task HandleAuthenticateAsync() + { + var claims = new[] { new Claim(ClaimTypes.Name, "TestUser") }; + var identity = new ClaimsIdentity(claims, SchemeName); + var principal = new ClaimsPrincipal(identity); + var ticket = new AuthenticationTicket(principal, SchemeName); + return Task.FromResult(AuthenticateResult.Success(ticket)); + } +} diff --git a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/packages.lock.json b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/packages.lock.json new file mode 100644 index 0000000..b557528 --- /dev/null +++ b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/packages.lock.json @@ -0,0 +1,767 @@ +{ + "version": 1, + "dependencies": { + "net10.0": { + "coverlet.collector": { + "type": "Direct", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "27jXSV/0DbVqF5jDrAxuQFZ9oaz6gmG03p8ttxAFk+X0M4woFYj7MoWDLCna5EGLb0CE6OE7X6ZH3Wt5smTtaA==" + }, + "FluentAssertions": { + "type": "Direct", + "requested": "[8.10.0, )", + "resolved": "8.10.0", + "contentHash": "yGvv+xp0gHFgGwhQcGoDdKIrhLhPi3zYYWWLLHPMctr+G6PWD7QH8L4wqoa5WRZquPbfSV/cP1MraI7ppAHrWw==" + }, + "Microsoft.AspNetCore.Mvc.Testing": { + "type": "Direct", + "requested": "[10.0.9, )", + "resolved": "10.0.9", + "contentHash": "Mt+5CtYz+xmog1S1TJt2owVKU8YquZtNy9bmO+kfrrtjEDZPzCw1qZ7o97PLpIEpt3yy4F5YdAUh9nKPm0CX5Q==", + "dependencies": { + "Microsoft.AspNetCore.TestHost": "10.0.9", + "Microsoft.Extensions.DependencyModel": "10.0.9", + "Microsoft.Extensions.Hosting": "10.0.9" + } + }, + "Microsoft.NET.Test.Sdk": { + "type": "Direct", + "requested": "[18.7.0, )", + "resolved": "18.7.0", + "contentHash": "49xH9j4UzCh2hMohJp53g3wUTvyycECw7CtVht4gfCz5ykudB1uBcF6D0TtgJPjCtP76UPW53bQElKdCeX+dUg==", + "dependencies": { + "Microsoft.CodeCoverage": "18.7.0", + "Microsoft.TestPlatform.TestHost": "18.7.0" + } + }, + "Moq": { + "type": "Direct", + "requested": "[4.20.72, )", + "resolved": "4.20.72", + "contentHash": "EA55cjyNn8eTNWrgrdZJH5QLFp2L43oxl1tlkoYUKIE9pRwL784OWiTXeCV5ApS+AMYEAlt7Fo03A2XfouvHmQ==", + "dependencies": { + "Castle.Core": "5.1.1" + } + }, + "xunit": { + "type": "Direct", + "requested": "[2.9.3, )", + "resolved": "2.9.3", + "contentHash": "TlXQBinK35LpOPKHAqbLY4xlEen9TBafjs0V5KnA4wZsoQLQJiirCR4CbIXvOH8NzkW4YeJKP5P/Bnrodm0h9Q==", + "dependencies": { + "xunit.analyzers": "1.18.0", + "xunit.assert": "2.9.3", + "xunit.core": "[2.9.3]" + } + }, + "xunit.runner.visualstudio": { + "type": "Direct", + "requested": "[3.1.5, )", + "resolved": "3.1.5", + "contentHash": "tKi7dSTwP4m5m9eXPM2Ime4Kn7xNf4x4zT9sdLO/G4hZVnQCRiMTWoSZqI/pYTVeI27oPPqHBKYI/DjJ9GsYgA==" + }, + "AutoMapper": { + "type": "Transitive", + "resolved": "16.1.1", + "contentHash": "VNEky8JA15ci+oIDRGHITOGOpV4dILsf8pnn24QhDl2urtqgJ2IXiS/V2EtGU17P/+f6OeFQPJETaZXV9QOIZg==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "10.0.0", + "Microsoft.Extensions.Options": "10.0.0", + "Microsoft.IdentityModel.JsonWebTokens": "8.14.0" + } + }, + "Castle.Core": { + "type": "Transitive", + "resolved": "5.1.1", + "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==", + "dependencies": { + "System.Diagnostics.EventLog": "6.0.0" + } + }, + "FluentValidation": { + "type": "Transitive", + "resolved": "12.1.1", + "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" + }, + "FluentValidation.DependencyInjectionExtensions": { + "type": "Transitive", + "resolved": "12.1.1", + "contentHash": "D0VXh4dtjjX2aQizuaa0g6R8X3U1JaVqJPfGCvLwZX9t/O2h7tkpbitbadQMfwcgSPdDbI2vDxuwRMv/Uf9dHA==", + "dependencies": { + "FluentValidation": "12.1.1", + "Microsoft.Extensions.DependencyInjection.Abstractions": "2.1.0" + } + }, + "Microsoft.AspNetCore.OpenApi": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "1ihb8FO9cGgEK1/m3CTtT/SfnynwmiZib0W2pcDVj3KSWk/Sca4VOXEtaptKQc582zpFrzTFiwkGRCglt6H+WQ==", + "dependencies": { + "Microsoft.OpenApi": "2.0.0" + } + }, + "Microsoft.AspNetCore.TestHost": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "mR4y30XFsVbn7EwV3Ic5/KMBO5QGwukTJE2ztGmpAST5ACX+Z+9r+Y6D1eibJojsOIW5KHpM4myo9/aRALJOyg==" + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "18.7.0", + "contentHash": "+wFfx9s7D9wegM0RziXMj2kvYDT4qcqXXtyjiQwSZOGQ2wwcOAJQcD6eQXk02jt0MvRNawtp8TJxTrV+wD8X1g==" + }, + "Microsoft.Data.Sqlite.Core": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "iZrONyMKPjxfVZnUktqO30QjzNwAGH+AxM61s8lKQnVhgbQ3bn0hiXI129ZmVicEbIcwljyy2OVsIYUR51ZHKQ==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.11" + } + }, + "Microsoft.EntityFrameworkCore": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "tu85SRzOT021V7EQlViCiAE7TqldVn469Y6lt5TEn/+XC4/MeNCHgMRSxqYuWqvF4zAQZUhCmtNEZuM3ss4LeA==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Abstractions": "10.0.9", + "Microsoft.EntityFrameworkCore.Analyzers": "10.0.9", + "Microsoft.Extensions.Caching.Memory": "10.0.9", + "Microsoft.Extensions.Logging": "10.0.9" + } + }, + "Microsoft.EntityFrameworkCore.Abstractions": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "GRMaiPkqYna/gCsyDffYDWmefGPC3hDrdMw+2rrGcQwhs6uZOsaMQXMJnoXQ35tx9SkBV2ieRRU9N/jLOO6BZw==" + }, + "Microsoft.EntityFrameworkCore.Analyzers": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "aiEFB+C5EsZGqxvMPazE07hbWsp4iPaufJpanGt5O+lrwv7mJLrqma5haVIgFAPCyhQkmk75XSCEubT1zUjxtA==" + }, + "Microsoft.EntityFrameworkCore.Relational": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "dEoyYKgiaZHHgOFm1WMWm1sFEsEuhPWufX4L9PekKtqd/RaIcPjkCjvbrVvJtApErb5wPSJhYvnTlxhH+p9h2g==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "10.0.9", + "Microsoft.Extensions.Caching.Memory": "10.0.9", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.9", + "Microsoft.Extensions.Logging": "10.0.9" + } + }, + "Microsoft.EntityFrameworkCore.Sqlite": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "HH9/nnRF/YmRrc3hUlgXjMBYKH5kFmd5UWC81l9U0ySQhwHTcgvDPSewB8DyQHzFJzNGgG7VFK6ynC6+XQz9WA==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.9", + "Microsoft.Extensions.Caching.Memory": "10.0.9", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.9", + "Microsoft.Extensions.DependencyModel": "10.0.9", + "Microsoft.Extensions.Logging": "10.0.9", + "SQLitePCLRaw.bundle_e_sqlite3": "2.1.11", + "SQLitePCLRaw.core": "2.1.11" + } + }, + "Microsoft.EntityFrameworkCore.Sqlite.Core": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "3bPEmACAaPJJSw+m5XwTSi3yZnVtaifa4d8gLsNMzW0Qu28jS5kADSfgJRBlq49RJ1K098VCzEDRJwM8gE6f2w==", + "dependencies": { + "Microsoft.Data.Sqlite.Core": "10.0.9", + "Microsoft.EntityFrameworkCore.Relational": "10.0.9", + "Microsoft.Extensions.Caching.Memory": "10.0.9", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.9", + "Microsoft.Extensions.DependencyModel": "10.0.9", + "Microsoft.Extensions.Logging": "10.0.9", + "SQLitePCLRaw.core": "2.1.11" + } + }, + "Microsoft.Extensions.ApiDescription.Server": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "NCWCGiwRwje8773yzPQhvucYnnfeR+ZoB1VRIrIMp4uaeUNw7jvEPHij3HIbwCDuNCrNcphA00KSAR9yD9qmbg==" + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "5fGxcw2vuYp8s0wio9H1ECiuk4iKSdTIlNuigdLIrkhg+5XAwgFVDB/5Ots3pfN/QhABLYXutA79JFtnUKDSHA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.9" + } + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "G9mregdatGWMCQWeCw012LDeJVP7G/XIxH8Ddbjc8bD1//dA+8VVQdcRE9jI1moyoJxSSZhHITUnNQ8FUDl5+Q==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "10.0.9", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.9", + "Microsoft.Extensions.Logging.Abstractions": "10.0.9", + "Microsoft.Extensions.Options": "10.0.9", + "Microsoft.Extensions.Primitives": "10.0.9" + } + }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "woZsWLhOQsASuxbmgiZJqiGUBNo3IjRdXC92xt8rRokza+P6/nIsnzq7sm9Or6ZYcRl2kL1ufj8HVzp1QlPTXw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.9", + "Microsoft.Extensions.Primitives": "10.0.9" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "qGhRPd3VxfLV9UqatVOiD9mAeUbj2KiMwGFYC5uXlzExiZQoe4X/hdmzGIU7BQjNLTqCnnbTHVyBglG3668/HA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.9" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "Tp/+LPb70RyjjtLg9m5C959eP4KrUpJHThZfAegZVpsfmGvzfuNkuYbI/ft+LvXhMSyUcAeOPaN6rzTccwnZAg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.9", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.9" + } + }, + "Microsoft.Extensions.Configuration.CommandLine": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "8D4HaqxWdm5M/nuhQffjPoR1ekhlpyKTXjFMAT5KlP0dvxkJe5JLAP6MAsuUEUxKWG09Bi5aAUaYMFKrMqWHqA==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.9", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.9" + } + }, + "Microsoft.Extensions.Configuration.EnvironmentVariables": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "JhKySWIL8+N4yFt4HPm1rGKCHooze+MBdTdpXc0bd/PGm31TrSUi2m0Nek1y441Wlv/RE6VH0W/DCv2xnmy8FA==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.9", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.9" + } + }, + "Microsoft.Extensions.Configuration.FileExtensions": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "NgLB9cYnIb0/djSDcnqo4GIGGWooxGmr/gCUe3/CRXcKqLizOFui8MyW4EVkTB/KNJL+oXdMXnD6ZRm3Y+qkrQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.9", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.9", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.9", + "Microsoft.Extensions.FileProviders.Physical": "10.0.9", + "Microsoft.Extensions.Primitives": "10.0.9" + } + }, + "Microsoft.Extensions.Configuration.Json": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "LiFKJgc9jZEW+7RhcSfsvCwoikt1lDdOqOn+whZC5zVHyg/gExftHl2QPtmfiHsEdDNg+Y+BDr6835tOfj8Y7A==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.9", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.9", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.9", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.9" + } + }, + "Microsoft.Extensions.Configuration.UserSecrets": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "ockJRreRW/HbGwoyHzYOxMucFBimvAZ8lKNwQLMHrS6mwkDUaCJMWzzeE+Rm9vgFlv2o/xqk8fm+FpqrDCnkTA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.9", + "Microsoft.Extensions.Configuration.Json": "10.0.9", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.9", + "Microsoft.Extensions.FileProviders.Physical": "10.0.9" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "NijozhERJDIaJ4k5TSMy1jOi0cSC2HfkvRD/Sl+kGSSKgVbFnF4GxgtMN/MrzHB8D1JxIrD4xSer9Blh9v3axQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.9" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "g41l/30G3K4B/d/L8kjux0+30e27c8D0FVQ/PFCpbekgfDpj9mnDhieP67EqXWvl1EWNeZh2rpR4F5B/jcDOHA==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "SCDTQ6HubnRvTUjR7dgMKHZvNoCb03t44ttHL8trlFTGgfDteWn/0nRdOxDhcI+lTWhKgd/flCVJEtAOPhSLNg==" + }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "NLXI3PbTe39q6/sgs7JYhmfPf7bMzReUoAJ0q9Po6yhfM+0anZa7PrEva4W2SdiLWGyB9eKZS9THGt2BP40xJg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.9", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.9", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.9" + } + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "86RgyFsmVslW4Nu28IXgt8tLglynGQrwjk/xhGZaTe8j6YIeR1Ywoc42hSHsBSl920CQdfqq2dBohZiGm3AkUA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.9", + "Microsoft.Extensions.Options": "10.0.9" + } + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "Oxn4vqDk+EwceTMpZxVm7L/UZEAM1qIQlNP1+7tBZckD+P4SKrm/5X4gMTPCTdpnau/xY8Sb4/0d6onomSg4ZA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.9" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "zm8WVod4swgprGrkxkuSILlbXqdDRqF+3y6U0I7jlmj4PMyKN6d8pzXZHUn5lr/gZVULzk/+FeTYlTupt6akpg==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.9", + "Microsoft.Extensions.FileSystemGlobbing": "10.0.9", + "Microsoft.Extensions.Primitives": "10.0.9" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "mvRf9qOH/LslWIee/h+lsElnoUyKotEwoPL31soqScmO/eoxObaTCLCdx2DdqPdRi9LnB+7qKZ49jfyrLZuc+w==" + }, + "Microsoft.Extensions.Hosting": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "HTgnvmK0ubesUFO16pLC+i9+RS8lEGd6TmDouuy75FsAgIFrSwUVhYCqG2IENzBJwgxGc/6Rsulfsvd9ZG/XkA==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.9", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.9", + "Microsoft.Extensions.Configuration.Binder": "10.0.9", + "Microsoft.Extensions.Configuration.CommandLine": "10.0.9", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "10.0.9", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.9", + "Microsoft.Extensions.Configuration.Json": "10.0.9", + "Microsoft.Extensions.Configuration.UserSecrets": "10.0.9", + "Microsoft.Extensions.DependencyInjection": "10.0.9", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.9", + "Microsoft.Extensions.Diagnostics": "10.0.9", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.9", + "Microsoft.Extensions.FileProviders.Physical": "10.0.9", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.9", + "Microsoft.Extensions.Logging": "10.0.9", + "Microsoft.Extensions.Logging.Abstractions": "10.0.9", + "Microsoft.Extensions.Logging.Configuration": "10.0.9", + "Microsoft.Extensions.Logging.Console": "10.0.9", + "Microsoft.Extensions.Logging.Debug": "10.0.9", + "Microsoft.Extensions.Logging.EventLog": "10.0.9", + "Microsoft.Extensions.Logging.EventSource": "10.0.9", + "Microsoft.Extensions.Options": "10.0.9" + } + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "Xd/2F+uWblTiUp+ssaDZN2ea4vmnHmW6PXugmqBHumyhqVkyeh6RJ3S2Zo/F+1bXIL/KuGqe2pKv6UiGOc1KeQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.9", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.9", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.9", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.9", + "Microsoft.Extensions.Logging.Abstractions": "10.0.9" + } + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "N7Gm9SjugYjmmnhwbBKC9DFqGqjfJvh6YfOJgtwh0AW0Xpok3dIVors1ik050XmUxKAgAc7nNngDIJyFb06K2g==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "10.0.9", + "Microsoft.Extensions.Logging.Abstractions": "10.0.9", + "Microsoft.Extensions.Options": "10.0.9" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "9S/DFt4cohlMPpzIxjG6kk0L8MuN2vDm9pbMCulxtJzzk82oJHVLBd8vuQxaPskaYQwKqmFmbannf5eoChgjYg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.9" + } + }, + "Microsoft.Extensions.Logging.Configuration": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "bUth5ip7YsZMXWZS42IRTI0zDrPEqdE+xnsmcL0Pk784grWKApDvc5UoMi2tP2qYJ5ylFzeVDuDu08sFATq1bg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.9", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.9", + "Microsoft.Extensions.Configuration.Binder": "10.0.9", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.9", + "Microsoft.Extensions.Logging": "10.0.9", + "Microsoft.Extensions.Logging.Abstractions": "10.0.9", + "Microsoft.Extensions.Options": "10.0.9", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.9" + } + }, + "Microsoft.Extensions.Logging.Console": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "WyZEG/O8jKqBOBF6/M6IJqiEyWFBUv6PDyzNoXDA0mBZwKtkuf7GiZ/0/8eU8OpLKKQL0O95oPOY1szrWIKofQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.9", + "Microsoft.Extensions.Logging": "10.0.9", + "Microsoft.Extensions.Logging.Abstractions": "10.0.9", + "Microsoft.Extensions.Logging.Configuration": "10.0.9", + "Microsoft.Extensions.Options": "10.0.9" + } + }, + "Microsoft.Extensions.Logging.Debug": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "r/A0ahpXmZH/8ltPjrFFWp12BIizK9cCVJXPcHyOad8e4eIX7P/geW+uBYdczkeCAaMebT4jEU7snOm4GnmKfA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.9", + "Microsoft.Extensions.Logging": "10.0.9", + "Microsoft.Extensions.Logging.Abstractions": "10.0.9" + } + }, + "Microsoft.Extensions.Logging.EventLog": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "goAl30/WwmdnWDPRwATaDPIK0iuDBnQSMTH2XYGVB1SwReg7hglhvDNjjpNhT25US3GF4I5q6BhTNs6nFYzEfg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.9", + "Microsoft.Extensions.Logging": "10.0.9", + "Microsoft.Extensions.Logging.Abstractions": "10.0.9", + "Microsoft.Extensions.Options": "10.0.9", + "System.Diagnostics.EventLog": "10.0.9" + } + }, + "Microsoft.Extensions.Logging.EventSource": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "tHynPVHbTicuaDpS2JVTxX0qA5VTg15CXgVKTwWvvudb5BvW5aVew8MMyek6LrDGAom7UbON1jf1T5GhpTilFA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.9", + "Microsoft.Extensions.Logging": "10.0.9", + "Microsoft.Extensions.Logging.Abstractions": "10.0.9", + "Microsoft.Extensions.Options": "10.0.9", + "Microsoft.Extensions.Primitives": "10.0.9" + } + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "hyNdX4c2UwkRkzb9byw0H2DQkRzwBM3mzY2sCM9egwzTyg8dvQJmp5noQHGEaaCORQrNK3DD2gREBsc2DlXS4A==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.9", + "Microsoft.Extensions.Primitives": "10.0.9" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "Y4E24zffF/aPS0igNvY6ZzAQfbxd6AYdC9L4brnH+uK0yYYHIR6FeGVQVVjAOo8wub1EQDl2B90lCcpqoTF7Yw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.9", + "Microsoft.Extensions.Configuration.Binder": "10.0.9", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.9", + "Microsoft.Extensions.Options": "10.0.9", + "Microsoft.Extensions.Primitives": "10.0.9" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "fmEbAUFsaIKirgLt/lYhuFRBwhcSJN31jjHgCdbQxJiWOum6EdLjkbgGuukSP9z/a+9LibaxII/kF+GwOXgC4g==" + }, + "Microsoft.IdentityModel.Abstractions": { + "type": "Transitive", + "resolved": "8.14.0", + "contentHash": "iwbCpSjD3ehfTwBhtSNEtKPK0ICun6ov7Ibx6ISNA9bfwIyzI2Siwyi9eJFCJBwxowK9xcA1mj+jBWiigeqgcQ==" + }, + "Microsoft.IdentityModel.JsonWebTokens": { + "type": "Transitive", + "resolved": "8.14.0", + "contentHash": "4jOpiA4THdtpLyMdAb24dtj7+6GmvhOhxf5XHLYWmPKF8ApEnApal1UnJsKO4HxUWRXDA6C4WQVfYyqsRhpNpQ==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.14.0" + } + }, + "Microsoft.IdentityModel.Logging": { + "type": "Transitive", + "resolved": "8.14.0", + "contentHash": "eqqnemdW38CKZEHS6diA50BV94QICozDZEvSrsvN3SJXUFwVB9gy+/oz76gldP7nZliA16IglXjXTCTdmU/Ejg==", + "dependencies": { + "Microsoft.IdentityModel.Abstractions": "8.14.0" + } + }, + "Microsoft.IdentityModel.Tokens": { + "type": "Transitive", + "resolved": "8.14.0", + "contentHash": "lKIZiBiGd36k02TCdMHp1KlNWisyIvQxcYJvIkz7P4gSQ9zi8dgh6S5Grj8NNG7HWYIPfQymGyoZ6JB5d1Lo1g==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.IdentityModel.Logging": "8.14.0" + } + }, + "Microsoft.OpenApi": { + "type": "Transitive", + "resolved": "2.7.5", + "contentHash": "0FA67RSnRM4tcBKqiqVu/HPdZ9+QOKbmeRjxRUGTCjPU4C0bmUhd97Dso7Yild5P7nOV6GxJ2xrK0Kv/O9xp0w==" + }, + "Microsoft.TestPlatform.ObjectModel": { + "type": "Transitive", + "resolved": "18.7.0", + "contentHash": "6rmgU4q3/WOpOPcncI0YW0Q/QpcQtwR2TTEXDR5+4TfSimPBAk6Z/BgKLeGgp1SOun0ROVUCCafXhRLwsHaPpA==" + }, + "Microsoft.TestPlatform.TestHost": { + "type": "Transitive", + "resolved": "18.7.0", + "contentHash": "kYwfmebCs8992zaxEDkvG7S+YEouTeKfYVKUFEkwh1W2dIoOaevBt80XSKVXCUFEhusjOIm1sFfHBnoJgygrRA==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "18.7.0", + "Newtonsoft.Json": "13.0.3" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "Npgsql": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "7nb5YzXuvWWJxB0J8DiyL3we+X4FOctZrt0fIBnucOIaIevFEEwGQVZKtiu9olXdlNAK1eNgqSral6r/jlhI4w==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "10.0.0" + } + }, + "Npgsql.EntityFrameworkCore.PostgreSQL": { + "type": "Transitive", + "resolved": "10.0.2", + "contentHash": "PsNYgPOSW41Xx19gin7y4EdZAPteWr9Cb01XkdObxOsPzi+mgBupBEN7J7+erXFsROPOILM7MlIoO9QzL8+LGQ==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[10.0.4, 11.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.4, 11.0.0)", + "Npgsql": "10.0.3" + } + }, + "Serilog": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "+cDryFR0GRhsGOnZSKwaDzRRl4MupvJ42FhCE4zhQRVanX0Jpg6WuCBk59OVhVDPmab1bB+nRykAnykYELA9qQ==" + }, + "Serilog.AspNetCore": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "a/cNa1mY4On1oJlfGG1wAvxjp5g7OEzk/Jf/nm7NF9cWoE7KlZw1GldrifUBWm9oKibHkR7Lg/l5jy3y7ACR8w==", + "dependencies": { + "Serilog": "4.3.0", + "Serilog.Extensions.Hosting": "10.0.0", + "Serilog.Formatting.Compact": "3.0.0", + "Serilog.Settings.Configuration": "10.0.0", + "Serilog.Sinks.Console": "6.1.1", + "Serilog.Sinks.Debug": "3.0.0", + "Serilog.Sinks.File": "7.0.0" + } + }, + "Serilog.Extensions.Hosting": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "E7juuIc+gzoGxgzFooFgAV8g9BfiSXNKsUok9NmEpyAXg2odkcPsMa/Yo4axkJRlh0se7mkYQ1GXDaBemR+b6w==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", + "Microsoft.Extensions.Logging.Abstractions": "10.0.0", + "Serilog": "4.3.0", + "Serilog.Extensions.Logging": "10.0.0" + } + }, + "Serilog.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "vx0kABKl2dWbBhhqAfTOk53/i8aV/5VaT3a6il9gn72Wqs2pM7EK2OB6No6xdqK2IaY6Zf9gdjLuK9BVa2rT+Q==", + "dependencies": { + "Microsoft.Extensions.Logging": "10.0.0", + "Serilog": "4.2.0" + } + }, + "Serilog.Formatting.Compact": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Settings.Configuration": { + "type": "Transitive", + "resolved": "10.0.1", + "contentHash": "ayFE7h66mqMqzwfPrzDCMbWU27FdNC2bkCG+jnkeHFZTBRh+yWdr4aa/2WuX7c8RmqxGPMW2UqoJ3fw9hK3QhA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Binder": "10.0.0", + "Microsoft.Extensions.DependencyModel": "10.0.0", + "Serilog": "4.3.0" + } + }, + "Serilog.Sinks.Console": { + "type": "Transitive", + "resolved": "6.1.1", + "contentHash": "8jbqgjUyZlfCuSTaJk6lOca465OndqOz3KZP6Cryt/IqZYybyBu7GP0fE/AXBzrrQB3EBmQntBFAvMVz1COvAA==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Sinks.Debug": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "4BzXcdrgRX7wde9PmHuYd9U6YqycCC28hhpKonK7hx0wb19eiuRj16fPcPSVp0o/Y1ipJuNLYQ00R3q2Zs8FDA==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Sinks.File": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "fKL7mXv7qaiNBUC71ssvn/dU0k9t0o45+qm2XgKAlSt19xF+ijjxyA3R6HmCgfKEKwfcfkwWjayuQtRueZFkYw==", + "dependencies": { + "Serilog": "4.2.0" + } + }, + "SQLitePCLRaw.bundle_e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.11", + "contentHash": "DC4nA7yWnf4UZdgJDF+9Mus4/cb0Y3Sfgi3gDnAoKNAIBwzkskNAbNbyu+u4atT0ruVlZNJfwZmwiEwE5oz9LQ==", + "dependencies": { + "SQLitePCLRaw.lib.e_sqlite3": "2.1.11", + "SQLitePCLRaw.provider.e_sqlite3": "2.1.11" + } + }, + "SQLitePCLRaw.core": { + "type": "Transitive", + "resolved": "2.1.11", + "contentHash": "PK0GLFkfhZzLQeR3PJf71FmhtHox+U3vcY6ZtswoMjrefkB9k6ErNJEnwXqc5KgXDSjige2XXrezqS39gkpQKA==" + }, + "SQLitePCLRaw.lib.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.11", + "contentHash": "Ev2ytaXiOlWZ4b3R67GZBsemTINslLD1DCJr2xiacpn4tbapu0Q4dHEzSvZSMnVWeE5nlObU3VZN2p81q3XOYQ==" + }, + "SQLitePCLRaw.provider.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.11", + "contentHash": "Y/0ZkR+r0Cg3DQFuCl1RBnv/tmxpIZRU3HUvelPw6MVaKHwYYR8YNvgs0vuNuXCMvlyJ+Fh88U1D4tah1tt6qw==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.11" + } + }, + "Swashbuckle.AspNetCore": { + "type": "Transitive", + "resolved": "10.2.3", + "contentHash": "8KNh1RWvofdU6DVLyBs4Z/OpUMnmf8oNvJQc0QxpwySRbi42bwLfdVMMrXZWANg5U5KQGQq1xW6r/hlcqw99tQ==", + "dependencies": { + "Microsoft.Extensions.ApiDescription.Server": "10.0.0", + "Swashbuckle.AspNetCore.Swagger": "10.2.3", + "Swashbuckle.AspNetCore.SwaggerGen": "10.2.3", + "Swashbuckle.AspNetCore.SwaggerUI": "10.2.3" + } + }, + "Swashbuckle.AspNetCore.Swagger": { + "type": "Transitive", + "resolved": "10.2.3", + "contentHash": "1jUUs3WQnrS0FUtaZPLSy1yYMEwS1zlvDmvQ2/eldPHUANX0LJSLVZecCMgSMdeGiRqeaRrIXLtSz++TCiTMww==", + "dependencies": { + "Microsoft.OpenApi": "2.7.5" + } + }, + "Swashbuckle.AspNetCore.SwaggerGen": { + "type": "Transitive", + "resolved": "10.2.3", + "contentHash": "y7t4coDRAeFYChmvlMRiH2OjbiRrm9AVIDgt17fQfs3x9PVAI5PiwWYOhg+4F13R4Q36WDc9lqfoOnNa3tNbGg==", + "dependencies": { + "Swashbuckle.AspNetCore.Swagger": "10.2.3" + } + }, + "Swashbuckle.AspNetCore.SwaggerUI": { + "type": "Transitive", + "resolved": "10.2.3", + "contentHash": "nthWONRs/FJ4yyG206g1cC52WEG8EqrjuMWjGdR+5XG7lbjFto6NqcI9EMICgVFom/UivIjUVwI76ZHbHwTPfQ==" + }, + "System.Diagnostics.EventLog": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "s2PcxHK4IYQ6gmD3VSBkym9tWGkFisKjcjWBdl7a+n4Yy66ae4beJ1ZdjDp060SSll4W3Rt4H2LW87dWckv+QQ==" + }, + "xunit.abstractions": { + "type": "Transitive", + "resolved": "2.0.3", + "contentHash": "pot1I4YOxlWjIb5jmwvvQNbTrZ3lJQ+jUGkGjWE3hEFM0l5gOnBWS+H3qsex68s5cO52g+44vpGzhAt+42vwKg==" + }, + "xunit.analyzers": { + "type": "Transitive", + "resolved": "1.18.0", + "contentHash": "OtFMHN8yqIcYP9wcVIgJrq01AfTxijjAqVDy/WeQVSyrDC1RzBWeQPztL49DN2syXRah8TYnfvk035s7L95EZQ==" + }, + "xunit.assert": { + "type": "Transitive", + "resolved": "2.9.3", + "contentHash": "/Kq28fCE7MjOV42YLVRAJzRF0WmEqsmflm0cfpMjGtzQ2lR5mYVj1/i0Y8uDAOLczkL3/jArrwehfMD0YogMAA==" + }, + "xunit.core": { + "type": "Transitive", + "resolved": "2.9.3", + "contentHash": "BiAEvqGvyme19wE0wTKdADH+NloYqikiU0mcnmiNyXaF9HyHmE6sr/3DC5vnBkgsWaE6yPyWszKSPSApWdRVeQ==", + "dependencies": { + "xunit.extensibility.core": "[2.9.3]", + "xunit.extensibility.execution": "[2.9.3]" + } + }, + "xunit.extensibility.core": { + "type": "Transitive", + "resolved": "2.9.3", + "contentHash": "kf3si0YTn2a8J8eZNb+zFpwfoyvIrQ7ivNk5ZYA5yuYk1bEtMe4DxJ2CF/qsRgmEnDr7MnW1mxylBaHTZ4qErA==", + "dependencies": { + "xunit.abstractions": "2.0.3" + } + }, + "xunit.extensibility.execution": { + "type": "Transitive", + "resolved": "2.9.3", + "contentHash": "yMb6vMESlSrE3Wfj7V6cjQ3S4TXdXpRqYeNEI3zsX31uTsGMJjEw6oD5F5u1cHnMptjhEECnmZSsPxB6ChZHDQ==", + "dependencies": { + "xunit.extensibility.core": "[2.9.3]" + } + }, + "dotnet.samples.aspnetcore.webapi": { + "type": "Project", + "dependencies": { + "AutoMapper": "[16.1.1, 17.0.0)", + "FluentValidation": "[12.1.1, )", + "FluentValidation.DependencyInjectionExtensions": "[12.1.1, )", + "Microsoft.AspNetCore.OpenApi": "[10.0.9, )", + "Microsoft.EntityFrameworkCore.Sqlite": "[10.0.9, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.2, )", + "Serilog.AspNetCore": "[10.0.0, )", + "Serilog.Settings.Configuration": "[10.0.1, )", + "Serilog.Sinks.Console": "[6.1.1, )", + "Serilog.Sinks.File": "[7.0.0, )", + "Swashbuckle.AspNetCore": "[10.2.3, )" + } + } + } + } +} \ No newline at end of file