Software guide

Modernizing Legacy .NET Applications Without Starting Over

Many businesses run critical software built on .NET Framework years ago. It works, but it is getting harder to hire for, patch, host and extend. A rewrite is rarely the answer. This guide shows the incremental path: what to inventory, which legacy pieces map to which modern ones, code before and after, and a phased plan that keeps the application in production the whole time.

7 min readUpdated

Level
Technical
Best for
CTOs, engineering leads and .NET developers
Assumes
Familiarity with .NET Framework applications (MVC, Web Forms or WCF), Visual Studio and IIS.

Key takeaways

  • Rewrites throw away years of encoded business rules; incremental modernization keeps them and ships value at every step.
  • Inventory project types and dependencies first. Web Forms, WCF and System.Web usage decide most of the effort.
  • Put a new ASP.NET Core app in front of the old one (YARP + System.Web adapters) and move routes across one at a time.
  • Multi-target shared libraries so old and new apps use the same business logic during the transition.
  • Target the current LTS release (.NET 10 at the time of writing); .NET 8 support ends in November 2026.

Why not just rewrite it?

A legacy application is usually ugly for a reason: it encodes years of edge cases, regulatory tweaks and customer-specific fixes that nobody fully remembers. A rewrite has to rediscover every one of them, while new features are frozen and the old system still needs support.

Know where you stand

.NET Framework 4.8.1 is still supported as part of Windows, but it receives fixes rather than new features, and many libraries now target modern .NET only. At the time of writing, .NET 10 is the current long-term support (LTS) release, supported until November 2028; .NET 8 reaches end of support in November 2026, so applications already on .NET 8 have their own upgrade to plan.

Inventory checklist and what each answer means
Look forWhy it matters
Project types: MVC, Web API, Web Forms, WCF, Windows services, WinForms/WPFMVC and Web API port with moderate changes; Web Forms and WCF need redesign or a compatibility layer.
HttpContext.Current, System.Web referencesThe most common blocker. Each use must be replaced or bridged.
NuGet packages, especially abandoned onesEach needs a modern version or a replacement.
COM, P/Invoke, System.Drawing, Windows registryWindows-only. System.Drawing.Common is only supported on Windows since .NET 6.
Data access: ADO.NET, EF6, stored proceduresEF6 runs on modern .NET, which allows a two-step move to EF Core.
Authentication: Forms auth, Windows auth, membership providersMoves to ASP.NET Core cookie auth or Identity; shared auth is needed during the transition.
Dependency health check (SDK-style projects)
dotnet list package --outdated
dotnet list package --vulnerable --include-transitive
dotnet list package --deprecated

Modern equivalents for common legacy pieces

Typical migration paths
LegacyModern pathNotes
ASP.NET MVC 5 / Web API 2ASP.NET Core MVC or minimal APIsClosest match; controllers, models and Razor views port with moderate changes.
ASP.NET Web FormsRazor Pages or BlazorNo direct port. Rebuild page by page behind a proxy while the old site keeps running.
WCF servicesCoreWCF, or gRPC / RESTCoreWCF keeps existing SOAP clients working; gRPC or REST for new clients.
Windows services.NET Worker ServiceRuns as a Windows service, a Linux systemd service or a container.
Global.asaxProgram.cs and middlewareStartup code and request pipeline move into one place.
web.config appSettingsappsettings.json, environment variables, a secret storeSame build, different settings per environment.
Entity Framework 6EF Core (or EF6 on modern .NET first)Two steps lowers risk on complex models.
Configuration, before (.NET Framework) and after (modern .NET)
// Before: web.config + static ConfigurationManager, read anywhere
var url = ConfigurationManager.AppSettings["PaymentApiUrl"];

// After: appsettings.json (or an environment variable) bound to a typed class
// appsettings.json  →  { "Payments": { "ApiUrl": "https://..." } }
public class PaymentOptions { public string ApiUrl { get; set; } = ""; }

builder.Services.Configure<PaymentOptions>(builder.Configuration.GetSection("Payments"));

public class PaymentClient(IOptions<PaymentOptions> options)
{
    private readonly string _url = options.Value.ApiUrl;
}

The incremental approach: the strangler fig pattern

Put a new ASP.NET Core application in front of the old one. It serves the routes you have migrated and forwards everything else to the legacy application, so users keep the same URLs and never see a switch. Microsoft documents this approach using the YARP reverse proxy and the System.Web adapters (the Microsoft.AspNetCore.SystemWebAdapters packages), which let the two applications share authentication and session state during the transition.

How traffic flows during an incremental migration
  1. UsersSame URLs as today
  2. ASP.NET Core appServes migrated pages and APIs
  3. YARP forwarderSends everything else on
  4. Legacy appShrinks with each release

Routes move across one at a time until the legacy app receives no traffic.

The heart of it: Program.cs in the new application
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddHttpForwarder();
builder.Services.AddSystemWebAdapters();   // shared auth/session with the legacy app

var app = builder.Build();
app.UseSystemWebAdapters();
app.MapDefaultControllerRoute();            // migrated routes are handled here...

// ...and anything not migrated yet is forwarded to the legacy application
app.MapForwarder("/{**catch-all}", app.Configuration["ProxyTo"]!);
app.Run();
Share business logic with both apps during the transition (.csproj)
<PropertyGroup>
  <TargetFrameworks>net48;net10.0</TargetFrameworks>
</PropertyGroup>

A phased plan, with a worked example

The table below is a worked example for a typical order-management system made of four projects: an MVC 5 web app, a business-logic class library, an EF6 data layer and a Windows service that runs nightly jobs.

Worked example: modernizing an order-management system
PhaseWhat changesWhat users notice
1. StabilizeSource control, a CI build, and characterization tests around order entry, invoicing and the nightly jobs.Nothing.
2. Project filesConvert all projects to SDK-style with PackageReference (the .NET Upgrade Assistant automates much of this).Nothing.
3. Shared librariesMulti-target the business library (net48;net10.0); replace abandoned packages.Nothing.
4. Background jobsMove the Windows service to a .NET Worker Service, deployed as a container.Nightly jobs run as before, now monitored.
5. Web, route by routeAdd the ASP.NET Core front app with YARP; move the order search API first, then reports, then order entry.Faster pages, one area at a time.
6. Data accessMove the data layer from EF6 to EF Core, table group by table group.Nothing, if tests hold.
7. RetireRemove the legacy app once its traffic reaches zero.Nothing; that is the point.

Pitfalls that catch most teams

  • HttpContext.Current everywhere. Static access to the request is common in older code; inject IHttpContextAccessor or pass what is needed explicitly.
  • Windows-only APIs. Image processing with System.Drawing, registry access and COM objects need replacements before the app can run on Linux.
  • Time zones and culture. Time zone IDs differ between Windows and Linux, and default culture can differ between servers; set both explicitly.
  • Authentication during the transition. Plan shared cookies or session from day one, or users will be asked to sign in twice.
  • Changing behavior by accident. Characterization tests that pin today's outputs (even odd ones) are what make each step safe.

Moving toward the cloud

Once the application runs on modern .NET it can run on Linux and in containers, which opens simpler and often cheaper hosting: Amazon ECS with Fargate, Azure App Service or Kubernetes. Add structured logging and tracing (for example OpenTelemetry), externalize configuration and secrets, and keep a modular monolith rather than splitting into microservices too early. Our AWS cloud migration guide covers the infrastructure side of the move.

Frequently asked questions

Do we have to leave .NET Framework now?

Not immediately; .NET Framework 4.8.1 is supported as part of Windows. But it gets no new features, more libraries target modern .NET only, and hiring for it is getting harder, so planning the move now avoids a rushed one later.

Can we keep releasing features during modernization?

Yes. The incremental approach keeps the application in production; new features can be built in the new code while older areas are migrated behind the proxy.

What about ASP.NET Web Forms?

Web Forms has no direct equivalent in modern .NET. Pages are rebuilt in Razor Pages or Blazor, usually one area at a time behind YARP, so users see a gradual improvement rather than a big switch.

Can a modernized .NET app run on Linux or in containers?

Yes. Modern .NET runs on Linux, macOS and Windows and suits containers well. Components that depend on Windows-only APIs need replacing, or can stay on Windows hosts until they are.

How long does modernization take?

Class libraries and an MVC or Web API application can often be moved in weeks. Large Web Forms or WCF systems are typically modernized in phases over several months, with value delivered at each phase.

How CodeMutual can help

Discuss your project with CodeMutual

Tell us what you're working on. We'll come back with a plain-English plan within one business day, and you'll talk directly to the engineers who would do the work.