Files
satellite-provider/_docs/02_tasks/done/AZ-500_dotnet10_migration.md
T
Oleksandr Bezdieniezhnykh 813136326f [AZ-500] .NET 8 -> .NET 10 migration
Coordinated cross-cutting bump: 9 csproj TFMs net8.0 -> net10.0;
global.json sdk.version 8.0.0 -> 10.0.0; all Dockerfiles + scripts/
+ .woodpecker on mcr.microsoft.com/dotnet/{sdk,aspnet,runtime}:10.0;
all Microsoft.AspNetCore.* (8.0.25) and Microsoft.Extensions.* (9.0.10)
packages -> 10.0.7. Serilog.AspNetCore retained at 8.0.3 (10.0.0
requires Serilog.Sinks.File >= 7.0.0; out of AZ-500 scope per "no
unrelated package bumps") -- documented in AGENTS.md. Swashbuckle
9.x bumped to 10.1.7 to track Microsoft.OpenApi 2.x; Program.cs +
ParameterDescriptionFilter.cs refactored for the 2.x namespace
(Microsoft.OpenApi), OpenApiSecuritySchemeReference, JsonSchemaType
enum, and IOpenApiSchema dictionary properties. Fixed implicit AC-5
prereq: scripts/run-performance-tests.sh PERF_DLL path bin/Release/
net8.0 -> net10.0. Docs sync: architecture.md + AGENTS.md.

ACs verified: AC-1..AC-4 + AC-7 + AC-8 by grep + build; AC-6 by
./scripts/run-tests.sh --full (271/271 unit tests + full integration
suite green); AC-5 short bootstrap-smoke (PERF_REPEAT_COUNT=2
PERF_UAV_BATCH_SIZE=2) succeeded at the bootstrap step (no exit 3),
PT-01..PT-07 PASS. PT-08 surfaced a pre-existing grep-pipefail bug
in run-performance-tests.sh:417 -- not an SDK problem; recorded as
follow-up in the perf-cycle3 leftover. Code review verdict:
PASS_WITH_WARNINGS (2 Medium deferred per scope discipline:
WithOpenApi ASPDEPR002 deprecation x8, CS8604 nullable in
ParameterDescriptionFilter.cs; both targeted at follow-up PBIs).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 05:28:01 +03:00

19 KiB

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): <TargetFramework>net8.0</TargetFramework><TargetFramework>net10.0</TargetFramework> 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.jsonsdk.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/<id> (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.010.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 "<TargetFramework>" --include="*.csproj" is run from the repo root Then every match shows <TargetFramework>net10.0</TargetFramework> 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 "<TargetFramework>" --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.