Modernizing legacy .NET Framework applications to .NET 10: a practical guide

How to move a business-critical .NET Framework system to modern .NET step by step, without stopping the business or betting everything on a rewrite.

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, BinaryFormatter and 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.
The result: you ship value every sprint, every migrated piece goes to production right away, and you can stop or slow down at any point without being left with two half-finished systems.

Step 3: Prepare the codebase on .NET Framework

Much of the risk can be removed before any code runs on modern .NET:

  1. Convert to SDK-style projects. The modern .csproj format is much shorter and works with both Framework and modern .NET targets.
  2. Move from packages.config to PackageReference.
  3. Upgrade to .NET Framework 4.7.2 or 4.8, and update NuGet packages to their latest versions that still support Framework.
  4. 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.
  5. 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 in Program.cs.
  • web.config app settings move to appsettings.json and the options pattern.
  • Third-party DI containers can usually be replaced by the built-in dependency injection.
  • HttpContext.Current goes away. Inject IHttpContextAccessor or, 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

PitfallHow to avoid it
Starting with the hardest moduleStart with something valuable but low-risk to prove the pipeline, proxying and deployment end to end.
Adding features during migrationKeep migration changes behavior-neutral and ship features separately, or regressions become impossible to trace.
BinaryFormatter and other removed APIsFind them during assessment. BinaryFormatter always throws in .NET 9 and later, so plan a serialization change early.
Assuming the database is fineOld stored procedures, triggers and implicit transactions often hold business logic. Inventory and test them too.
No rollback planWith 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