Modernizing a .NET Framework application means moving it onto current .NET, where it gets a supported runtime, cross-platform hosting, and every performance and language improvement shipped since .NET Core 1.0, without stopping the business for the months or years a full rewrite would take. This guide is for architects and senior engineers planning that move for a real production system: how to assess what you have, run the .NET Framework and modern .NET versions side by side while you migrate, replace WCF, Web Forms and EF6 piece by piece, and roll the change out safely. .NET Framework itself is not going away on any announced schedule, but it receives no new features, and the .NET ecosystem, tooling and hiring pool have moved on, which is the real cost of staying behind.
What Does Modernizing a .NET Framework Application Involve?#
Modernization is rarely one migration; it is several, and they do not all have to happen at once. A typical .NET Framework application bundles at least four separate concerns that each need their own plan: the web framework (ASP.NET MVC or Web Forms), any service layer (WCF), data access (often EF6 or raw ADO.NET), and a set of Windows-only or IIS-only dependencies that snuck in over the years. Treating this as one big-bang rewrite is the single biggest reason these projects fail; treating it as a sequence of independently shippable steps, each one leaving the application in a working, deployable state, is what makes modernization something you can actually finish.
Assessment and Planning: Where to Start#
Before changing code, build an honest inventory. List every NuGet package and check it against current .NET, most packages published in the last several years already support it, and the ones that do not are your first real blockers. Search the codebase for the Windows-only APIs covered later in this guide, and for every WCF service, Web Forms page and EF6 DbContext, since each one becomes its own migration task. Then rank the results by two independent axes: how much business risk a piece carries if it breaks, and how much technical effort it will take to move, and start with the pieces that are high value and comparatively low risk, not necessarily the ones that are technically easiest.
Pick a strategy before you pick a first target. An in-place upgrade retargets the existing project directly and works well for a library or a small service with few Windows-only dependencies. A strangler fig migration runs the old and new applications side by side behind a reverse proxy, moving one route or one feature at a time, and is the right default for anything large enough that a single cutover is too risky. A full rewrite is rarely worth it purely for the .NET Framework problem; reserve it for cases where the existing design is the real problem and the framework version is incidental.
Getting Started: The Strangler Fig with System.Web Adapters and YARP#
Microsoft's Microsoft.AspNetCore.SystemWebAdapters packages map System.Web APIs, HttpContext, session state, and related types, onto ASP.NET Core's equivalents, so pages and handlers that still depend on System.Web can run inside a modern ASP.NET Core host with minimal changes. Register them in the new application:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSystemWebAdapters();
var app = builder.Build();
app.UseSystemWebAdapters();
app.MapControllers();
app.Run();The adapters also support a remote app mode, in which the new ASP.NET Core application can call back into the still-running .NET Framework application for session state or other server-only resources it has not migrated yet, so a route can move to the new app before every one of its dependencies has.
In front of both applications, YARP becomes the strangler fig: it routes each request to whichever application currently owns that path, so you migrate one route at a time and nothing else changes for the caller.
{
"ReverseProxy": {
"Routes": {
"new-checkout": {
"ClusterId": "modern-app",
"Match": { "Path": "/checkout/{**catch-all}" }
},
"everything-else": {
"ClusterId": "legacy-app",
"Match": { "Path": "/{**catch-all}" }
}
},
"Clusters": {
"modern-app": { "Destinations": { "d1": { "Address": "https://modern-app.internal" } } },
"legacy-app": { "Destinations": { "d1": { "Address": "https://legacy-app.internal" } } }
}
}
}As each feature moves, add a more specific route ahead of the catch-all and delete the corresponding page from the legacy application once it is no longer receiving traffic.
AI-Assisted Modernization Tooling#
Microsoft's tooling in this space has moved fast and changed names more than once, so verify the current project before following a specific command. The original, standalone .NET Upgrade Assistant CLI has given way to an AI-powered agent that plugs into GitHub Copilot: it analyzes a solution, proposes a migration plan, and carries out changes such as retargeting projects and fixing APIs that no longer exist, rather than only flagging them for you to fix by hand. It is available as a Visual Studio Code extension, a GitHub Copilot CLI plugin, a Visual Studio context-menu command, and through the GitHub Copilot coding agent, and it requires an active GitHub Copilot subscription. Treat its output the way you would a junior engineer's pull request: a useful first pass through mechanical changes such as namespace and package updates, reviewed and tested before it merges, not a substitute for the planning and business-logic decisions covered in the rest of this guide.
Windows-Only Dependencies: What Blocks a Move to Modern .NET#
A handful of APIs are the usual reason a project cannot retarget cleanly. System.Drawing.Common has been Windows-only since .NET 6 and throws on other platforms; replace GDI+-based image processing with a cross-platform library such as ImageSharp or SkiaSharp. COM interop, System.DirectoryServices and integrated Windows authentication are inherently Windows-specific and either stay that way or need a redesign around a cross-platform identity provider. System.Configuration.ConfigurationManager-style XML configuration still works through a compatibility package, but new code should move to IConfiguration and appsettings.json. When some of these dependencies cannot be removed yet, target the net10.0-windows platform-specific TFM instead of plain net10.0: it keeps Windows-only APIs compiling and running while you move off .NET Framework, and you can drop the -windows suffix later once the last dependency is gone.
Testing and Rollout Strategy#
A migration without a safety net just trades one set of unknowns for another. Before moving anything, get characterization tests around the current behavior of whatever you are about to touch, since Web Forms and WCF code in particular is often undertested and easy to change accidentally. Run the legacy and modern implementations side by side wherever you can, replaying real requests or a shadow traffic sample against both, and compare responses before cutting real users over. Roll out by route or by customer segment through the same YARP configuration used for the strangler fig, so a bad migration step affects a small, reversible slice of traffic instead of everyone at once, and keep the legacy path deployable until the new one has run cleanly in production for a full business cycle. Integration tests against the new ASP.NET Core host, run against a real or Testcontainers-hosted database rather than mocks, catch the class of bug that unit tests around individual handlers miss.
Best Practices#
- Sequence the migration by business value and risk, not by which piece is technically easiest to move first.
- Keep the application deployable after every step. A strangler fig only works if both sides can ship independently throughout the migration.
- Characterize existing behavior with tests before changing it, especially around WCF and Web Forms code that is often under-tested today.
- Treat AI-assisted modernization tools as a fast first pass, not a merge button. Review the mechanical changes they make the same way you would review a colleague's pull request.
- Move configuration and Windows-only dependencies early, even before the bigger WCF, Web Forms or EF6 work, since they block retargeting the project at all.
- Prefer CoreWCF over a gRPC rewrite when external clients cannot change on your timeline, and revisit gRPC later for the calls that are genuinely internal.
- Retire the legacy path deliberately. Set a date to delete the old code once traffic has fully moved, rather than leaving two implementations running indefinitely.
Common Pitfalls#
Trying to migrate everything in one release. The projects that succeed ship the migration in many small, low-risk steps; the ones that fail plan a single cutover months out and discover the gap between plan and reality all at once.
Underestimating Windows-only dependencies. A System.Drawing.Common call three layers deep in a rarely touched module can block an entire project from retargeting until someone finds it, often late in the effort.
Skipping characterization tests on legacy code. Web Forms and WCF code that has not changed in years is exactly the code most likely to have undocumented behavior that only a test, not a code read, will catch before it reaches production.
Rewriting WCF contracts and clients at the same time as moving the host. Changing the transport, the contract and the client all in one step multiplies the number of things that can go wrong at once; move the host to CoreWCF first, and change the contract later if you still want to.
Leaving the strangler fig in place indefinitely. A reverse proxy splitting traffic between two applications is a migration tool, not a permanent architecture; every route left unmigrated is a piece of .NET Framework still running in production.
In-Place Upgrade vs Strangler Fig vs Rewrite#
| Approach | What it means | Risk | Timeline | Best when |
|---|---|---|---|---|
| In-place upgrade | Retarget the existing project directly to modern .NET | Low per step, but blocked entirely by any incompatible dependency | Days to weeks | Libraries and small services with few Windows-only dependencies |
| Strangler fig | Run old and new side by side behind a reverse proxy, migrate route by route | Low; each step is small and reversible | Weeks to many months | Large applications where a single cutover is too risky |
| Full rewrite | Replace the application from scratch | High; a long period with no working system to compare against | Months to years | The existing design, not just the framework version, is the real problem |
Most production .NET Framework applications modernize fastest and most safely with a strangler fig: it turns one large, risky migration into many small, ordinary deployments.
Frequently Asked Questions#
Should I upgrade to modern .NET or rewrite the application?#
Upgrade whenever the existing design is sound and the problem is really the framework version; a strangler fig migration gets you onto current .NET without touching the parts of the system that already work. Reserve a rewrite for cases where the architecture itself, not just its runtime, is the actual problem, since a rewrite carries far more risk and rarely pays back the extra cost purely to solve a framework version problem.
Can I run .NET Framework and modern .NET code in the same application?#
Not in the same process, since they are different runtimes, but you can run them as two separate applications behind a reverse proxy such as YARP, which is exactly what the strangler fig pattern does. The Microsoft.AspNetCore.SystemWebAdapters packages go further and let the new ASP.NET Core application call back into the still-running .NET Framework application for state it has not migrated yet.
Is WCF completely unsupported on modern .NET?#
The System.ServiceModel client libraries for consuming a WCF service are still supported and updated for modern .NET, but the WCF service host itself does not run there. CoreWCF is the community and Microsoft-backed port of the WCF service side, letting an existing SOAP service and its clients keep working while the host moves to current .NET; gRPC is the better long-term target only once you control every client.
Do I have to migrate EF6 and my data layer before moving the web framework?#
No, and in a strangler fig they usually move independently. A page or service can move to modern .NET while its DbContext briefly stays on EF6 in a bridging layer, or the data access for one bounded piece of functionality can move to EF Core first while the rest of the application keeps using EF6, as long as both share the same database safely during the transition.
What is the first thing I should modernize?#
Start with an inventory, not a migration: list every NuGet package, Windows-only API, WCF service and Web Forms page, and rank them by business value and technical risk. The first piece you actually move should be small, low-risk, and end-to-end, proving out your strangler fig routing and rollout process before you commit to it for the parts of the system that matter most.
Summary#
- Split modernization into independent tracks, web framework, services, data access, Windows-only dependencies, and sequence them by business value and risk rather than attempting one cutover.
- A strangler fig, System.Web adapters on the ASP.NET Core side and YARP in front of both applications, lets old and new code run side by side and migrate route by route.
- AI-assisted modernization tooling can automate the mechanical parts of an upgrade, but the tools and their names change quickly, so verify the current one and review its changes like any other pull request.
- Move CoreWCF or gRPC, Blazor or Razor Pages, and EF Core in independently deployable steps, and target
net10.0-windowsas an interim step when a Windows-only dependency cannot be removed yet. - Characterize existing behavior with tests before you touch it, and roll out gradually through the same routing layer that powers the strangler fig.