GDPR Compliance

We use cookies to ensure you get the best experience on our website. By continuing to use our site, you accept our use of cookies, privacy policy and terms of service.

Technical guide

.NET 8 and .NET 9 end of support: what actually breaks

Both versions stop receiving security updates on November 10, 2026. For a well-maintained web API the upgrade to .NET 10 is an afternoon. The risk is concentrated in about a dozen specific places, and in the things that quietly share the same date.

Craftware Technologies · Last updated September 15, 2026 · 11-minute read

Support endsNovember 10, 2026.NET 8 (LTS) and .NET 9 (STS)
Only supported target.NET 10 (LTS)Supported through November 14, 2028
Not affected.NET Framework 4.8Follows the Windows OS lifecycle

The date, and what it does not mean

.NET 8 (LTS) and .NET 9 (STS) both reach end of support on November 10, 2026. After that date Microsoft issues no further security updates for either version and provides no technical support for applications running on them. .NET 10, an LTS release, is supported through November 14, 2028 and is the only current upgrade target.

What end of support does not mean: your application does not stop. Nothing expires, no license check fails, no service refuses to start. A .NET 8 app on November 11, 2026 runs exactly as it did the day before. What changes is that the next vulnerability disclosed in the runtime, in ASP.NET Core, or in any of the framework libraries will not be patched for you, and your hosting provider, your auditor, and your customer's security questionnaire will all start treating the estate differently within a quarter or two.

This is not an outage to be avoided. It is a patch pipeline going dark, so the honest question is not "will we make it" but "what are we still running on an unpatched runtime in January, and who decided that was acceptable."

The version confusion that costs people the most time

If it targets .NET Framework 4.6.2, 4.7.2, 4.8 or 4.8.1, this deadline does not apply to it.

.NET Framework has been a component of the Windows operating system since 4.5.2, and components inherit the support lifecycle of the parent product. .NET Framework 4.8 and 4.8.1 are supported for as long as the Windows version they are installed on is supported. There is no November 2026 cliff for those applications.

The two estates get conflated constantly, and the consequence runs both ways: teams plan a .NET Framework modernization that was never urgent and miss the .NET 8 service that was. The inventory question is not "is this legacy." It is "what does the TargetFramework element say."

A .NET Framework to .NET 10 move is a genuine re-platforming project, with real dead ends: ASP.NET Web Forms has no forward path, server-side WCF means adopting CoreWCF, Windows Workflow means CoreWF, and anything sitting on System.Web, AppDomains or .NET Remoting needs redesign rather than porting. That work is worth doing. It is not the same decision as this one and should not be funded out of the same panic.

What to check first

Half a day of inventory tells you whether this is a sprint or a project. In rough order of how often each one changes the estimate:

  1. Count target frameworks, not applications. Search the repository for <TargetFramework> and <TargetFrameworks> rather than working from memory or a spreadsheet. The ones that get missed are consistently the same: test projects, source generators and analyzers, build and deployment tooling, worker services, Azure Functions apps, and anything with a Dockerfile that pins an SDK image tag.
  2. Check the build machine before the code. Targeting net10.0 is officially supported only in Visual Studio 18.0 (2026) and later. Visual Studio 2022 17.14 will produce warning NETSDK1233: Targeting .NET 10.0 or higher in Visual Studio 2022 17.14 is not supported. The 10.0.100 SDK still lists 17.14 as its minimum, but 10.0.200 raises that to 18.0, so the reprieve is short. Your CI agents, your container SDK images and every developer machine need the .NET 10 SDK, and in a regulated or image-locked environment that provisioning request has its own lead time. Teams discover this on the last weekend, which is the worst possible time.
  3. Triage dependencies before touching your own code. Run dotnet list package --outdated, then --deprecated, then --vulnerable. Upgrades are far more often blocked by one unmaintained package than by anything the compiler objects to. Sort the output into three piles: has a .NET 10 build, needs a major version bump with its own breaking changes, has no maintained build at all. The third pile is your actual project plan.
  4. Read the .NET 9 breaking changes even though you are skipping .NET 9. Going from 8 to 10 in one step is the right move, and it is also how the most expensive surprises get through. You inherit every behavioral change from both releases, and .NET 9's are the ones that fail at runtime rather than at compile time.
  5. Confirm the deployment target accepts .NET 10. The ASP.NET Core Module itself is forward and backward compatible across in-support releases, but the Hosting Bundle also installs the runtime and the ASP.NET Core shared framework, so every IIS server running framework-dependent apps needs the .NET 10 bundle (then net stop was /y and net start w3svc). Container base images changed distribution (see below). Check the supported operating system list against what you actually run: Alpine 3.20, openSUSE Leap 15.6 and SUSE Linux Enterprise 15.6 are out of support for .NET 10, while Windows Server 2016 through 2025 remain supported.

What actually breaks

The build stops before the code does

  • NU1015: a PackageReference with no version is now an error. It was the NU1604 warning before, and restore quietly picked the lowest available version, which was almost never what anyone wanted. Specify versions; the escape hatch is <SdkAnalysisLevel>9.0.300</SdkAnalysisLevel>, which reverts every behavior gated on that switch and should be temporary.
  • HTTP feeds fail in more places. Non-HTTPS package sources have been error NU1302 since the .NET 9 SDK. .NET 10 widens it: http:// audit sources now raise NU1302 as well, and HTTP warnings are promoted to errors in dotnet package list, dotnet package search, and package push and delete. Internal feeds on plain HTTP are common enough that this catches a lot of corporate builds. Move the feed to HTTPS, or set allowInsecureConnections to true on the source in nuget.config while you do.
  • NU1510 is a warning raised when a project targeting net10.0 or later has a direct PackageReference that overlaps a library the framework already provides, and NuGet prunes it.
  • dotnet restore audits transitive packages. NuGetAuditMode now defaults to all rather than direct for projects targeting .NET 10 or later, so a vulnerability three levels down in a dependency you have never heard of can fail a build that passed yesterday. <NuGetAuditMode>direct</NuGetAuditMode> puts it back.

None of this is difficult. All of it is unbudgeted, and it lands in CI rather than on a developer's machine.

Serialization, which fails quietly

  • The in-box BinaryFormatter is gone as of .NET 9. The APIs remain, but the implementation always throws PlatformNotSupportedException, and setting EnableUnsafeBinaryFormatterSerialization is no longer sufficient on its own. Anything that persisted objects with it (legacy session state, a distributed cache, an on-disk queue, a column full of serialized blobs) fails on first use. The only way back is the unsupported System.Runtime.Serialization.Formatters package, and treating that as a destination rather than a bridge is how this becomes next year's problem.
  • System.Text.Json now validates property name conflicts. A type with a property named $type, $id, $ref or a configured type discriminator name now throws InvalidOperationException at serialization time rather than emitting invalid JSON with duplicate keys. Rename the property, or mark it [JsonIgnore].
  • XmlSerializer no longer ignores properties marked [Obsolete]. Properties deliberately hidden from the wire format come back, so if you have integration partners consuming that XML the contract changes underneath you. [Obsolete(IsError = true)] now throws during serializer creation, and AppContext.SetSwitch("Switch.System.Xml.IgnoreObsoleteMembers", true) restores the old behavior.

These compile cleanly and fail with production data, which is what makes them the expensive ones.

Data access

  • SQL parameter names are simplified, so @__city_0 becomes @city. Snapshot tests, query interceptors and anything parsing parameter names will need updating, and cached plans recompile on first run.
  • Application Name is injected into the connection string, which can split connection pooling if other data access in the same process uses a different value, and can escalate a TransactionScope to a distributed transaction. Set Application Name explicitly to prevent it.
  • Parameterized collections now use multiple parameters by default, which changes query plans. EF.Constant(), EF.Parameter(), EF.MultipleParameters() and the global UseParameterizedCollectionMode give you the old shape back where a plan regression matters.
  • On Azure SQL and at compatibility level 170 or higher, the native json type becomes the default for JSON columns that were nvarchar(max). Pin it with HasColumnType("nvarchar(max)") or UseCompatibilityLevel(160) if you are not ready, and note that SQL Server cannot run DISTINCT over JSON arrays, so some queries fail outright after the switch.
  • EF tools now require --framework on multi-targeted projects, which breaks migration scripts in CI.

HTTP behavior

  • HttpClientFactory uses SocketsHttpHandler as its primary handler as of .NET 9. Proxy resolution, certificate handling and connection lifetime behave differently, which is exactly the class of change that passes every test and then behaves oddly behind a corporate proxy. Code that casts the primary handler to HttpClientHandler now throws InvalidCastException.
  • Header values and URI query strings are redacted in HttpClient logging by default. Nothing breaks, but the logs you have been debugging with go quiet at the least convenient moment.
  • MailAddress rejects consecutive dots, so addresses that used to validate now do not.
  • HTTP/3 is disabled by default when PublishTrimmed or PublishAot is set. Re-enable it with <Http3Support>true</Http3Support>.

Where it runs

  • Default container tags are Ubuntu 24.04, and Debian images are not shipped for .NET 10 at all. If your Dockerfile installs packages, assumes paths, or depends on Debian-specific behavior, it needs revisiting. Microsoft's reasoning is support alignment: the Ubuntu base outlives the .NET release, while Debian's does not.
  • Non-root and port 8080. The app user (UID 1654) and ASPNETCORE_HTTP_PORTS defaulting to 8080 both arrived in .NET 8, and anyone coming from .NET Framework or an older container setup meets them now. The user is opt-in, so images still run as root unless the Dockerfile adds USER app; the port default is not. Port mappings, Kubernetes manifests, health probes and WEBSITES_PORT on Azure App Service all have to agree.

Desktop and Blazor

  • WPF: empty ColumnDefinitions and RowDefinitions elements now fail the XAML compile with MC3063, with no opt-out. A markup file that has been fine for a decade breaks the build.
  • Windows Forms: .NET 9 added security analyzers, of which WFO1000 is an error by default and breaks builds of custom controls that do not declare serialization intent. .NET 10 adds obsoletions WFDEV004, WFDEV005 and WFDEV006, which stay warnings unless you treat warnings as errors.
  • Blazor WebAssembly: blazor.boot.json is now inlined into dotnet.js. Any deployment that referenced it directly for integrity checks, CDN rules or assembly renaming needs another approach. BlazorCacheBootResources is gone, and the Blazor-Environment header is replaced by WasmApplicationEnvironmentName.
  • ASP.NET Core: MapStaticAssets() arrived in .NET 9 and fingerprints and pre-compresses app-owned assets at build time, changing cache behavior and asset URLs. It complements UseStaticFiles() rather than replacing it: default documents, files outside the web root, directory browsing and custom response headers still need the middleware, and dropping it returns 404 for /.

What will not move by November 10

Some of this is not a runtime upgrade at all, and pretending otherwise is how a two-week job becomes a two-month one.

Azure Functions on the in-process model Same date

This is the big one, and it shares the date exactly: the in-process model for .NET Azure Functions retires on November 10, 2026. Moving to the isolated worker model is a rewrite of every function, not a version bump. [FunctionName] becomes [Function], the injected ILogger parameter becomes constructor injection, bindings gain suffixes and output bindings move to return types ([Blob] becomes [BlobOutput], CosmosDB becomes CosmosDBInput), Startup becomes Program.cs, and FUNCTIONS_WORKER_RUNTIME becomes dotnet-isolated. If you have a dozen function apps, count them this week rather than in October, and move them straight to .NET 10 so you are not doing it twice.

PowerShell 7.4 Same date

It is built on .NET 8 and its support ends the same day, November 10, 2026. PowerShell's lifecycle follows the .NET version it was built on. If you ship modules, run automation runbooks, or have deployment scripts pinned to 7.4, that is a second migration hiding behind the first. PowerShell 7.6 is built on .NET 10 and is supported to November 14, 2028.

Packages with no maintained .NET 10 build

Four options, worth choosing deliberately: replace the package, fork and retarget it, isolate it behind an interface so the replacement is a contained change later, or accept it and record the risk. What does not work is discovering in week eight that the upgrade is blocked on an abandoned library, because by then the decision gets made under time pressure and it is usually the wrong one.

Things you do not control

A vendor product certified only to .NET 8. A hosting provider without a .NET 10 runtime. A locked build image with its own change calendar. Write each one down as an accepted risk with a named owner, a review date, and a compensating control (network isolation, WAF coverage, accelerated monitoring of .NET security advisories) rather than reporting the estate as done.

The .NET Framework estate

Worth repeating, because it is the most common planning error of this cycle: it is not on this clock. Scope it separately, on its own merits, with its own budget.

A sequence that fits the time left

Roughly eight weeks remain. That is enough for most estates if the order is right.

01

Inventory and tooling

Every target framework, every deployment target, the SDK on the build agents. Output: a list of applications with an owner against each.

02

Dependency triage

The three piles. Output: the things that are not a simple bump, which is the only part of this carrying real schedule risk.

03

Bump and compile

One branch per application, net10.0, packages to 10.0.x, fix what the compiler says. For a maintained web API this is often a single afternoon.

04

Test what does not compile-fail

Serialization round trips against real persisted data, HTTP calls through the actual proxy, EF query plans at production volumes, container startup as the non-root user on the new base image.

05

Deploy in the order you can afford to be wrong in

Internal tools first, customer-facing last.

The .NET Upgrade Assistant handles step 3 well and will save you an hour or two per project. It does not find behavioral changes, which is where all the risk in steps 2 and 4 lives. Use it, and do not mistake a green run for a finished migration.

The applications that will still be on .NET 8 in January are not the hard ones. They are the ones nobody inventoried in September.

Craftware Technologies builds and modernizes custom .NET and Azure software from Sugar Land, Texas, with senior engineers in US Central time. If you are working through an upgrade and want a second pair of eyes on the dependency triage, sales@craftwaretech.com reaches us.