Many companies still run their most important software on .NET Framework: an ASP.NET MVC portal from 2014, a Web Forms back-office app, a WCF service that "nobody touches". These systems work, and that is exactly why they are hard to change. But every year they get more expensive to host, harder to hire for, and further away from the tooling the rest of the industry uses.
This guide lays out the approach we use at GRI Consulting to bring legacy .NET applications onto modern .NET (currently .NET 10, the long-term support release) while the business keeps running.
Why modernize at all?
.NET Framework 4.8.1 is still supported as part of Windows, but it is in maintenance mode: it gets security fixes and nothing else. All of the investment Microsoft makes now goes into modern .NET. In practice, staying on Framework means:
- Higher hosting costs. You're limited to Windows servers and IIS. Modern .NET runs on Linux, in containers and on Azure App Service for Linux, often on noticeably smaller instances.
- Lost performance. Each modern .NET release brings real runtime, JIT and ASP.NET Core performance gains that a Framework app never sees.
- Shrinking ecosystem. More and more NuGet packages, SDKs and Azure libraries target modern .NET only.
- Hiring friction. Engineers want to work with current tools, and onboarding into a 2012-era codebase is slow.
Step 1: Assess before you touch anything
The biggest migration failures we see come from starting to change code before anyone understands what is in the solution. Start with an inventory:
- Project types. Class libraries, ASP.NET MVC / Web API, Web Forms, WCF, Windows services, WinForms/WPF. Each has a different path.
- Dependencies. List every NuGet package and third-party DLL, and check whether each has a modern .NET version. Abandoned dependencies are usually the real blockers.
- Framework-only APIs. Look for
System.Web, AppDomains, .NET Remoting, Code Access Security,BinaryFormatterand WCF server-side hosting. None of these exist in modern .NET in the same form. - Test coverage. Be honest about how much is covered. If the answer is "almost nothing", that changes the plan.
Microsoft's upgrade tooling, including the .NET Upgrade Assistant and the newer AI-assisted modernization features in Visual Studio and GitHub Copilot, can produce an analysis report and automate much of the mechanical work. Treat its output as a starting map, not a finished plan.
Step 2: Choose incremental over big-bang
The tempting option is to freeze the old system and rewrite it from scratch. For anything business-critical, we almost always advise against that. Rewrites take longer than estimated, the old system keeps changing underneath you, and you only find out whether the new one works on launch day.
The proven alternative is the strangler fig pattern. A new ASP.NET Core application sits in front of the legacy app and takes over one route, page or feature at a time. Anything it doesn't handle yet is proxied to the old app. Microsoft supports this directly:
- YARP (Yet Another Reverse Proxy) forwards unmigrated requests from the new ASP.NET Core app to the legacy one.
- System.Web adapters (
Microsoft.AspNetCore.SystemWebAdapters) let both apps share session and authentication, so users never notice which app served a page.
Step 3: Prepare the codebase on .NET Framework
Much of the risk can be removed before any code runs on modern .NET:
- Convert to SDK-style projects. The modern
.csprojformat is much shorter and works with both Framework and modern .NET targets. - Move from
packages.configtoPackageReference. - Upgrade to .NET Framework 4.7.2 or 4.8, and update NuGet packages to their latest versions that still support Framework.
- Retarget shared libraries to .NET Standard 2.0 (or multi-target
net48;net10.0). Then the same business logic can be used by both the old and the new application during the transition. This is the key to doing it incrementally. - Add characterization tests around the most important behavior. They record what the system does today, whether or not it's "correct", so you can tell when a migration step changes something.
Step 4: Migrate each layer
ASP.NET MVC and Web API → ASP.NET Core
This is usually the smoothest path. Controllers, views and routing map fairly directly. The main changes:
Global.asax, HTTP modules and handlers become middleware inProgram.cs.web.configapp settings move toappsettings.jsonand the options pattern.- Third-party DI containers can usually be replaced by the built-in dependency injection.
HttpContext.Currentgoes away. InjectIHttpContextAccessoror, better, pass what you need explicitly.
// Program.cs — the new home for what used to live in Global.asax and web.config
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.Configure<BillingOptions>(builder.Configuration.GetSection("Billing"));
builder.Services.AddScoped<IInvoiceService, InvoiceService>();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapDefaultControllerRoute();
app.Run();
Web Forms → Blazor, Razor Pages or React
There is no automated path for Web Forms. Microsoft never ported it to modern .NET. Each page has to be rebuilt, and that's where the strangler approach pays off most: rebuild the busiest or most-changed pages first, and leave rarely used admin screens for last. Blazor feels the most familiar to Web Forms developers because it is component-based and stays in C#. React with an ASP.NET Core API is a strong choice when you also want a modern front-end ecosystem or plan to add a mobile app later.
WCF services → CoreWCF, gRPC or REST
If many clients depend on your SOAP contracts, CoreWCF, a community-supported port of the WCF server, lets you move the service to modern .NET with few contract changes. For new or internal consumers, use gRPC for service-to-service calls or plain REST APIs for everything else.
Entity Framework 6 → EF Core
EF6 itself runs on modern .NET (6.4 and later), which gives you a useful intermediate step: migrate the application first, then the data access layer. Moving to EF Core brings better performance and LINQ translation, but watch for differences in lazy loading, query behavior and EDMX models, which EF Core doesn't support.
Windows-specific code
The Microsoft.Windows.Compatibility package restores many Windows-only APIs
(registry, event log, System.Drawing and more), which is handy for getting things
running. Isolate that code behind interfaces so you can replace it later and eventually run on Linux.
Step 5: Modernize how it runs, not just what it runs on
Retargeting the framework is half the value. The other half comes from changing how the software is delivered:
- CI/CD pipelines in GitHub Actions or Azure DevOps that build, test and deploy every change.
- Infrastructure as code (Bicep or Terraform) so environments are reproducible and reviewable.
- Managed hosting such as Azure App Service or Azure Container Apps instead of hand-maintained VMs.
- Secrets in Azure Key Vault and managed identities instead of connection strings in config files.
- Observability with OpenTelemetry and Application Insights so you can see how the new system behaves in production.
Common pitfalls
| Pitfall | How to avoid it |
|---|---|
| Starting with the hardest module | Start with something valuable but low-risk to prove the pipeline, proxying and deployment end to end. |
| Adding features during migration | Keep migration changes behavior-neutral and ship features separately, or regressions become impossible to trace. |
BinaryFormatter and other removed APIs | Find them during assessment. BinaryFormatter always throws in .NET 9 and later, so plan a serialization change early. |
| Assuming the database is fine | Old stored procedures, triggers and implicit transactions often hold business logic. Inventory and test them too. |
| No rollback plan | With the strangler approach, rollback is just routing traffic back to the legacy app. Keep that option until each piece is proven. |
How long does it take?
It depends far more on the shape of the codebase than on its size. A well-layered MVC application with decent tests can move in weeks. A large Web Forms system with business logic in code-behind files is a multi-month program. Because the incremental approach puts each piece into production as soon as it's done, you don't have to know the full timeline up front to start getting value.
Planning a .NET modernization?
We've been building and modernizing .NET systems for over a decade. If you have a .NET Framework application that needs a path forward, we're happy to take a look and give you an honest assessment.
Email davit@gri.consulting