.NET Migration · 2026-05-11
.NET Framework to .NET 8 Migration: What Actually Breaks (and How to Fix It)
A migration leader's checklist: the real breakage points in .NET Framework → .NET 8 — async void patterns, System.Web dependencies, configuration, and the testing strategy that keeps the business running.
Most .NET Framework → .NET 8 migrations fail not in the compile step but in the assumptions the old code made. Here’s the breakage list I work through on every migration — with what actually breaks and the fix.
1. System.Web and the hidden ASP.NET dependency
Anything touching HttpContext.Current, System.Web.* (except System.Web.Http abstractions), or System.Web.Configuration does not compile in .NET 8. The fix is not a shim — it’s the architecture change you wanted anyway: inject IHttpContextAccessor, move session state out of the web layer, and replace HttpRuntime.Cache with IMemoryCache/IDistributedCache.
2. async void in event handlers
.NET Framework tolerated async void in ASP.NET event handlers; .NET 8’s exception handling does not — an unobserved exception in an async void takes down the request pipeline and makes the app unresponsive with no stack you can find.
Fix: convert event handlers to async Task, or catch inside and route to a logger. This is the single highest-value grep of a migration: async void → fix every hit.
3. Configuration split
ConfigurationManager.AppSettings and web.config sections die. Move to appsettings.json with IOptions<T>, and treat connection strings as first-class config with named clients. Give the config a migration window where both sources load, so the app runs during the switch.
4. The “it compiles, then times out” class
Framework apps often use Task.Run to hide synchronous calls, or synchronous .Result in web contexts. .NET 8’s thread pool and the ASP.NET Core request pipeline punish both: sync-over-async deadlocks and thread-pool starvation. Convert to true async/await end to end; measure with metrics on the request path.
5. Testing strategy that keeps the business running
The only safe migration is one where every release step is verifiable:
- Freeze a set of golden outputs (API responses, reports) from the Framework version and diff against .NET 8
- Migrate one vertical slice at a time — never the whole app in one branch
- Deploy the .NET 8 app behind a canary/flag before full cutover
What makes it go fast
A typed compat.md per vertical: every System.Web usage, every sync-over-async, every config key — logged as you find them, fixed in order. Migration is a series of small, known refactors, not a rewrite. It’s mechanical — but only if the list is complete.