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>
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.jsonpinssdk.version=10.0.0withrollForward=latestMinor(so any 10.0.x host SDK satisfies).- Both Dockerfiles (
SatelliteProvider.Api/Dockerfile,SatelliteProvider.IntegrationTests/Dockerfile) usemcr.microsoft.com/dotnet/sdk:10.0,aspnet:10.0,runtime:10.0. scripts/run-tests.shand.woodpecker/01-test.ymlreferencemcr.microsoft.com/dotnet/sdk:10.0.Microsoft.AspNetCore.Authentication.JwtBearer,Microsoft.AspNetCore.OpenApi,Serilog.AspNetCoreand everyMicrosoft.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 buildsucceeds; API serves on:18980afterdocker-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.mdTech Stack table reflects.NET 10/ASP.NET Core 10.AGENTS.mdTech 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.csprojSatelliteProvider.Common/SatelliteProvider.Common.csprojSatelliteProvider.DataAccess/SatelliteProvider.DataAccess.csprojSatelliteProvider.Tests/SatelliteProvider.Tests.csprojSatelliteProvider.IntegrationTests/SatelliteProvider.IntegrationTests.csprojSatelliteProvider.TestSupport/SatelliteProvider.TestSupport.csprojSatelliteProvider.Services.TileDownloader/SatelliteProvider.Services.TileDownloader.csprojSatelliteProvider.Services.RegionProcessing/SatelliteProvider.Services.RegionProcessing.csprojSatelliteProvider.Services.RouteManagement/SatelliteProvider.Services.RouteManagement.csproj
- SDK pin:
global.json→sdk.version=10.0.0,rollForward=latestMinor(allowPrerelease=falseretained). - Microsoft.AspNetCore. package bumps* (in
SatelliteProvider.Api/SatelliteProvider.Api.csproj):Microsoft.AspNetCore.Authentication.JwtBearer8.0.25 → 10.0.x (highest 10.0 patch on NuGet)Microsoft.AspNetCore.OpenApi8.0.25 → 10.0.x- Same
JwtBearerbump inSatelliteProvider.Tests/SatelliteProvider.Tests.csproj(consistency rule)
- Serilog bump (in
SatelliteProvider.Api/SatelliteProvider.Api.csproj):Serilog.AspNetCore8.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.Memory9.0.10 → 10.0.x (Tests, Services.TileDownloader)Microsoft.Extensions.Configuration.Abstractions9.0.10 → 10.0.x (DataAccess)Microsoft.Extensions.Configuration.Json9.0.10 → 10.0.x (Tests)Microsoft.Extensions.DependencyInjection9.0.10 → 10.0.x (Tests)Microsoft.Extensions.DependencyInjection.Abstractions9.0.10 → 10.0.x (RegionProcessing, RouteManagement)Microsoft.Extensions.Hosting.Abstractions9.0.10 → 10.0.x (RegionProcessing, RouteManagement)Microsoft.Extensions.Http9.0.10 → 10.0.x (Tests, Services.TileDownloader)Microsoft.Extensions.Logging.Abstractions9.0.10 → 10.0.x (DataAccess, Tests, all 3 services)Microsoft.Extensions.Logging.Console9.0.10 → 10.0.x (Tests)Microsoft.Extensions.Options9.0.10 → 10.0.x (Tests)Microsoft.Extensions.Options.ConfigurationExtensions9.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.0SatelliteProvider.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 tomcr.microsoft.com/dotnet/sdk:8.0→:10.0.woodpecker/01-test.yml: 1 reference tomcr.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 -dsucceeds; 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 aPERF_REPEAT_COUNT=2short 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.
- Full
- Docs:
_docs/02_document/architecture.mdTech Stack table (line ~67):ASP.NET Core (Minimal API) | 8.0→10.0. Also any.NET 8.0mention in the prose at the top of the file.AGENTS.mdTech Stack section:.NET 8.0→.NET 10.
Excluded
- Bumping packages that are not in the
Microsoft.AspNetCore.*/Microsoft.Extensions.*/Serilog.AspNetCorelineup: 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.mdcontract — JWT validation behavior is preserved exactly (JwtBearer 10.x is API-compatible with 8.x for theTokenValidationParameterssurface 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/uploadpermissions-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.mdv1.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.0is 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.xmust still validate signature + lifetime + iss + aud as configured inProgram.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 restoremay 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.xis not yet on NuGet, STOP and ask — do not silently keep 8.0.25 (that defeats AC-4 in spirit even if it letsdotnet restoresucceed). - 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 hostdotnet buildfrom 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.0could 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 probehttp://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 viadocker 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.0is multi-arch (Microsoft publishes amd64 + arm64). Cycle 4 Step 16 Deploy is the integration point — verify the CI run succeeds before merging to dev.