.NET 8 reaches end of support on 10 November 2026. If your strangler migration started in 2024 or 2025, every slice you have shipped so far is almost certainly on it, and so is the test harness, the routing shim and the mail relay. The legacy half of the system is the part everyone worries about; this is the part that quietly became a deadline while you were busy with it.
.NET 10 is the current LTS release. It shipped on 11 November 2025 and is supported until 14 November 2028. .NET 9 was a standard-term release and went out of support in May 2026, so there is no intermediate stop worth making. The move is .NET 8 to .NET 10, and for most migration-era code it is a small change. This tutorial is how to do it without pausing the migration or losing the parity evidence you have built up.
Check the official support policy before you plan dates; the support table is the authority, not this page.
Decide the order first
Do not upgrade everything in one branch. The order that keeps you safe is:
- The characterization-test harness.
- The routing shim.
- One low-traffic migrated slice.
- The rest of the slices.
- New slices start on .NET 10 from the first commit.
The harness goes first because it is the instrument. If you upgrade a slice and the harness at the same time and something goes red, you cannot tell which change caused it. The harness has no production traffic and the blast radius is a failed build.
Upgrade the harness
Install the .NET 10 SDK alongside the .NET 8 one; they coexist. Then, in the harness project:
<TargetFramework>net10.0</TargetFramework>
Run it against the unchanged legacy system and the unchanged .NET 8 slices:
dotnet test
You want green with zero approved-file edits. If a golden-master file now differs, the SDK changed something in how your harness serializes or formats, not something in the application. Common culprits are JsonSerializer defaults, DateTime round-tripping and culture-sensitive formatting in your normalizer. Pin the behavior in the harness rather than editing the approved file:
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.GetCultureInfo("en-US");
An approved file edited during a framework upgrade is a recorded behavior you stopped protecting. If you must edit one, note it in the migration log with the reason.
Upgrade one slice
Per project, the mechanical part is three lines and a package pass:
<TargetFramework>net10.0</TargetFramework>
dotnet list package --outdated
dotnet list package --deprecated
dotnet build -warnaserror
Bump Microsoft.* packages to their 10.x versions as a group. Mixing an 8.x Microsoft.EntityFrameworkCore with a 10.x Microsoft.Extensions.* graph usually restores and then fails at runtime, which is the worst place to find out.
Read the build warnings rather than suppressing them. The ones that matter in migration-era code:
- Obsoleted members. Several APIs that were warnings in .NET 8 are errors or removed later.
BinaryFormatteris the one that catches legacy ports most often, because it is what the original app used to stash objects in session or in a cache table. If you carried that forward, this is the forcing function to replace it; see the BinaryFormatter migration guidance. - Nullable annotations. Framework annotations get more precise each release. New warnings here are information about your code, not noise.
- Analyzer severity changes. If you build with
-warnaserrorin CI, expect one cleanup commit.
Then run the harness against the upgraded slice with nothing else changed:
CHARACTERIZATION_BASE_URL=https://slice-10.internal/ dotnet test
Update the servers before you deploy
The runtime upgrade is a server change, not just a build change. For a slice hosted on IIS next to the legacy app, install the .NET 10 hosting bundle on the web servers and recycle. The hosting bundle is backward compatible: a .NET 8 app keeps running after you install it, which is what lets you upgrade servers ahead of applications instead of in the same window.
Check the supported-OS list before you schedule anything. Each .NET release drops some older Windows and Linux versions, and a legacy estate often has a 2012 R2 or 2016 box that the current release does not support. If that is where your slice runs, the server move is the project and the framework bump is a detail of it.
For containers, change the base image tags together:
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
What this does not touch
The legacy half is unaffected. .NET Framework 4.8 is a component of Windows and is supported on the same lifecycle as the OS; it is not what is expiring in November. Your VB6 executables, Classic ASP pages and Access front ends are also unaffected. Nothing about this upgrade is a reason to accelerate a slice that is not ready.
Two practical notes:
- COM interop and 32-bit shims. If a slice reaches a 32-bit legacy dependency through an out-of-process host, upgrade the host and the caller in the same change and re-run the integration tests. Process boundaries hide version mismatches until load.
- Deployment artifacts. Self-contained publishes,
runtimeconfig.jsonroll-forward settings and any pinned SDK inglobal.jsonall need the same pass. A staleglobal.jsonwill quietly keep building on .NET 8 while you believe you have moved.
Budget it honestly
For a typical migration-era codebase, three to six slices plus harness and shim, this is one to three days of engineering plus a deployment window per environment. It is not a modernization project and it should not be sold as one. The cost of skipping it is that your new system becomes the thing running on an unsupported runtime, which is the position you hired the migration to get out of.
If your .NET 8 slices are still in flight on 10 November, the practical answer is to finish the slice, then upgrade, and to tell whoever owns the audit finding the date you will be current. Measured, not guessed.