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.
- 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.
| Look for | Why it matters |
|---|---|
| Project types: MVC, Web API, Web Forms, WCF, Windows services, WinForms/WPF | MVC and Web API port with moderate changes; Web Forms and WCF need redesign or a compatibility layer. |
HttpContext.Current, System.Web references | The most common blocker. Each use must be replaced or bridged. |
| NuGet packages, especially abandoned ones | Each needs a modern version or a replacement. |
COM, P/Invoke, System.Drawing, Windows registry | Windows-only. System.Drawing.Common is only supported on Windows since .NET 6. |
| Data access: ADO.NET, EF6, stored procedures | EF6 runs on modern .NET, which allows a two-step move to EF Core. |
| Authentication: Forms auth, Windows auth, membership providers | Moves to ASP.NET Core cookie auth or Identity; shared auth is needed during the transition. |
dotnet list package --outdated
dotnet list package --vulnerable --include-transitive
dotnet list package --deprecatedModern equivalents for common legacy pieces
| Legacy | Modern path | Notes |
|---|---|---|
| ASP.NET MVC 5 / Web API 2 | ASP.NET Core MVC or minimal APIs | Closest match; controllers, models and Razor views port with moderate changes. |
| ASP.NET Web Forms | Razor Pages or Blazor | No direct port. Rebuild page by page behind a proxy while the old site keeps running. |
| WCF services | CoreWCF, or gRPC / REST | CoreWCF keeps existing SOAP clients working; gRPC or REST for new clients. |
| Windows services | .NET Worker Service | Runs as a Windows service, a Linux systemd service or a container. |
| Global.asax | Program.cs and middleware | Startup code and request pipeline move into one place. |
| web.config appSettings | appsettings.json, environment variables, a secret store | Same build, different settings per environment. |
| Entity Framework 6 | EF Core (or EF6 on modern .NET first) | Two steps lowers risk on complex models. |
// 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.
- UsersSame URLs as today
- ASP.NET Core appServes migrated pages and APIs
- YARP forwarderSends everything else on
- Legacy appShrinks with each release
Routes move across one at a time until the legacy app receives no traffic.
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();<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.
| Phase | What changes | What users notice |
|---|---|---|
| 1. Stabilize | Source control, a CI build, and characterization tests around order entry, invoicing and the nightly jobs. | Nothing. |
| 2. Project files | Convert all projects to SDK-style with PackageReference (the .NET Upgrade Assistant automates much of this). | Nothing. |
| 3. Shared libraries | Multi-target the business library (net48;net10.0); replace abandoned packages. | Nothing. |
| 4. Background jobs | Move the Windows service to a .NET Worker Service, deployed as a container. | Nightly jobs run as before, now monitored. |
| 5. Web, route by route | Add 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 access | Move the data layer from EF6 to EF Core, table group by table group. | Nothing, if tests hold. |
| 7. Retire | Remove the legacy app once its traffic reaches zero. | Nothing; that is the point. |
Pitfalls that catch most teams
HttpContext.Currenteverywhere. Static access to the request is common in older code; injectIHttpContextAccessoror 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.