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:
- 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. - Check the build machine before the code. Targeting
net10.0is officially supported only in Visual Studio 18.0 (2026) and later. Visual Studio 2022 17.14 will producewarning 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. - 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. - 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.
- 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 /yandnet 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: aPackageReferencewith no version is now an error. It was theNU1604warning 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
NU1302since the .NET 9 SDK. .NET 10 widens it:http://audit sources now raiseNU1302as well, and HTTP warnings are promoted to errors indotnet 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 setallowInsecureConnectionstotrueon the source innuget.configwhile you do. NU1510is a warning raised when a project targetingnet10.0or later has a directPackageReferencethat overlaps a library the framework already provides, and NuGet prunes it.dotnet restoreaudits transitive packages.NuGetAuditModenow defaults toallrather thandirectfor 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
BinaryFormatteris gone as of .NET 9. The APIs remain, but the implementation always throwsPlatformNotSupportedException, and settingEnableUnsafeBinaryFormatterSerializationis 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 unsupportedSystem.Runtime.Serialization.Formatterspackage, and treating that as a destination rather than a bridge is how this becomes next year's problem. System.Text.Jsonnow validates property name conflicts. A type with a property named$type,$id,$refor a configured type discriminator name now throwsInvalidOperationExceptionat serialization time rather than emitting invalid JSON with duplicate keys. Rename the property, or mark it[JsonIgnore].XmlSerializerno 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, andAppContext.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_0becomes@city. Snapshot tests, query interceptors and anything parsing parameter names will need updating, and cached plans recompile on first run. Application Nameis 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 aTransactionScopeto a distributed transaction. SetApplication Nameexplicitly to prevent it.- Parameterized collections now use multiple parameters by default, which changes query plans.
EF.Constant(),EF.Parameter(),EF.MultipleParameters()and the globalUseParameterizedCollectionModegive you the old shape back where a plan regression matters. - On Azure SQL and at compatibility level 170 or higher, the native
jsontype becomes the default for JSON columns that werenvarchar(max). Pin it withHasColumnType("nvarchar(max)")orUseCompatibilityLevel(160)if you are not ready, and note that SQL Server cannot runDISTINCTover JSON arrays, so some queries fail outright after the switch. - EF tools now require
--frameworkon multi-targeted projects, which breaks migration scripts in CI.
HTTP behavior
HttpClientFactoryusesSocketsHttpHandleras 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 toHttpClientHandlernow throwsInvalidCastException.- Header values and URI query strings are redacted in
HttpClientlogging by default. Nothing breaks, but the logs you have been debugging with go quiet at the least convenient moment. MailAddressrejects consecutive dots, so addresses that used to validate now do not.- HTTP/3 is disabled by default when
PublishTrimmedorPublishAotis 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
appuser (UID 1654) andASPNETCORE_HTTP_PORTSdefaulting 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 addsUSER app; the port default is not. Port mappings, Kubernetes manifests, health probes andWEBSITES_PORTon Azure App Service all have to agree.
Desktop and Blazor
- WPF: empty
ColumnDefinitionsandRowDefinitionselements now fail the XAML compile withMC3063, 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
WFO1000is an error by default and breaks builds of custom controls that do not declare serialization intent. .NET 10 adds obsoletionsWFDEV004,WFDEV005andWFDEV006, which stay warnings unless you treat warnings as errors. - Blazor WebAssembly:
blazor.boot.jsonis now inlined intodotnet.js. Any deployment that referenced it directly for integrity checks, CDN rules or assembly renaming needs another approach.BlazorCacheBootResourcesis gone, and theBlazor-Environmentheader is replaced byWasmApplicationEnvironmentName. - 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 complementsUseStaticFiles()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.
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.
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.
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.
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.
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.
Sources
Every claim on this page traces to Microsoft's own documentation.
- .NET 8 and .NET 9 end of support
- Breaking changes in .NET 10
- Breaking changes in .NET 9
- ASP.NET Core: .NET 9 to .NET 10
- ASP.NET Core: .NET 8 to .NET 9
- EF Core 10 breaking changes
- Azure Functions: in-process to isolated worker
- PowerShell support lifecycle
- .NET Framework lifecycle FAQ
- .NET SDK, MSBuild and Visual Studio versioning
- Default container tags now use Ubuntu
- .NET 10 supported operating systems