.NET 8 和 .NET 9 升級至 .NET 10:2026 年 11 月 10 日前的檢查清單
Your repo isn't on .NET 8 in just one file: what to change before November 10
LTS Lift 整理了 .NET 8 和 .NET 9 在 2026 年 11 月 10 日停止支援前升級至 .NET 10 的檢查項目,指出相關設定分散於專案檔、SDK、容器映像及 CI 等多處。
On 10 November 2026, .NET 8 and .NET 9 both reach end of support. So does the in-process model for Azure Functions. After that date there are no more security patches for those runtimes. Nothing stops working on the 11th, but every vulnerability found from then on stays open.
.NET 10 is the current LTS release, supported until November 2028, so it's the obvious target. And for a lot of projects the core of the upgrade really is a one-line change:
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net10.0</TargetFramework>
The problem is that the version your app actually builds and runs on isn't decided in that one line. In a typical repo it's spread across six or seven files, and each one can quietly keep you on .NET 8 after you think you've moved. This post walks through where to look, what to change, and what tends to break.
1. Project files, including the ones that don't look like they set a version
The obvious place is <TargetFramework> in each .csproj, .fsproj or .vbproj. Less obvious:
-
<TargetFrameworks>(plural) for multi-targeted libraries, e.g.net8.0;netstandard2.0. -
Properties. Many solutions set
<TargetFramework>$(AppTfm)</TargetFramework>and defineAppTfmin aDirectory.Build.propsone or two folders up. Searching fornet8.0in csproj files finds nothing; the real value is in the props file. -
Old-style projects use
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>. Those are .NET Framework, which is a different (and bigger) migration. -
net48/net462TFMs in SDK-style projects are also .NET Framework. 4.6.2 reaches end of support on 12 January 2027.
A quick first pass:
grep -rn --include=*.csproj --include=*.props -E "TargetFramework|AppTfm" .
If your TFMs come from a property, change it in Directory.Build.props once rather than editing every project.
2. global.json
global.json pins the SDK used to build. If it says 8.0.404, a machine with only the .NET 10 SDK will refuse to build (or roll forward in ways you didn't intend), and CI will happily keep using SDK 8.
{ "sdk": { "version": "10.0.100", "rollForward": "latestFeature" } }
Two details: the version must be a full version number like 10.0.100, not 10.0. And rollForward: latestFeature saves you from bumping the file for every patch release.
3. Dockerfiles and compose files
Base images carry their own version:
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
...
FROM mcr.microsoft.com/dotnet/aspnet:8.0
Both lines need to move to 10.0. Things to know when you do it:
-
The default .NET 10 image tags are Ubuntu 24.04, not Debian. If your Dockerfile runs
apt-get installfor anything Debian-specific, or uses a-bookwormtag, check the package names. - If you're coming from .NET 6 or 7: since .NET 8 the default container port is 8080, not 80.
EXPOSE, compose port mappings, Kubernetes manifests and App Service'sWEBSITES_PORTmay all need updating. - .NET 9 images no longer include zlib. If you call it natively, install it.
-
docker-compose.ymloften has its own image references (a worker, a migration job), sometimes with a variable tag like${DOTNET_TAG}. Those are easy to forget.
4. CI pipelines
The build agent installs whatever SDK the pipeline asks for:
- uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
In Azure Pipelines it's the UseDotNet@2 task's version. Matrix builds are a common hiding place: dotnet-version: ${{ matrix.dotnet }} with the actual values defined a few lines above.
One .NET 10 SDK change to know about if your scripts parse CLI output: some messages now go to stderr, and --interactive defaults to true in user contexts.
5. Packages
Move Microsoft.AspNetCore.*, Microsoft.EntityFrameworkCore.* and Microsoft.Extensions.* to 10.0.x. For third-party packages, only upgrade what doesn't support .NET 10 or has a known vulnerability. An LTS upgrade is a bad time to take three unrelated major versions at once.
The .NET 10 SDK audits transitive packages during restore. With TreatWarningsAsErrors, a build that was green on 8 can go red on 10 for reasons unrelated to your code. Look at what it flags rather than turning audit off.
dotnet package list --outdated # .NET 10 SDK syntax
dotnet list package --vulnerable --include-transitive # older SDKs
6. Azure Functions: in-process has to become isolated
This is the item that turns "a one-line change" into real work. The in-process model ends support on the same day, and .NET 10 Functions only run on the isolated worker model. Microsoft's migration guide covers it in full. The steps that catch people:
-
Microsoft.NET.Sdk.Functionsis replaced by the worker packages, and everyMicrosoft.Azure.WebJobs.Extensions.*by itsMicrosoft.Azure.Functions.Worker.Extensions.*equivalent. -
FunctionsStartupgoes away; configuration moves into aProgram.cs. -
[FunctionName]becomes[Function],ILoggercomes from DI, and output bindings work differently (IAsyncCollector<T>is gone). -
Serialization switches to System.Text.Json. Newtonsoft's
[JsonProperty]attributes are ignored without any error. Your function still runs, it just reads or writes different property names. -
FUNCTIONS_WORKER_RUNTIMEchanges todotnet-isolated. Change the setting and deploy the new code together, ideally through a staging slot and a swap. Doing one without the other gives you AZFD0013 errors in between.
7. What compiles but behaves differently
Going 8 → 10 crosses two sets of breaking changes (9.0 and 10.0). The ones I'd check first because they don't show up as build errors:
-
BinaryFormatteralways throws (since 9). -
BackgroundService.ExecuteAsyncnow runs entirely as a Task, so synchronous work at the top of it no longer delays startup. - Configuration keeps
nullvalues. - Cookie authentication returns 401 instead of redirecting to login for API endpoints.
- In Development the host validates DI scopes on build, so latent DI bugs suddenly appear locally.
- EF Core 9 throws if you apply migrations while the model has pending changes. EF Core 10 on Azure SQL may generate a migration that changes JSON columns to the new
jsontype. After upgrading, rundotnet ef migrations add UpgradeCheck. If it isn't empty, read it before anything gets deployed.
Finding all of it at once
Doing the search above by hand works, but it's easy to miss a compose file or a property two directories up. I wrote a small open-source CLI, dotnet-eol-scan, that reads all of these sources as text and lists every .NET version it finds with its end-of-support date. It needs Node 18+, has no dependencies, and doesn't send anything anywhere.
npx github:khoaanh10/dotnet-eol-scan
Here is its output on a small demo repo: an ASP.NET Core API whose TFM comes from Directory.Build.props, a Functions project still on in-process, a shared netstandard2.0 library, a Dockerfile, global.json and a GitHub Actions workflow (run with --date 2026-10-24 so the numbers are stable):
[SOON] .github/workflows/build.yml GitHub Actions setup-dotnet: 8.0 end of support: 2026-11-10 (17 days left)
[SOON] global.json SDK pin (global.json): 8.0 end of support: 2026-11-10 (17 days left)
[SOON] src/Shop.Api/Dockerfile Docker image (sdk): 8.0 end of support: 2026-11-10 (17 days left)
[SOON] src/Shop.Api/Dockerfile Docker image (aspnet): 8.0 end of support: 2026-11-10 (17 days left)
[SOON] src/Shop.Api/Shop.Api.csproj TargetFramework: net8.0 end of support: 2026-11-10 (17 days left)
Resolved from $(AppTfm) in Directory.Build.props
[SOON] src/Shop.Functions/Shop.Functions.csproj TargetFramework: net8.0 end of support: 2026-11-10 (17 days left)
[SOON] src/Shop.Functions/Shop.Functions.csproj Azure Functions: in-process model end of support: 2026-11-10 (17 days left)
Must move to isolated worker model before upgrading to .NET 10
[OK ] src/Shop.Core/Shop.Core.csproj TargetFramework: netstandard2.0 end of support: -
EOL: 0 | Ending soon: 7 | Info: 0 | OK: 1
Seven places, one of which (the API's TFM) wouldn't show up in a search for net8.0 in project files. It exits with code 1 when something is out of support or ending soon, so you can run it in CI:
- name: Scan for end-of-life .NET versions
shell: bash
run: npx --yes github:khoaanh10/dotnet-eol-scan#v0.2.0 . --md --fail-on eol | tee -a "$GITHUB_STEP_SUMMARY"
After the upgrade, run it again: everything should read OK except whatever you've deliberately left for later.
Its limits, so you know what it won't catch: it doesn't run MSBuild, so Condition attributes aren't evaluated, and properties set in .targets files, environment variables or on the command line are reported as INFO for you to check by hand. It doesn't look at NuGet packages. Use dotnet list package --vulnerable for that.
A short version of the checklist
- Find every version pin: project files,
Directory.Build.props,global.json, Dockerfiles, compose files, CI. - Record baseline build warnings and test results before changing anything.
- Move TFMs, SDK, images and CI to 10 in one branch.
- Update Microsoft packages to 10.0.x; read what the restore audit reports.
- Migrate any in-process Functions to the isolated worker.
- Build, test, run in Docker, and check for an unexpected EF migration.
- Compare against the baseline, then deploy to a non-production slot first.
The tool is MIT-licensed and on GitHub: https://github.com/khoaanh10/dotnet-eol-scan. If it gets something wrong on your repo, an issue with a redacted snippet of the file is the most useful thing you can send.
This article was written with help from an AI assistant. I checked the commands and the scanner output by running them, and the breaking changes against the Microsoft docs linked above.
來源:DEV Community · dev.to