Migrating from .NET Framework to .NET 10: A Step-by-Step Playbook
Plenty of businesses still run their core systems on .NET Framework 4.x: ASP.NET MVC 5 or Web Forms, WCF services, Entity Framework 6, all hosted on Windows and IIS. They work, but they're getting harder to staff, harder to secure and more expensive to host. Moving to modern .NET (.NET 10 is the current long-term support release) brings big performance gains, cross-platform hosting, containers and a much better developer experience. This playbook is the process I use when I migrate client applications, including the parts that usually go wrong.
Why migrate, and why now
- Performance and hosting cost: ASP.NET Core typically handles several times the requests per server of classic ASP.NET, and runs on cheaper Linux hosts and containers.
- Security and support: .NET Framework 4.8.1 still gets security fixes as part of Windows, but there are no new features, and much of the library ecosystem has moved on.
- Hiring: developers want to work on modern .NET. A legacy stack is a recruiting and retention problem.
- Cloud and AI: new Azure SDKs, OpenTelemetry, Aspire and AI libraries such as Microsoft.Extensions.AI target modern .NET first.
Step 1: assess before you touch code
A migration estimate without an assessment is a guess. Spend the first week building an inventory:
- Projects and dependencies: list every project, its type (class library, MVC, Web Forms, WCF, Windows service) and its NuGet packages. For each package, check whether a version for modern .NET exists or whether there's a replacement.
- API compatibility: run the analysers built into Visual Studio's modernization tooling (GitHub Copilot app modernization for .NET, which replaces the older .NET Upgrade Assistant) to find APIs that don't exist in modern .NET:
System.Web,AppDomain, .NET Remoting,System.Drawingon Linux, and so on. - Hidden Windows dependencies: registry access, COM components, Windows authentication, the GAC, MSMQ, scheduled tasks and file paths with backslashes.
- Test coverage: be honest about it. If there are no tests, the first deliverable is a set of characterisation tests around the most important flows.
The output is a migration map: what can be retargeted as-is, what needs rewriting, and what should be replaced by something else entirely.
Step 2: move the class libraries first
Business logic and data access libraries are usually the easiest part. Convert them to SDK-style projects and multi-target, so the old application keeps working while you migrate:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net48;net10.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
</PropertyGroup>
</Project>
Where code differs between targets, isolate it behind an interface rather than scattering #if NET48 everywhere. Libraries that target .NET Standard 2.0 can be consumed by both worlds, which is useful during the transition.
Step 3: choose a strategy for the web app
There are two realistic options for the web layer.
Big-bang rewrite
Create a new ASP.NET Core project, port all controllers and views, and switch over on a release date. This is fine for small applications (a few dozen controllers, little Web Forms). For anything bigger it means a long code freeze, a risky cut-over and two codebases to maintain in the meantime.
Incremental migration (strangler fig)
For most business applications I use the incremental approach Microsoft documents: put a new ASP.NET Core app in front of the old one and move routes across one at a time.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSystemWebAdapters(); // shared session/auth with the old app
builder.Services.AddHttpForwarder();
var app = builder.Build();
app.UseSystemWebAdapters();
app.MapControllers(); // routes already migrated
// Everything not yet migrated is forwarded to the legacy ASP.NET app
app.MapForwarder("/{**catch-all}", app.Configuration["LegacyAppUrl"]!)
.Add(static b => ((RouteEndpointBuilder)b).Order = int.MaxValue);
app.Run();
The Microsoft.AspNetCore.SystemWebAdapters packages let both applications share session state and authentication, so users don't notice which app served a page. YARP does the forwarding. You release continuously, each migrated route is tested in production, and the old app shrinks until it can be switched off.
Step 4: deal with the hard parts
| Legacy technology | Modern replacement |
|---|---|
| ASP.NET MVC 5 / Web API 2 | ASP.NET Core MVC or Minimal APIs (closest port, mostly mechanical) |
| Web Forms | No direct port: Razor Pages or Blazor, rewritten screen by screen |
| WCF server | CoreWCF for a like-for-like port, or gRPC / REST for new contracts |
| Entity Framework 6 | EF Core (EF6 runs on modern .NET as a stepping stone) |
| Windows Services | Worker Service (BackgroundService), hosted as a service or a container |
| web.config settings | appsettings.json, environment variables, Azure Key Vault |
| Global.asax, HTTP modules | Middleware and the Program.cs pipeline |
| Forms / Windows authentication | ASP.NET Core Identity, cookie auth, or Microsoft Entra ID with OpenID Connect |
Two pieces of advice from experience. First, if EF6 to EF Core is a big job (lots of EDMX, lazy loading everywhere), run EF6 on modern .NET first and migrate the data layer as a separate project. Second, don't use the migration to also change the database, the UI design and the business rules. One big change at a time.
Step 5: test like you mean it
- Characterisation tests before migrating a feature: record what the old system returns, then assert the new one returns the same.
- Integration tests for every migrated endpoint with
WebApplicationFactory. - Performance baselines: measure key pages before and after so you can show the gain (and catch regressions such as a lost query cache).
- Run both versions side by side in staging, ideally replaying real traffic.
Step 6: deploy and decommission
Once migrated, take advantage of what modern .NET makes easy: containerise the app, host it on Azure App Service for Linux or Azure Container Apps, add health checks and OpenTelemetry, and set up a CI/CD pipeline with automated tests. Then actually switch the legacy application off. Many migrations stall at 90% because nobody plans the last few awkward pages; budget for them explicitly.
How long does a migration take?
It depends far more on Web Forms, WCF and test coverage than on lines of code. As a rough guide: a mid-sized MVC application with good separation can move in weeks; a large Web Forms system with business logic in code-behind is a multi-month, screen-by-screen programme. The assessment in Step 1 is what turns that range into a reliable plan.
Summary
Assess first, migrate libraries by multi-targeting, move the web app incrementally with YARP and System.Web adapters, replace WCF, Web Forms and EF6 deliberately, and test every step. Done this way, a .NET Framework to .NET 10 migration is a series of small, safe releases instead of one risky rewrite.
I help teams modernise legacy .NET applications. If you're planning a migration, see my .NET development services or ask me for an assessment.