.NET Framework to Modern .NET: The Migration Guide

ModernLift · ·11 min read
Part 2 of 8

Migrating a .NET Framework application to modern .NET is a platform move done in four repeating steps: assess the estate to map dependencies and the Framework-only features that cannot port directly; multi-target the shared libraries so the same code compiles for both platforms; carve out one slice — a page, an endpoint, or a service — and rebuild it on modern .NET; then prove it behaves identically to the original under shadow traffic before shifting load to it behind a facade. Repeat slice by slice until the Framework app is empty and can be retired. Avoid the big-bang rewrite, which is where most of these projects fail.

Part 1 established that moving from .NET Framework to modern .NET is a platform migration, not an upgrade. This part is the playbook for doing it — the end-to-end method that takes a running Framework application and lands it on modern .NET without a freeze, a big-bang cutover, or a bet-the-company date.

The method is not specific to .NET; it is the same incremental, parity-first discipline that works for any business-critical migration. What is specific to .NET is knowing which parts of the platform port cleanly and which have to be rebuilt — and sequencing the work so the rebuilds come after you have proven the loop on the easy parts.

Step 1: Assess the estate honestly

You cannot plan a migration you have not measured. The first job is to map what the application actually depends on, because the size of the migration is decided by its Framework-only surface, not by its line count.

The assessment answers three questions:

  • What are the dependencies, and are they portable? Every NuGet package, every referenced assembly, every external system. Modern equivalents exist for most; a handful of older packages are Framework-only and will need replacing. Microsoft’s compatibility analyzers can flag APIs that are unavailable or behave differently on modern .NET.
  • Where are the Framework-only technologies? WebForms pages (.aspx), server-side WCF services, ASMX web services, System.Web usage, WCF hosting, Windows-specific APIs, and anything bound to the old web.config/machine.config model. These are the slices that must be rebuilt, and they drive the timeline.
  • Where does the business logic actually live? In a clean app, logic sits in libraries that port easily. In a typical Framework monolith, logic is smeared through code-behind files and service implementations — entangled with the very technologies you are leaving. Untangling that is the real work.

The output is a dependency map and a slice inventory: a list of candidate slices, each tagged “ports cleanly” or “must be rebuilt,” ordered by the pressure relieving it would relieve.

Step 2: Multi-target the shared libraries

Before touching the application surface, move the foundation. Most .NET Framework solutions have class libraries holding domain models and business logic. These are the leaf dependencies everything else sits on, and they are usually the most portable code in the system.

Convert these libraries to the modern SDK-style project format and multi-target them — compile the same source for both .NET Framework and modern .NET (net48 and, say, net8.0). Now a single library builds artifacts for both platforms. The Framework app keeps using its build; new modern-.NET components consume the other. This is the bridge that lets old and new code share the same logic during the transition, instead of forking it into two diverging copies.

Where the shared .NET Standard contract covers what you need, lean on it — it was designed precisely to let one library serve both worlds.

Step 3: Stand up the facade

The mechanism that makes the migration incremental is a facade — a routing layer in front of the application that can send each request to either the Framework app or a new modern-.NET component. Early on it routes everything to Framework. As each slice is rebuilt and proven, you flip its route to the new component. This is the strangler fig pattern applied to a platform move, and it is what turns one terrifying cutover into a long series of small, reversible ones.

The facade also hosts the anti-corruption layer between the two worlds — translating between the legacy app’s shapes and the new component’s clean contracts, so the new code is not infected by the old model’s quirks. That boundary is worth getting right; it is detailed in the anti-corruption layer part of the strangler-fig series.

Step 4: Migrate one slice, prove it, cut it over

With libraries multi-targeted and a facade in place, the core loop begins. For each slice:

  1. Carve it out. Pick one unit of behavior — a page, an API endpoint, a service operation — small enough to reason about end to end.
  2. Rebuild it on modern .NET. A WebForms page becomes a Blazor component or a Razor Page; a WCF operation becomes a gRPC or REST endpoint; the underlying logic comes from the multi-targeted library where possible. (Parts 3 through 5 cover these specific rebuilds.)
  3. Prove parity. Run the new slice in shadow mode against live traffic — the same requests hit both the Framework path and the new one, and you compare outputs, data effects, and edge cases. The slice is not promoted until it matches. This is the discipline of parity validation: equivalence is demonstrated, not assumed.
  4. Cut over behind the facade. Shift the slice’s traffic to the modern component. If anything diverges, the route flips back in seconds. Once it is stable, the Framework version of that slice is decommissioned.

Then the next slice. Each one ships independently, the business runs throughout, and being wrong about a slice is a contained, reversible event rather than a catastrophe.

What ports cleanly, and what does not

A realistic guide saves you from the trap of assuming everything recompiles:

  • Ports with modest change: domain logic in class libraries, most data-access code (Entity Framework Core is the modern target), business rules, and anything already expressed against the shared API surface.
  • Ports with rework: configuration (the web.config model gives way to the modern configuration and dependency-injection system), logging, and authentication/authorization, which use different abstractions on modern .NET.
  • Must be rebuilt: ASP.NET Web Forms (no forward port — Part 3), server-side WCF and ASMX (replaced by gRPC, REST, or CoreWCF — Part 4), and Windows-specific integrations that have no cross-platform equivalent.

Naming the “must be rebuilt” set up front is what keeps the plan honest. These are not bugs in the migration; they are the migration.

A second thing the assessment should pin down is the target version, because modern .NET ships annually and the versions are not equal. The even-numbered releases are Long-Term Support (LTS) — .NET 8 is LTS, supported for three years from its November 2023 release — while the odd-numbered ones are Standard-Term Support (STS) with roughly eighteen months of support. For a multi-year migration of a business-critical system, target an LTS release: it gives the program a stable platform that will not fall out of support mid-flight, and it avoids the churn of chasing a new runtime every year. The cadence itself is a reason to be deliberate — .NET 6, an LTS release, already ended support on November 12, 2024 (Microsoft), so “modern .NET” is itself a moving target you should aim at carefully rather than assume is evergreen.

Why not just rewrite it onto modern .NET?

Because the rewrite is the failure mode, not the shortcut — the slowest-looking path, one proven slice at a time, is the fastest one that actually finishes. A big-bang rewrite onto modern .NET freezes the roadmap, has to reach parity with years of accumulated behavior before it can ship anything, and concentrates all the risk on one distant date. That shape is why up to 70% of digital transformations fail to deliver on their objectives (BCG, 2023). The incremental method trades the thrill of a clean slate for the certainty of continuous delivery — and the system keeps earning the whole time. The full argument is in refactoring vs rewriting.

Not every migration needs the full machinery

Some applications are small and portable enough that “convert the projects, fix what the analyzer flags, and ship” is genuinely the right call — not every .NET migration needs a facade and a parity harness. The incremental machinery earns its keep when the app is business-critical, when its Framework-only surface is large, or when an outage or a behavior regression would be expensive. Match the ceremony to the stakes: a small internal tool and a revenue-carrying monolith are not the same migration, and pretending they are wastes effort in one direction or risk in the other.

Where this leads

The loop above treats “rebuild the slice” as a single step, but for the two hardest .NET technologies that step is a project of its own. Part 3, Migrating ASP.NET Web Forms to Modern .NET, takes on the one with no forward port at all — the WebForms screens that so often hold the most business logic and the least documentation, and that have to be rebuilt rather than carried across.

Frequently asked questions

Can you upgrade .NET Framework to .NET 8 in place?
Not as a single in-place upgrade for any non-trivial app. Modern .NET is a different runtime, not a higher version number, so there is no "change the target and recompile" path when the code touches WebForms, server-side WCF, System.Web, or other Framework-only surfaces. What you can do is convert the project files to the modern SDK format, multi-target shared libraries so they build for both platforms, and then migrate the application one slice at a time. The Upgrade Assistant automates parts of the mechanical conversion, but the architectural moves are manual.
How long does a .NET Framework migration take?
It depends almost entirely on how much of the app uses Framework-only technologies and how tangled the codebase is — not on raw lines of code. An app that is mostly portable class libraries with a thin web layer can move quickly; a WebForms-and-WCF monolith with logic spread through the UI takes far longer because those layers must be rebuilt, not ported. The incremental approach changes the question from "how long until it is all done" to "how soon does the first slice ship," which is usually weeks, with value landing continuously after that.
What is the safest order to migrate a .NET app?
Move the leaf dependencies first — the shared libraries and domain logic that everything else depends on — by multi-targeting them so they run on both platforms. Then migrate the application surface inward from there, one slice at a time, prioritizing slices that relieve a real pressure (a hot path, a blocked dependency, a high-risk component) over slices that are merely easy. Leave the hardest Framework-only pieces — WebForms screens, WCF hosts — until you have proven the loop on simpler slices and built the facade that lets old and new run side by side.
All 8 parts of .NET Legacy Modernization →