On November 10, 2026, .NET 8 and .NET 9 reach end of support on the same day. .NET 8 is the long-term support (LTS) release from November 2023. .NET 9 is a standard-term release, which Microsoft now supports for 24 months, so it ends together with .NET 8. After that date neither gets security fixes.
The destination is .NET 10, the LTS release that shipped in November 2025 and is supported until November 2028. If you’re on .NET 8 there’s no reason to stop at .NET 9: go straight to 10. This is the checklist I follow for a typical ASP.NET Core service, from the project file to the container image.
1. Install the SDK and pin it
Install the .NET 10 SDK, then make the repository say which SDK it expects. A global.json at the root keeps every machine and every CI run on .NET 10 SDKs while still picking up patch and feature-band releases (10.0.1xx, 10.0.2xx, …); use "latestPatch" to stay on one feature band:
{
"sdk": {
"version": "10.0.100",
"rollForward": "latestFeature"
}
}
With latestFeature, any installed 10.0 SDK at or above 10.0.100 is accepted, but a newer major version is not.
2. Change the target framework in one place
If your solution has a Directory.Build.props, the upgrade is a one-line change for every project. If it doesn’t, this is a good moment to add one:
<Project>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
Projects that target net10.0 compile as C# 14 by default, so there’s no LangVersion to bump.
3. Update the packages that follow the runtime
Packages such as Microsoft.EntityFrameworkCore.*, Microsoft.AspNetCore.* and most Microsoft.Extensions.* are versioned with .NET, so the 8.x versions should move to 10.x together. The SDK now has noun-first commands, and this one lists what’s behind:
dotnet package list --outdated
Two things tend to surprise people at this step:
- Restore now audits transitive packages. For projects targeting .NET 10,
NuGetAuditModedefaults toall, so a vulnerable package deep in your dependency tree produces anNU1901–NU1904warning. WithTreatWarningsAsErrors, that fails the restore. Rundotnet nuget why <project> <package>to see which top-level package brings it in, and upgrade that one. - LINQ for
IAsyncEnumerableis now built in. .NET 10 shipsSystem.Linq.AsyncEnumerable, which clashes with the communitySystem.Linq.Asyncpackage. Remove that package reference (or move to its version 7), and replace calls such asSelectAwaitwith the newSelectoverloads.
4. Test the behaviour changes, not just the build
A clean build isn’t the finish line. Going from 8 to 10 means taking the .NET 9 and .NET 10 breaking changes together. These are the ones most likely to affect a web API or a worker service:
BackgroundServicestarts differently. In .NET 10, all ofExecuteAsyncruns on a background thread. Before, the code up to the firstawaitran synchronously during startup and blocked other services. If you relied on that ordering, move that work into an override ofStartAsyncand callbase.StartAsyncafter it.nullin configuration staysnull. The binder used to treat anullvalue as missing, and the JSON provider turned it into an empty string. Now"Timeout": nullbinds asnull, so check options classes that have non-null defaults.- Tracing uses the W3C propagator by default.
DistributedContextPropagator.Currentnow follows the W3C Trace Context and Baggage specifications. Baggage no longer uses the oldCorrelation-Contextheader. If an older service downstream still expects it, you can switch back withDistributedContextPropagator.CreatePreW3CPropagator()while you migrate. HttpClientFactorylogs redact header values (a .NET 9 change). If you debug integrations from logs, you’ll see*where header values used to be. To log the harmless ones again, callRedactLoggedHeaderswith an explicit list of the secret ones, per client or for all of them withConfigureHttpClientDefaults(b => b.RedactLoggedHeaders(["Authorization", "Cookie", "Set-Cookie", "X-Api-Key"])). Every header left off that list ends up in your logs.BinaryFormatteralways throws (also .NET 9). If anything still serializes with it, this is the upgrade where it finally breaks. The only way back is an unsupported compatibility package, so plan to replace it rather than re-enable it.- The runtime no longer installs its own
SIGTERMhandler. ASP.NET Core and anything on the Generic Host are unaffected, because the host registers its own. A plain console app that counted onProcessExitto clean up needs aPosixSignalRegistration.
Advertisement
5. Move the container images and the pipeline
Update the image tags in your Dockerfile from 8.0 to 10.0:
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish src/Api -c Release -o /app
FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
COPY --from=build /app .
USER $APP_UID
ENTRYPOINT ["dotnet", "Api.dll"]
USER $APP_UID runs the app as the non-root user the images have shipped since .NET 8, and a .dockerignore that excludes **/bin, **/obj and .git keeps COPY . . from dragging build output and local files into the image.
The default .NET 10 images are based on Ubuntu, and Debian images are no longer provided. Most apps won’t notice. However, a RUN apt-get install … line that depends on a Debian package name is worth testing.
In CI, let the pipeline read the SDK version from global.json, so there’s only one place to change next time. With GitHub Actions:
- uses: actions/checkout@v7
- uses: actions/setup-dotnet@v6
with:
global-json-file: global.json
- run: dotnet test
6. Adopt C# 14 where it removes code
Nothing forces you to use the new language features, but a few of them delete real boilerplate. The examples below compile and run on the .NET 10 SDK.
The field keyword refers to the compiler-generated backing field, so a property with a little logic in its setter no longer needs a private field:
public sealed class Customer
{
public required string Name { get; init; }
public required string Email
{
get;
set => field = value.Trim().ToLowerInvariant();
}
}
Null-conditional assignment allows ?. on the left of =. The right side is only evaluated when the target isn’t null:
customer?.Email = LoadEmail(); // LoadEmail() doesn't run when customer is null
Extension members add extension properties and static extensions, grouped in an extension block:
public static class StringExtensions
{
extension(string value)
{
public bool IsBlank => string.IsNullOrWhiteSpace(value);
public string Truncate(int maxLength) =>
value.Length <= maxLength ? value : value[..maxLength];
}
}
// "".IsBlank == true, "Hello, .NET 10".Truncate(5) == "Hello"
Also handy: nameof(List<>) now works with unbound generic types, and lambda parameters can take modifiers without their types, as in (text, out result) => int.TryParse(text, out result).
7. Bonus: scripts without a project
.NET 10 can run a single .cs file with dotnet run app.cs, with no .csproj. Add a shebang, make the file executable (chmod +x app.cs, with LF line endings) and it runs like any script, ./app.cs:
#!/usr/bin/env dotnet
Console.WriteLine($"Running on .NET {Environment.Version}");
File-based apps can reference packages and projects with #:package and #:project directives, and dotnet publish app.cs produces a native executable. (File-based apps publish with Native AOT by default, so that needs the native toolchain, such as clang on Linux, and AOT-compatible code; #:property PublishAot=false gives a regular publish.) They’re a good replacement for the small console projects that pile up in a tools/ folder.
The short version
- Install the .NET 10 SDK and pin it in
global.json. - Change
TargetFrameworktonet10.0, ideally inDirectory.Build.props. - Update the
Microsoft.*packages to 10.x, and deal with NuGet audit warnings andSystem.Linq.Async. - Test the behaviour changes:
BackgroundServicestartup,nullconfiguration values, W3C tracing, redacted headers,BinaryFormatterandSIGTERM. - Move the Docker images (now Ubuntu-based) and point CI at
global.json. - Adopt C# 14 where it deletes code.
Start with the service with the best test coverage, and give yourself a few weeks before November 10.


