.NET 8 and .NET 9 reached end of support on November 10th, and the same week .NET 11 launched at .NET Conf. If you spent the last few months moving apps to .NET 10, you may be tempted to ignore .NET 11 completely. I would not. Not because you need to adopt it tomorrow, but because the cheapest time to learn what an upgrade will cost is long before you decide to do it.
This post is about making that decision with evidence. Let's dive in!
First, Pick Your Release on Purpose
Microsoft's lifecycle documentation is clear about the two tracks. Even-numbered releases such as .NET 10 are Long Term Support (LTS) and are supported for a minimum of three years, or one year after the next LTS ships if that is later. Odd-numbered releases such as .NET 11 are Standard Term Support (STS) and are supported for 24 months. .NET 10 is supported until November 2028.
For most line-of-business systems, I treat that as a staffing question more than a technical one. An STS release is a fine choice for a team that upgrades every year on purpose. It is a poor choice for a team that upgrades when something forces it. Be honest about which team you are.
Remember, if you are a .NET MAUI user, you must go to .NET 11 by May of 2027 to be supported!
The Problem With "We'll Look at It Later"
The upgrade that hurts is the one you discover all at once. Package versions, analyzers, a new default language version, a hosting platform that is not ready yet. Each is small. Together, discovered at a deadline, they make for a bad week.
A canary build spreads that discovery over time. Production keeps targeting .NET 10, while a non-blocking CI job builds and tests the same code against .NET 11 on a schedule. You get a running list of what breaks, and when it stops breaking, you have your answer.
Step 1: One Property Controls the Target Framework
Instead of hard-coding the target framework in every project, define it once. In Directory.Build.props at the repository root:
<Project>
<PropertyGroup>
<!-- Production target. The canary job overrides this on the command line. -->
<AppTfm Condition="'$(AppTfm)' == ''">net10.0</AppTfm>
</PropertyGroup>
</Project>
Then each project file uses it:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>$(AppTfm)</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
Now the canary build is a command-line switch, not a branch full of edits:
dotnet build -p:AppTfm=net11.0
dotnet test --no-build -p:AppTfm=net11.0
If you reference packages whose versions differ by framework, use a Condition on the PackageReference or ItemGroup that checks $(AppTfm). Those conditions are exactly where the canary tends to find its first findings, which is the point.
Step 2: Let the SDK Pin Production, Not the Canary
A global.json that pins the SDK is good practice for repeatable builds. Microsoft's documentation shows rollForward controlling how strictly the SDK version is matched. For production builds, I like to stay inside the major version you have validated:
{
"sdk": {
"version": "10.0.100",
"rollForward": "latestFeature"
}
}
That selects 10.0.100 or a later feature band or patch for 10.0. It also means the canary job cannot use an SDK 11 with this file in place, because the file resolves to a 10.0 SDK. For the canary job, use a separate file or remove this one before building. The simplest version of the second option is a single pipeline step that deletes global.json on the canary runner only.
Step 3: A Non-Blocking Canary Job
Here is a GitHub Actions example. The canary runs weekly and on demand, and it is allowed to fail without turning your main branch red:
name: dotnet-11-canary
on:
schedule:
- cron: "0 9 * * 1"
workflow_dispatch:
jobs:
canary:
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v4
- name: Remove SDK pin for the canary only
run: rm -f global.json
- uses: actions/setup-dotnet@v4
with:
dotnet-version: 11.0.x
- name: Build against .NET 11
run: dotnet build -c Release -p:AppTfm=net11.0 -bl:canary.binlog
- name: Test against .NET 11
run: dotnet test -c Release --no-build -p:AppTfm=net11.0
- uses: actions/upload-artifact@v4
if: always()
with:
name: canary-binlog
path: canary.binlog
Check that the SDK version you request is available through your setup action before you rely on this. If you want to start before general availability reaches your tooling, the setup-dotnet action has an input for choosing the release quality. Azure DevOps and other CI systems have equivalents. The pattern matters more than the product.
The binary log is not decoration. When the canary fails, canary.binlog opening it in a log viewer is the fastest way to see which target, analyzer, or package caused it.
Step 4: Know What Changes Underneath You
The .NET 11 overview on Microsoft Learn was last updated for release candidate 2, so read the current page before you rely on specifics. Two items are worth understanding because they affect a canary build even when you did not change a line of your own code:
- C# 15 is the default language version for projects that target .NET 11. Retargeting therefore also changes your language version, which can surface new compiler diagnostics.
- Runtime Async no longer requires
<EnablePreviewFeatures>true</EnablePreviewFeatures>for projects that targetnet11.0. That is a runtime-level change, so give your async-heavy code paths real test time, not only a green build.
Treat those as prompts for focused testing, not as reasons to worry.
Step 5: Add an SDK End-of-Life Check
This month's lesson is that support dates sneak up on teams. A feature that arrived in .NET 11 Preview 5 helps with that. When the MSBuild property CheckSdkVulnerabilities is true, the build warns with NETSDK1239 if the SDK running the build is end of life. It is opt-in, and it reads a local cache of SDK release metadata that the CLI refreshes in the background at most every 24 hours, so it does not make network calls during the build.
<Project>
<PropertyGroup>
<CheckSdkVulnerabilities>true</CheckSdkVulnerabilities>
</PropertyGroup>
</Project>
Put that in Directory.Build.props next to AppTfm. Note that this warning is about the SDK that runs your build, not the framework you target. A different warning, NETSDK1138, covers a target framework that is out of support. You want to know about both.
Step 6: Check the Things the Build Cannot Tell You
A green canary does not prove you are ready. Before you move anything real, confirm:
- Your hosting platform supports the runtime you intend to run. Do not assume.
- Your container base images and build agents will have the SDK and runtime available.
- Your third-party packages, including UI component suites, have a release that supports the new target.
- Your monitoring still reports what you expect after the move.
None of that shows up in dotnet build. It shows up when someone owns the checklist.
The Decision Rule I Use
If the canary stays green for a few weeks and the checklist above is clear, you have a real choice, and you made it with data. If you decide to stay on .NET 10 LTS, you have not lost anything. You have a recorded reason and a date to revisit it. That is a decision. "We never looked" is not.
Your Next Step
This week, add the AppTfm property to one repository and run a single manual canary build with -p:AppTfm=net11.0. Do not fix anything yet. Just count what breaks, and write the number down.
If this type of process was helpful, or if you have a better process to start rolliing releases forward please share below!
Categories