Most of the business applications we are asked to look after still run on .NET Framework 4.8. They work. They are supported as part of Windows and will be for years. So the question we hear is a fair one: why move at all?
Because the Framework no longer moves. It receives security fixes and nothing else. Every improvement in performance, tooling, hosting and libraries since 2019 has gone into modern .NET, and the gap widens each November. More practically, the libraries your application depends on are starting to drop Framework support, the developers you want to hire have never worked with web.config transforms, and anything new — containers, Linux hosting, the current versions of the API and AI tooling — assumes modern .NET.
None of that makes it urgent this month. It does make it inevitable, and an inevitable change is much cheaper done deliberately than in a hurry.
Why "upgrade the whole solution" goes wrong
The tempting plan is a branch, a few months of conversion work, and a single cut-over. On a solution of any size, it fails in a predictable way. The branch drifts from production while the business keeps asking for changes. The conversion uncovers dependencies nobody knew about. The cut-over weekend arrives with hundreds of untested differences in behaviour, and the safest option on the Sunday night is to roll back.
The approach that works is the opposite: move one project at a time, keep every step in production, and never have a version of the application that cannot ship.
Step one: find out what you actually have
Before converting anything, inventory the solution. For each project we want to know three things: what it references, what references it, and which Framework-only technologies it touches. The usual suspects:
- ASP.NET Web Forms. There is no Web Forms in modern .NET and there will not be. Pages have to be rebuilt, which is why they move last.
System.Webeverywhere.HttpContext.Currentreached from a business-logic class is the single most common reason a "simple" library will not move.- WCF services. The client side is available in modern .NET. Hosting a WCF service is not, although the community CoreWCF project covers many cases.
- Other Framework-only pieces. .NET Remoting, AppDomains, Windows Workflow Foundation, some reporting components, and any third-party control suite that never released a modern version.
This inventory is usually a pleasant surprise. In a typical solution, the class libraries holding the business logic touch none of these, and they are the part that matters most.
Step two: the libraries move first
Class libraries are where to start, because they can be made to serve both worlds at once.
Convert each project to the modern SDK-style project format — a mechanical change the application does not notice. Then retarget the library at .NET Standard 2.0, or multi-target it at both net48 and a modern version. Either way, the existing Framework application keeps consuming it exactly as before, and a modern application can reference the same code.
Where a library reaches into System.Web for the current user or a session value, the fix is to pass that information in rather than reach out for it. This is good design regardless of the migration, and it is usually a smaller change than people fear: an interface for "who is the current user", implemented once for each host.
At the end of this step nothing visible has changed, and that is the point. The business logic — the part with ten years of knowledge in it — now runs on both platforms, and every subsequent step is about the thin layers on top.
Step three: new work on the new platform
Once the core libraries are shared, stop adding to the old host. New endpoints go into a modern ASP.NET Core application that references the same libraries and talks to the same database. If you have already started adding an API to the application, that API is the natural first modern project.
For the web application itself, Microsoft's own recommended route is incremental. A modern ASP.NET Core application sits in front of the old one and proxies any request it does not yet handle back to it. Routes move across one at a time. Users see a single site with a single URL, and each route can move back if something misbehaves. Shared authentication and session state between the two hosts is the fiddly part, and there are adapters for exactly this.
This is the same pattern that makes every other part of modernization safe: the old system keeps working while the new one takes over a piece at a time.
What to leave alone
Not everything should move. A Windows service that runs a nightly import, has not changed in four years and has no dependency problems can stay on the Framework indefinitely. So can an internal admin screen used twice a month. The test is simple: does this component change often, cause problems, or block something else? If none of the three, it is not where the effort should go.
Equally, resist doing the migration and a redesign in the same step. Moving a module to modern .NET and rewriting its data access at the same time means that when behaviour changes, you cannot tell which change caused it. Move it as-is, prove it behaves identically, then improve it.
The behaviour differences nobody warns you about
Most of the migration is mechanical. The risk is concentrated in a handful of quiet differences:
- Globalization. Modern .NET can use a different culture data source from the Framework, and string comparison and sorting can produce different results for some inputs. Anything that sorts names or compares codes deserves a test.
- Configuration.
web.configandapp.configgive way toappsettings.jsonand environment variables. Settings that were silently inherited frommachine.configon the server need to be found and made explicit. - Serialization defaults. Moving from Newtonsoft.Json to
System.Text.Jsonchanges casing, date handling and how unknown properties are treated. Mobile apps and partners consuming your API will notice before you do. - Entity Framework. EF6 runs on modern .NET, which means the data access layer does not have to change at the same time as the host. Moving to EF Core is a separate project with its own testing.
The defence against all four is the same: characterization tests written before the move, that record what the system does today — right or wrong — so that any difference is visible the moment it appears.
Which version to target
Target the current long-term-support release, which today is .NET 10. LTS releases are supported for three years, and moving from one modern version to the next is a small job compared with leaving the Framework — usually a target change and an afternoon of fixing warnings. The hard part is the first move. After that, staying current is routine maintenance.
What it looks like in practice
For a typical line-of-business solution, the sequence is: an inventory and a plan in the first couple of weeks; libraries converted and multi-targeted over the following month, shipping as they go; a modern host for the API and new features; and then the web front end moving route by route, at whatever pace the business is comfortable with. Web Forms pages are rebuilt when they are next due for meaningful change, not all at once.
There is no weekend where everything changes. There is a steady series of small releases, each one in production, each one reversible. A year later the application runs on modern .NET, and nobody outside the development team could point to the day it happened.
That is the goal. If you would like to see what the first two weeks of that process involve, we have written about what we look for in a legacy .NET codebase, and the full staged approach is on our modernization page.