documentation/dev/updating-dotnet-version.md
This checklist documents every file that needs updating when bumping the .NET SDK version (e.g., .NET 10 → .NET 11).
| Property | Purpose | Example |
|---|---|---|
| TFMBase | Lowest .NET for class libraries only (no platform TFMs) | net6.0 |
| TFMPrevious | Previous .NET with full platform support | net9.0 |
| TFMCurrent | Current .NET with full platform support | net10.0 |
| TPV*Previous | Target Platform Versions for TFMPrevious | TPViOSPrevious=18.0 |
| TPV*Current | Target Platform Versions for TFMCurrent | TPViOSCurrent=26.0 |
Important: Starting with .NET 10, Apple TPVs use Xcode 26 unified SDK versioning. iOS, MacCatalyst, tvOS, and macOS all use
26.0(not the OS version like18.0or15.0). Check for valid TPVs withdotnet new console -f net10.0-iosand observe the error message listing valid versions.
global.json — Update sdk.version to the new SDK feature band (e.g., 10.0.100). Use "rollForward": "latestPatch" to accept any patch version available on CI agents.scripts/azure-templates-variables.yml — Update DOTNET_VERSION and DOTNET_WORKLOAD_VERSION to match the new SDK versionscripts/install-dotnet-workloads.ps1 — Review Tizen script URL (Samsung repo may update)Note: Do NOT set
workloadVersioninglobal.json. Native builds skip SDK install but still read global.json, causing failures if the pinned workload version isn't pre-installed.
source/SkiaSharp.Build.props — This is the most critical file:
dotnet workload list)SupportedOSPlatformVersion: Each .NET version may raise the minimum supported OS versions. For .NET 10: iOS/tvOS minimum is 12.2, MacCatalyst minimum is 15.0, macOS minimum is 12.0. These are enforced by the workloads and will cause build errors if too low.
All use $(TFMPrevious)-platform$(TPVPrevious);$(TFMCurrent)-platform$(TPVCurrent) pattern.
binding/SkiaSharp.NativeAssets.Android/SkiaSharp.NativeAssets.Android.csprojbinding/SkiaSharp.NativeAssets.iOS/SkiaSharp.NativeAssets.iOS.csprojbinding/SkiaSharp.NativeAssets.MacCatalyst/SkiaSharp.NativeAssets.MacCatalyst.csprojbinding/SkiaSharp.NativeAssets.tvOS/SkiaSharp.NativeAssets.tvOS.csprojbinding/SkiaSharp.NativeAssets.Tizen/SkiaSharp.NativeAssets.Tizen.csprojbinding/SkiaSharp.NativeAssets.macOS/SkiaSharp.NativeAssets.macOS.csproj (also has BasicTargetFrameworks)binding/HarfBuzzSharp.NativeAssets.Android/HarfBuzzSharp.NativeAssets.Android.csprojbinding/HarfBuzzSharp.NativeAssets.iOS/HarfBuzzSharp.NativeAssets.iOS.csprojbinding/HarfBuzzSharp.NativeAssets.MacCatalyst/HarfBuzzSharp.NativeAssets.MacCatalyst.csprojbinding/HarfBuzzSharp.NativeAssets.tvOS/HarfBuzzSharp.NativeAssets.tvOS.csprojbinding/HarfBuzzSharp.NativeAssets.Tizen/HarfBuzzSharp.NativeAssets.Tizen.csprojbinding/HarfBuzzSharp.NativeAssets.macOS/HarfBuzzSharp.NativeAssets.macOS.csprojsource/SkiaSharp.Views/SkiaSharp.Views.Blazor/SkiaSharp.Views.Blazor.csproj — Update TFM list and add PackageReference for new Microsoft.AspNetCore.Components.Web versiontests/SkiaSharp.Tests.Devices/SkiaSharp.Tests.Devices.csproj — Uses $(MauiTargetFrameworksAppCurrent)tests/SkiaSharp.Tests.Integration/SkiaSharp.Tests.Integration.csproj — Hardcoded TFMtests/SkiaSharp.Tests.Integration/Tests/LinuxConsoleTests.cs — Hardcoded TFM in string templatetests/SkiaSharp.Tests.Integration/Tests/Maui*Tests.cs — Hardcoded TFMs in TargetFramework propertybuild.cake — 4 hardcoded TFMs in test tasks (~lines 285, 333, 365, 397)scripts/infra/managed/utils-managed.cake — Framework check list (add new netX.0)scripts/infra/docs/docs.cake — Apple/Android ref package names include TFM+TPV (e.g., Microsoft.iOS.Ref.net10.0_18.0)native/winui/build.cake — WinUI Projection output path uses $(WindowsTargetFrameworksPrevious)utils/SkiaSharpGenerator/SkiaSharpGenerator.csprojutils/NativeLibraryMiniTest/docker/NativeLibraryMiniTest.csprojsamples/Basic/*/SkiaSharpSample.csproj — 16 files with hardcoded TFMssamples/Basic/UnoPlatform/SkiaSharpSample/Properties/launchSettings.jsonscripts/azure-templates-variables.yml — DOTNET_VERSION, DOTNET_WORKLOAD_VERSION, XCODE_VERSION, EMSCRIPTEN_VERSION, test device versionsscripts/azure-templates-stages-native-wasm.yml — Add new .NET emscripten entryscripts/azure-templates-jobs-bootstrapper.yml — Review workload install stepWASM emsdk mapping (do this whenever the new SDK bundles a new Emscripten version). The .NET WASM SDK links apps with a specific Emscripten toolchain, and a static library built with one Emscripten version cannot be linked by a different one (the wasm object format is incompatible → link failure). Check the new SDK's bundled version (e.g.
dotnet workload list/ theMicrosoft.NET.Runtime.Emscripten.*pack). Known mapping so far: .NET 8 → 3.1.34, .NET 9/10 → 3.1.56, .NET 11 → 5.0.6. When it changes for the new SDK, you must:
- Add a build matrix block (all 4
st/mt/simd/simd+mtvariants) for the new Emscripten version inscripts/azure-templates-stages-native-wasm.yml, and register itsnative_wasm_<version>_*artifacts in both merger lists inscripts/azure-templates-stages-native-merge.yml, so the packages ship a static library for it.- Add a
NativeFileReferenceentry for the new TFM in all four WASM targets files, keeping eachnetX.0on the Emscripten version its SDK actually uses:
binding/SkiaSharp.NativeAssets.WebAssembly/buildTransitive/SkiaSharp.targetsbinding/HarfBuzzSharp.NativeAssets.WebAssembly/buildTransitive/HarfBuzzSharp.targetsbinding/IncludeNativeAssets.SkiaSharp.targetsbinding/IncludeNativeAssets.HarfBuzzSharp.targetsConvention for the conditions: the newest entry stays open-ended (
VersionGreaterThanOrEquals(TFV, 'A')) so a future SDK that keeps the same Emscripten version keeps working with no code change (e.g. .NET 9 and .NET 10 both use 3.1.56). Only when a new SDK actually diverges do you close the previous entry with an upper bound (… and VersionLessThan(TFV, 'B')) and add a new open-ended entry for the new version — the waynet9.0–net10.xwas capped at< 11.0once .NET 11 moved to 5.0.6. The packaging globs (**/*over the version folder) pick up new version directories automatically — no nuspec/csproj change needed.
scripts/infra/native/linux/docker/*/Dockerfile — Update FROM mcr.microsoft.com/dotnet/sdk:X.0 to new version
nuget.config — Remove old preview feeds, keep dotnet-public + dotnet-eng + test-device-runnersNote:
nuget.orgis a disallowed source in the SkiaSharp CI pipeline. If you encounter missing package restore errors during development, you can temporarily add nuget.org to work through issues, but it must be removed before merging. Request mirroring for any missing packages.
Before merging a .NET upgrade PR, verify these items:
nuget.config — Must NOT contain nuget.org source (disallowed in CI)scripts/azure-pipelines-complete.yml — buildExternals parameter must be reset to 'latest' (not a specific build ID)documentation/dev/updating-dotnet-version.md reflects any new learningsWhen upgrading .NET versions, watch for these common issues:
.NET 9 changed System.Numerics.Matrix4x4.CreateFromAxisAngle to go through Quaternion, producing slightly different floating-point results. Tests using exact float comparisons may need tolerance adjustments. The AssertSimilar helper in tests/Tests/SkiaSharp/SKTest.cs uses Math.Round() (not truncation) to handle this.
Starting with .NET 10, Apple workloads use Xcode 26 unified SDK versioning. The TPV is 26.0 for all Apple platforms, not the OS version numbers like 18.0 (iOS), 15.0 (MacCatalyst), etc. Build errors like NETSDK1140: 18.0 is not a valid TargetPlatformVersion for iOS indicate this issue.
Check the MAUI release notes for API changes. Common issues:
Microsoft.Maui.Hosting.Compatibility removed in .NET 10)Tizen is not an official Microsoft workload. Samsung may lag behind on .NET version support. Check https://github.com/Samsung/Tizen.NET for compatibility before upgrading.
These use MSBuild properties from SkiaSharp.Build.props:
$(BasicTargetFrameworks) — NativeAssets.Linux, Win32, WebAssembly, etc.$(WindowsTargetFrameworks) — NativeAssets.WinUI, NanoServer, Views.WinUI$(MauiTargetFrameworks) — Views.Maui.Core, Views.Maui.Controls$(UnoTargetFrameworks) — Views.Uno.WinUI, Skia, Wasm$(TFMCurrent) — Benchmarks, test console projects, Direct3Dbinding/NativeAssets.Build.targets — Uses $(TFMCurrent)native/winui/.../SkiaSharp.Views.WinUI.Native.Projection.csproj — Uses $(WindowsTargetFrameworksPrevious)IsTargetFrameworkCompatible('net7.0') conditions in binding csproj files — floor check.sln / .slnf files — don't encode TFMssamples/Gallery/ — Legacy samples, not updatedSince platform workloads only support 2 versions at a time, testing a preview means shifting the TFM chain:
TFMPrevious ← old TFMCurrent (e.g., net10.0)TFMCurrent ← the preview version (e.g., net11.0)global.json SDK version ← preview SDK (e.g., 11.0.100-preview.1)global.json allowPrerelease ← trueDOTNET_VERSION ← preview SDK versionDOTNET_WORKLOAD_VERSION ← preview workload set versionThere is no side-by-side preview mechanism — the DOTNET_VERSION in the pipeline IS the SDK version, preview or not.
After installing the new SDK, check actual workload TPVs:
dotnet workload list
# Then check manifest files in:
# ~/.dotnet/sdk-manifests/<version>/
# Or try to create a project and observe the error for valid TPVs:
dotnet new console -f net10.0-ios
# Error will list valid TPVs like: 26.0, 26.2
Workloads are pinned via the DOTNET_WORKLOAD_VERSION pipeline variable, which is passed to install-dotnet-workloads.ps1 as -WorkloadVersion. This uses the .NET SDK workload sets feature (dotnet workload install --version <version>) for reproducible builds.
Why not use workloadVersion in global.json? Native builds (which skip SDK/workload install) still read global.json. If the pinned workload version isn't pre-installed on the agent, the build fails immediately. By passing the version through the pipeline variable, we control when workload pinning applies.
Exception: Tizen is not an official workload — it uses Samsung's custom install scripts from Samsung/Tizen.NET repository.
To speed up CI iteration when debugging managed code issues, set the buildExternals parameter to a previous build ID that has successful native stages. This skips native compilation and downloads artifacts from the specified build instead.
If CI agents don't have the exact SDK version in global.json, use "rollForward": "latestPatch" to accept any patch version in the same feature band (e.g., 10.0.100 accepts 10.0.102).
If dotnet workload restore fails with "no project found", the pipeline uses explicit dotnet workload install with a list of workloads instead.