What end-of-life really means
When Microsoft declares a runtime “end of life,” the headlines often make it sound like your entire stack is about to collapse. In truth, the reality is less dramatic but still important to understand.
When an open-source tool like .NET has a version that reaches EOL, a few things happen:
- We get no more new features or bug fixes from Microsoft
- There are only limited (or no) security updates
- Say goodbye to official support
- It’s a risk, but not always a showstopper if your codebase is stable.
Which might lead you to the question….
Why I haven’t upgraded yet??
The first reason: dependencies.
A handful of third-party libraries my projects rely on still haven’t fully aligned with .NET 8. Even if the core framework is ready, those gaps create friction, and I’d rather wait for the ecosystem to settle.
The second issue is testing. For small projects, a simple smoke test might be enough, but for larger systems the story is different. A full regression pass across multiple services takes time, coordination, and money. Every migration cycle means rerunning that gauntlet, and unless there’s a clear business win, it can be hard to justify.
There’s also deployment inertia to think about. My work happens in a tightly controlled environment where upgrading a runtime involves policy reviews, compliance checks, and operational approvals.
Lastly, there’s the whole cost vs. ROI calculation. The performance gains and new APIs in .NET 7 and 8 are nice, but so far they haven’t tipped the scale far enough to outweigh the migration burden – at least for my current workloads.
How I “Light Up” on New Runtimes without Forking the Codebase
Instead of trying to detect “.NET version” at runtime, I treat newer features as opt-in compile-time upgrades and keep a single codebase that targets both TFMs. The idea is:
- Multi-target the project (0 and net8.0) so the compiler can include the best code path per target.
- Use #if NET8_0_OR_GREATER (or similar) to call newer APIs, and ship a polyfill or fallback for .NET 6.
- Keep runtime logging so you can confirm what actually booted in prod.
- If a dependency needs a newer version only on .NET 8, use conditional PackageReference.
Project file: multi-targeting with conditional packages
<!-- MyProject.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net6.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<!-- Example: newer package only when building for net8.0 -->
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="System.Text.Json" Version="8.0.5" />
</ItemGroup>
<!-- Older compatible package for net6.0 if you really need it -->
<ItemGroup Condition="'$(TargetFramework)' == 'net6.0'">
<PackageReference Include="System.Text.Json" Version="6.0.10" />
</ItemGroup>
</Project>
This lets you keep one solution while the compiler emits the right binaries for each runtime
A small “feature lighting” example with polyfill
Say you want to use .NET 8’s faster primitives (e.g., SearchValues<T> in System.Buffers) when available, but still run fine on .NET 6. Wrap the new API behind an adapter and provide a fallback implementation.
// IStringSearch.cs
public interface IStringSearch
{
int IndexOfAny(ReadOnlySpan<char> input, ReadOnlySpan<char> needles);
}
// StringSearch.Fast.cs (included for all TFMs, guarded inside the method)
public sealed class StringSearch : IStringSearch
{
public int IndexOfAny(ReadOnlySpan<char> input, ReadOnlySpan<char> needles)
{
#if NET8_0_OR_GREATER
// .NET 8: use SearchValues for better perf / vectorization
var sv = System.Buffers.SearchValues.Create(needles);
return input.IndexOfAny(sv);
#else
// .NET 6: simple fallback loop (kept tight and allocation-free)
for (int i = 0; i < input.Length; i++)
{
char c = input[i];
for (int j = 0; j < needles.Length; j++)
{
if (c == needles[j])
return i;
}
}
return -1;
#endif
}
}
You don’t branch on “version” at runtime. You compile the best path per TFM and ship both. The app just uses IStringSearch, and the right IL is baked in at build time.
How I mitigate the risks
Staying on an EOL runtime does carry risk, and I take that seriously.
That’s why I get extended security patching from a third-party vendor. I subscribed, connected to their patch repository, and I get new security updates for .NET 6 as soon as they’re available.
I currently use TuxCare’s Extended Lifecycle Support for Libraries – which offers long-term patching for other EOL open-source libraries.
What might finally force me off .NET 6
For now, I am protected and compliant thanks to the third-party extended patching I get. However, there are some things that might convince me to put in the time investment to deal with an upgrade to .NET 8.
Here are the triggers I’m watching out for:
- A critical, unpatchable runtime vulnerability
- Libraries dropping .NET 6 support
- Hosting platforms requiring .NET 7/8
- Policy or roadmap shifts that mandate alignment
So that’s that
Yes, you can stay on .NET 6 for pretty much as long as you want if you can keep getting patches. There are a few ways to do that, but the easiest and most affordable path is via a reliable third party.
Get End Of Life Support With Tuxcare
