# Migrate solution from .NET 8 LTS to .NET 10 **Task**: AZ-500_dotnet10_migration **Name**: .NET 8 → .NET 10 migration **Description**: Migrate every project in the solution from `net8.0` to `net10.0`, bump the SDK pin, switch every Docker base image and CI image from the `8.0` line to the `10.0` line, bump every `Microsoft.AspNetCore.*` and `Microsoft.Extensions.*` package from the 8.x / 9.x line to the matching 10.x line, and verify the full test + integration + build pipeline stays green. Also closes the cycle-3 perf-harness leftover that was blocked on the host SDK / project SDK mismatch. **Complexity**: 5 points **Dependencies**: None **Component**: Cross-cutting (every component, every Dockerfile, every CI script, two docs) **Tracker**: AZ-500 **Epic**: none (cycle-4 .NET 10 migration; cross-cutting infra) ## Problem The project pins to .NET 8 LTS via `global.json` (`sdk.version=8.0.0`, `rollForward=latestMinor`) and every csproj targets `net8.0`. With .NET 10 GA since November 2025 (~6 months stable as of cycle 4), the host development environments are now provisioning .NET 10 SDK by default. The cycle-3 perf-harness replay surfaced this mismatch concretely: `scripts/run-performance-tests.sh` failed at its bootstrap step because the host had only .NET 10.0.103 and `global.json` would not roll forward (`latestMinor` only rolls within the 8.0.x band). Beyond the immediate perf-script issue, staying on .NET 8 LTS means: - The development experience drifts further from the host SDK with every machine refresh - Microsoft.AspNetCore.* security patches are still flowing for 8.x today (LTS through Nov 2026), but the next major patch line will be 10.x - New language/runtime features (C# 14, .NET 10 perf wins for ASP.NET pipeline) are inaccessible This task lifts the entire solution to .NET 10 in a single coordinated change so the runtime, SDK, packages, Docker images, CI images, and docs all move together — preventing partial-state confusion that a phased migration would create. ## Outcome - All 9 csproj files target `net10.0`. - `global.json` pins `sdk.version=10.0.0` with `rollForward=latestMinor` (so any 10.0.x host SDK satisfies). - Both Dockerfiles (`SatelliteProvider.Api/Dockerfile`, `SatelliteProvider.IntegrationTests/Dockerfile`) use `mcr.microsoft.com/dotnet/sdk:10.0`, `aspnet:10.0`, `runtime:10.0`. - `scripts/run-tests.sh` and `.woodpecker/01-test.yml` reference `mcr.microsoft.com/dotnet/sdk:10.0`. - `Microsoft.AspNetCore.Authentication.JwtBearer`, `Microsoft.AspNetCore.OpenApi`, `Serilog.AspNetCore` and every `Microsoft.Extensions.*` package are pinned to the matching 10.0.x line (or the highest available 10.0 patch at implementation time). - All Microsoft.Extensions.* packages (currently 9.0.10) move to 10.0.x as a single coordinated bump. - Full unit + integration test suite passes against the migrated build (no behavioral regression). - Container image `docker-compose build` succeeds; API serves on `:18980` after `docker-compose up`. - The cycle-3 perf-harness leftover (`_docs/_process_leftovers/2026-05-12_perf-cycle3-harness-execution.md`) is replayed as part of cycle-4 Step 15 (Performance Test) gate; deleted on green or updated with failure detail. - `_docs/02_document/architecture.md` Tech Stack table reflects `.NET 10` / `ASP.NET Core 10`. - `AGENTS.md` Tech Stack section reflects `.NET 10`. ## Scope ### Included - **TFM bump (9 csproj files)**: `net8.0` → `net10.0` in: - `SatelliteProvider.Api/SatelliteProvider.Api.csproj` - `SatelliteProvider.Common/SatelliteProvider.Common.csproj` - `SatelliteProvider.DataAccess/SatelliteProvider.DataAccess.csproj` - `SatelliteProvider.Tests/SatelliteProvider.Tests.csproj` - `SatelliteProvider.IntegrationTests/SatelliteProvider.IntegrationTests.csproj` - `SatelliteProvider.TestSupport/SatelliteProvider.TestSupport.csproj` - `SatelliteProvider.Services.TileDownloader/SatelliteProvider.Services.TileDownloader.csproj` - `SatelliteProvider.Services.RegionProcessing/SatelliteProvider.Services.RegionProcessing.csproj` - `SatelliteProvider.Services.RouteManagement/SatelliteProvider.Services.RouteManagement.csproj` - **SDK pin**: `global.json` → `sdk.version=10.0.0`, `rollForward=latestMinor` (`allowPrerelease=false` retained). - **Microsoft.AspNetCore.* package bumps** (in `SatelliteProvider.Api/SatelliteProvider.Api.csproj`): - `Microsoft.AspNetCore.Authentication.JwtBearer` 8.0.25 → 10.0.x (highest 10.0 patch on NuGet) - `Microsoft.AspNetCore.OpenApi` 8.0.25 → 10.0.x - Same `JwtBearer` bump in `SatelliteProvider.Tests/SatelliteProvider.Tests.csproj` (consistency rule) - **Serilog bump** (in `SatelliteProvider.Api/SatelliteProvider.Api.csproj`): - `Serilog.AspNetCore` 8.0.3 → latest 10.x if published, else stay on 8.0.3 with a one-line note in the batch report - **Microsoft.Extensions.* coordinated bump** (every csproj that references them): - `Microsoft.Extensions.Caching.Memory` 9.0.10 → 10.0.x (Tests, Services.TileDownloader) - `Microsoft.Extensions.Configuration.Abstractions` 9.0.10 → 10.0.x (DataAccess) - `Microsoft.Extensions.Configuration.Json` 9.0.10 → 10.0.x (Tests) - `Microsoft.Extensions.DependencyInjection` 9.0.10 → 10.0.x (Tests) - `Microsoft.Extensions.DependencyInjection.Abstractions` 9.0.10 → 10.0.x (RegionProcessing, RouteManagement) - `Microsoft.Extensions.Hosting.Abstractions` 9.0.10 → 10.0.x (RegionProcessing, RouteManagement) - `Microsoft.Extensions.Http` 9.0.10 → 10.0.x (Tests, Services.TileDownloader) - `Microsoft.Extensions.Logging.Abstractions` 9.0.10 → 10.0.x (DataAccess, Tests, all 3 services) - `Microsoft.Extensions.Logging.Console` 9.0.10 → 10.0.x (Tests) - `Microsoft.Extensions.Options` 9.0.10 → 10.0.x (Tests) - `Microsoft.Extensions.Options.ConfigurationExtensions` 9.0.10 → 10.0.x (RegionProcessing, RouteManagement, Services.TileDownloader) - **Docker base image bumps**: - `SatelliteProvider.Api/Dockerfile`: `mcr.microsoft.com/dotnet/aspnet:8.0` → `:10.0`, `mcr.microsoft.com/dotnet/sdk:8.0` → `:10.0` - `SatelliteProvider.IntegrationTests/Dockerfile`: `mcr.microsoft.com/dotnet/sdk:8.0` → `:10.0`, `mcr.microsoft.com/dotnet/runtime:8.0` → `:10.0` - **Script + CI image bumps**: - `scripts/run-tests.sh`: 3 references to `mcr.microsoft.com/dotnet/sdk:8.0` → `:10.0` - `.woodpecker/01-test.yml`: 1 reference to `mcr.microsoft.com/dotnet/sdk:8.0` → `:10.0` - **Verification**: - Full `./scripts/run-tests.sh --full` (unit + integration via docker-compose) passes after the migration. - `docker-compose build && docker-compose up -d` succeeds; API responds with HTTP 401 on `/api/satellite/region/` (expected for unauthenticated probe), and HTTP 301/200 on `/swagger`. - **AC-5 perf-bootstrap smoke**: a dedicated smoke run of `./scripts/run-performance-tests.sh` (full PT-01..PT-08 OR a `PERF_REPEAT_COUNT=2` short variant if time-constrained) succeeds at the bootstrap step (no SDK error). This explicitly proves the cycle-3 leftover is closed; the leftover file is then deleted in the same commit OR (if a perf threshold fails) updated with the failure detail. - **Docs**: - `_docs/02_document/architecture.md` Tech Stack table (line ~67): `ASP.NET Core (Minimal API) | 8.0` → `10.0`. Also any `.NET 8.0` mention in the prose at the top of the file. - `AGENTS.md` Tech Stack section: `.NET 8.0` → `.NET 10`. ### Excluded - Bumping packages that are not in the `Microsoft.AspNetCore.*` / `Microsoft.Extensions.*` / `Serilog.AspNetCore` lineup: Dapper, Npgsql, dbup-postgresql, Newtonsoft.Json, SixLabors.ImageSharp, Microsoft.IdentityModel.Tokens, System.IdentityModel.Tokens.Jwt, xunit, Moq, FluentAssertions, coverlet.collector, Microsoft.NET.Test.Sdk, Swashbuckle.AspNetCore, Serilog.Sinks.File. These remain at their current pinned versions; the implementer only verifies they restore on net10 — if any fails to restore, surface and STOP, do not bump opportunistically. - Adopting C# 14 features in production code. The TFM bump enables them, but no source-code rewrite is in scope. - Performance optimizations enabled by .NET 10 (e.g., new ASP.NET pipeline improvements). Out of scope; should appear as separate PBIs if/when measured benefit is demonstrated. - Production deployment (Step 16 Deploy in cycle 4 handles that — this PBI only ships the change to dev). - Cycle-3 perf full retro-replay: only the bootstrap smoke is gated by AC-5. The full perf threshold check happens at Step 15 of cycle 4 (which also closes the leftover). - Touching the suite-level `suite/_docs/10_auth.md` contract — JWT validation behavior is preserved exactly (JwtBearer 10.x is API-compatible with 8.x for the `TokenValidationParameters` surface used here). ## Acceptance Criteria **AC-1: Every csproj targets net10.0** Given the post-PBI repository When `grep -r "" --include="*.csproj"` is run from the repo root Then every match shows `net10.0` and zero matches show `net8.0` or any other TFM. **AC-2: SDK pin is on the 10.x line** Given the post-PBI `global.json` When the file is read Then `sdk.version` is `10.0.0` and `rollForward` is `latestMinor`. **AC-3: All Docker base images and CI images are on the 10.0 line** Given the post-PBI repository When `grep -r "mcr.microsoft.com/dotnet/" --include="*Dockerfile" --include="*.yml" --include="*.sh"` is run Then every match references the `:10.0` tag and zero matches reference `:8.0` (or any earlier tag). **AC-4: Microsoft.AspNetCore.* + Microsoft.Extensions.* + Serilog.AspNetCore packages match the 10.x line** Given the post-PBI csproj files When `grep -E '(Microsoft\.AspNetCore\.|Microsoft\.Extensions\.|Serilog\.AspNetCore)' --include="*.csproj" -r` is run Then every Microsoft.AspNetCore.* and Microsoft.Extensions.* match resolves to `Version="10.0.x"` (any 10.0 patch); Serilog.AspNetCore resolves to a 10.x version OR retains `8.0.3` with the batch report explaining why no 10.x was published; AND no Microsoft.AspNetCore/Extensions match resolves to an 8.x or 9.x version. **AC-5: Perf-script bootstrap succeeds (closes cycle-3 leftover)** Given the migrated repository AND `docker-compose up -d --build` has completed (API on :18980) When `./scripts/run-performance-tests.sh` is invoked (either full run or `PERF_REPEAT_COUNT=2 PERF_UAV_BATCH_SIZE=2 ./scripts/run-performance-tests.sh` for a short bootstrap-only smoke) Then the script does NOT exit with code 3 at the `dotnet build SatelliteProvider.IntegrationTests` bootstrap step (i.e. the host SDK / global.json mismatch is gone), AND `_docs/_process_leftovers/2026-05-12_perf-cycle3-harness-execution.md` is either deleted (if all perf checks pass) or updated with the actual failure detail (if perf thresholds regress — no longer an SDK problem). **AC-6: All tests pass against the migrated build** Given the migrated repository When `./scripts/run-tests.sh --full` is run Then all unit + integration tests pass with no new failures vs. the pre-migration baseline (cycle-3 closing test report). **AC-7: Container image build succeeds** Given the migrated repository When `docker-compose build` is run from the repo root Then both the `api` and `integration-tests` images build successfully without warnings about package downgrade, framework conflict, or missing base image. **AC-8: Documentation reflects the migration** Given the post-PBI repository When `_docs/02_document/architecture.md` Tech Stack table and the prose at the top of the file are read Then both reference `.NET 10` / `ASP.NET Core 10` and zero references to `.NET 8.0` / `8.0` (in the framework-version sense) remain in that file. Same check for `AGENTS.md` Tech Stack section. ## Non-Functional Requirements **Compatibility** - 8.x → 10.x is a major framework bump. The cycle-3 architecture-compliance baseline (no Critical/High Architecture findings going into cycle 4) must hold after the migration — re-run a baseline scan on the migrated build (Step 12 Test-Spec Sync triggers this naturally). If new findings emerge from runtime behavior change, surface and decide before deploy. - The JWT validation contract (signature + lifetime + iss + aud + 30s clock skew) MUST be preserved exactly. AZ-487 + AZ-494 integration tests are the gate; if any of them regress, STOP and root-cause before continuing. - The `/api/satellite/upload` permissions-claim policy (AZ-488) MUST work unchanged. AZ-488's integration tests are the gate. - The N-source tile storage contract (`_docs/02_document/contracts/data-access/tile-storage.md` v1.0.0) is unaffected — this is a runtime/SDK migration, not a data-model change. **Performance** - The full cycle-3 perf harness (PT-01..PT-08) when run at Step 15 of cycle 4 must not regress beyond the existing thresholds. .NET 10 generally improves ASP.NET pipeline perf, so we expect equal-or-better numbers — but the actual measurement gates this. AC-5 only proves the bootstrap, not the thresholds. **Reliability** - Any package that fails to restore against `net10.0` is a hard STOP — the implementer surfaces it and we decide (bump that package as part of this PBI, or roll back the migration). Silent fallback is not acceptable. **Security** - The migration MUST NOT regress the cycle-2/3 security posture. Specifically: `Microsoft.AspNetCore.Authentication.JwtBearer 10.0.x` must still validate signature + lifetime + iss + aud as configured in `Program.cs`. The cycle-3 dependency_scan.md needs a new pass after the bump (cycle-4 Step 14 Security Audit handles that). ## Unit Tests | AC Ref | What to Test | Required Outcome | |--------|-------------|-----------------| | AC-6 | All existing unit tests in SatelliteProvider.Tests | Pass unchanged on net10.0 | ## Blackbox Tests | AC Ref | Initial Data/Conditions | What to Test | Expected Behavior | NFR References | |--------|------------------------|-------------|-------------------|----------------| | AC-1 | Migrated repo | `grep -r "" --include="*.csproj"` | All matches show `net10.0`, none show any other TFM | — | | AC-2 | Migrated repo | Read `global.json` | `sdk.version=10.0.0`, `rollForward=latestMinor` | — | | AC-3 | Migrated repo | `grep -r "mcr.microsoft.com/dotnet/" --include="*Dockerfile" --include="*.yml" --include="*.sh"` | All matches reference `:10.0` | — | | AC-4 | Migrated repo | `grep -E '(Microsoft\.AspNetCore\.\|Microsoft\.Extensions\.\|Serilog\.AspNetCore)' --include="*.csproj" -r` | M.AspNetCore.* and M.Extensions.* on 10.0.x; Serilog.AspNetCore on 10.x or documented 8.0.3 | — | | AC-5 | Migrated repo + `docker-compose up -d --build` complete | `./scripts/run-performance-tests.sh` (full or short variant) | Bootstrap step exits 0; leftover file deleted or updated with non-SDK failure detail | Performance | | AC-6 | Migrated repo | `./scripts/run-tests.sh --full` | All unit + integration tests pass | Compatibility, Reliability | | AC-7 | Migrated repo | `docker-compose build` from repo root | Both images build without warnings about downgrade or framework conflict | Compatibility | | AC-8 | Migrated repo | Read `_docs/02_document/architecture.md` and `AGENTS.md` | All `.NET 8.0` framework-version mentions are now `.NET 10` | — | ## Constraints - **Coordinated bump**: TFM, SDK pin, Docker images, CI images, and Microsoft.AspNetCore.* / Microsoft.Extensions.* package versions ALL move in the same commit (or same set of commits inside one PR). Do NOT split into "TFM-first, packages-later" — that creates an intermediate state where `dotnet restore` may pick the wrong framework version of a package. - **Implementer must verify package availability** before pinning a 10.0.x version. If `Microsoft.AspNetCore.Authentication.JwtBearer 10.0.x` is not yet on NuGet, STOP and ask — do not silently keep 8.0.25 (that defeats AC-4 in spirit even if it lets `dotnet restore` succeed). - **Use the established Docker pattern for builds** (per AGENTS.md): `docker run --rm -v "$PROJECT_ROOT:/src" -w /src mcr.microsoft.com/dotnet/sdk:10.0 sh -c "..."`. Do NOT invoke host `dotnet build` from this agent's terminal — AGENTS.md is explicit about this. The single exception is during interactive debugging by the user. - **Do not silently fold in unrelated package bumps**. If the implementer notices `Microsoft.NET.Test.Sdk 17.8.0` could go to 17.x — that is a separate PBI. Note it in the batch report; do not bump. - **Do not rename any database objects** during this task (coderule.mdc constraint). The migration is runtime-only. - **Cycle-3 perf leftover deletion**: AC-5 specifies that the leftover file is deleted ONLY when the full perf script runs cleanly. If only the short bootstrap-smoke variant is run during this PBI's implementation, the leftover stays in place and is closed by Step 15 of this same cycle (which runs the full perf harness against the migrated build). ## Risks & Mitigation **Risk 1: One of the `Microsoft.AspNetCore.*` 10.0.x packages introduces a behavioral change in JWT validation** - *Risk*: AZ-487/AZ-494 integration tests fail because JwtBearer 10.x changed default `TokenValidationParameters`, the lifetime tolerance behavior, or the WWW-Authenticate header shape. - *Mitigation*: AC-6 (full test suite green) is the gate. If a regression appears, the implementer reads the JwtBearer 10.x release notes, narrows the diff, and either updates the configuration to preserve 8.x semantics OR surfaces and asks. Do not silently relax the test. **Risk 2: `Microsoft.AspNetCore.OpenApi 10.x` breaks the Swagger UI generation** - *Risk*: AZ-353 / cycle-3 swagger-ready endpoint surface regresses. - *Mitigation*: AC-7 (image build) catches build-level breakage. After `docker-compose up`, manually probe `http://localhost:18980/swagger` — should return 301/200, not 500. **Risk 3: Microsoft.Extensions.* 10.0.x cascade** - *Risk*: Bumping all M.E.* to 10.0.x may pull in transitive updates that conflict with the still-pinned `Microsoft.IdentityModel.Tokens 7.0.3` / `System.IdentityModel.Tokens.Jwt 7.0.3`. - *Mitigation*: `dotnet restore` (run via `docker run sdk:10.0`) surfaces conflicts immediately. If a conflict appears, the implementer documents it and either bumps the IdentityModel packages as part of this PBI (preferred — they're security-sensitive) OR keeps 7.0.3 with a downgrade-tolerated note. **Risk 4: `Serilog.AspNetCore` has no 10.x line published** - *Risk*: Implementer can't bump cleanly. - *Mitigation*: Per Scope.Included, falling back to 8.0.3 on net10.0 is acceptable as long as it's documented in the batch report. Serilog targets netstandard 2.0; 8.0.3 should restore on net10 unless a transitive dep is the blocker. **Risk 5: Cycle-3 perf-harness still fails after migration for a non-SDK reason** - *Risk*: AC-5 short smoke succeeds at the bootstrap step but Step 15 (Performance Test) of cycle 4 still finds threshold regressions (e.g., PT-08 batch p95 > 2000ms because of a real .NET 10 ASP.NET pipeline change). - *Mitigation*: AC-5 only gates the bootstrap step (the cycle-3 leftover's specific failure mode). Step 15 of cycle 4 is the real perf-threshold gate. If thresholds regress, that becomes a Step 15 decision (defer to leftover, raise threshold, or root-cause), not a blocker for THIS PBI. **Risk 6: CI agent (`platform: arm64`) does not yet have a `mcr.microsoft.com/dotnet/sdk:10.0` arm64 image cached** - *Risk*: First CI run after merge takes longer than usual; or worse, the arm64 manifest doesn't exist for the chosen tag. - *Mitigation*: `mcr.microsoft.com/dotnet/sdk:10.0` is multi-arch (Microsoft publishes amd64 + arm64). Cycle 4 Step 16 Deploy is the integration point — verify the CI run succeeds before merging to dev.