Migrating off .NET Framework is no longer a "nice to have". .NET Framework is a legacy, Windows-only runtime in maintenance mode: it receives security and reliability fixes but no new features, no performance work, and no cross-platform support. Modern .NET moved on years ago, and the release cadence is unforgiving. .NET 6 reached end of support on 12 November 2024, and .NET 7 ended in May 2024. .NET 8 is the current LTS release, supported until 10 November 2026 — which is exactly why it is the target you should aim at, not an interim version that is already dead or dying.
The good news: the tooling and patterns for this migration are mature. The honest news: a real migration is a bottom-up engineering project, not a right-click "upgrade" button, and some pieces (WebForms, WCF server hosting) require genuine rewrites. This guide walks the whole path with concrete before/after code. If you want a machine to do the mechanical 80% and flag the hard 20%, that is exactly what our EOL checker and Mira's automated migration passes are built for.
Assess before you touch a line of code
Never start by editing project files. Start by building an accurate picture of what you have and what will break.
- .NET Upgrade Assistant — run it in analyze mode first (
upgrade-assistant analyze). It produces an incompatibility report per project without changing anything. - API portability / platform analyzers — the classic .NET Portability Analyzer plus the built-in
CA1416platform-compatibility analyzer surface APIs that do not exist or are Windows-only on .NET 8. - Dependency inventory — enumerate every NuGet package and check whether it targets
net8.0or at leastnetstandard2.0. Packages that are Framework-only (or abandoned) are your critical path.
Expect the analyzers to flag a predictable set of dead ends. These have no direct port and dictate scope:
The four things that turn an "upgrade" into a "rewrite": ASP.NET WebForms (no port — ever), WCF server hosting (the client stack lives on, the server does not), .NET Remoting, and AppDomain isolation. If your app leans on any of these, budget for redesign, not retargeting.
Reshape the project files
The old verbose .csproj lists every source file and uses packages.config. .NET 8 requires the SDK-style format, which globs files and uses PackageReference. The difference is dramatic:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
</PropertyGroup>
<ItemGroup><Compile Include="Services\OrderService.cs" /></ItemGroup>
<ItemGroup><Reference Include="Newtonsoft.Json, Version=..." /></ItemGroup>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup><PackageReference Include="Newtonsoft.Json" Version="13.0.3" /></ItemGroup>
</Project>
For shared class libraries that must temporarily serve both worlds, multi-target with <TargetFrameworks>net48;net8.0</TargetFrameworks> (note the plural) so old and new callers can both consume them during the transition.
The seven-step migration
1. Inventory and run the analyzers
As above: report first, decisions second. Produce a spreadsheet of projects, their dependencies, and each analyzer flag. This is your migration backlog.
2. Convert to SDK-style and pick a target
Convert projects to SDK-style. Choose net8.0 for anything you are moving wholesale, or multi-target libraries that still need to feed a not-yet-migrated Framework host.
3. Migrate class libraries first (bottom-up)
Work the dependency graph from the leaves up. Retarget the libraries with no internal dependencies first, ideally to netstandard2.0 so they are consumable everywhere, then move up toward the entry-point projects. Never start at the web layer.
4. Replace unsupported APIs
Configuration is the most common change. ConfigurationManager.AppSettings and web.config give way to appsettings.json and injected IConfiguration:
var cs = ConfigurationManager.ConnectionStrings["Db"].ConnectionString;
var retries = int.Parse(ConfigurationManager.AppSettings["MaxRetries"]);
public class OrderService(IConfiguration config)
{
private readonly string _cs = config.GetConnectionString("Db")!;
private readonly int _retries = config.GetValue<int>("MaxRetries");
}
The other notorious one is the ambient HttpContext.Current, which does not exist in ASP.NET Core. Inject IHttpContextAccessor instead:
var user = HttpContext.Current.User.Identity.Name;
// Program.cs: builder.Services.AddHttpContextAccessor();
public MyClass(IHttpContextAccessor accessor) { _accessor = accessor; }
var user = _accessor.HttpContext?.User.Identity?.Name;
For data access, EF6 maps to EF Core — mostly a namespace and API change (DbContext survives, but Database.SetInitializer, EDMX designer models and some LINQ translations do not, so test queries). For services exposed over WCF, migrate the host to CoreWCF if you must keep the SOAP contract, or take the opportunity to move to gRPC or a minimal REST API if clients allow.
5. Stand up an ASP.NET Core host
Global.asax and the System.Web pipeline are replaced by a single Program.cs with explicit middleware ordering:
// Global.asax.cs
protected void Application_Start() {
RouteConfig.RegisterRoutes(RouteTable.Routes);
GlobalConfiguration.Configure(WebApiConfig.Register);
}
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddHttpContextAccessor();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
HTTP modules and handlers become middleware; Server.MapPath becomes IWebHostEnvironment.ContentRootPath; session and caching move to the Microsoft.Extensions abstractions.
6. Route incrementally behind YARP (strangler fig)
Do not attempt a big-bang cutover. Put YARP (Microsoft's reverse proxy) in front of both apps. New .NET 8 endpoints are served by the new host; everything else falls through to the legacy Framework app. Move routes one at a time until nothing is left pointing at the old system.
// appsettings.json — YARP falls back to legacy for unmigrated routes
"ReverseProxy": {
"Routes": {
"legacy": { "ClusterId": "legacy", "Match": { "Path": "{**catch-all}" } }
},
"Clusters": { "legacy": { "Destinations": {
"d1": { "Address": "http://legacy-app.internal/" } } } }
}
7. Test and cut over
Wrap legacy behaviour in characterization tests before you change it, run integration tests for parity, shift traffic gradually through the proxy, monitor error rates and latency, then retire the legacy host once its route table is empty.
Breaking-changes and replacement reference
| .NET Framework API / feature | .NET 8 replacement |
|---|---|
System.Web / Global.asax | ASP.NET Core Program.cs + middleware pipeline |
web.config app settings | appsettings.json + IConfiguration |
ConfigurationManager | IConfiguration / options pattern |
HttpContext.Current | IHttpContextAccessor via DI |
| ASP.NET MVC 5 / Web API 2 | ASP.NET Core MVC / minimal APIs |
| WebForms (.aspx) | No port — rewrite to Blazor, Razor Pages or MVC |
| WCF server hosting | CoreWCF, or gRPC / REST |
WCF client (ServiceModel) | System.ServiceModel.* client packages (supported) |
| .NET Remoting | No replacement — redesign to gRPC / REST / message queue |
AppDomain isolation / unloading | AssemblyLoadContext (partial) or process isolation |
| Entity Framework 6 | Entity Framework Core 8 |
BinaryFormatter | Obsolete/removed — use System.Text.Json or protobuf |
System.Drawing.Common (cross-platform) | Windows-only in .NET 8 — use ImageSharp / SkiaSharp |
| Windows Registry, WMI, DirectoryServices | Microsoft.Windows.Compatibility (Windows-only, CA1416) |
For adjacent runtimes, see our companion guides on what to do now that .NET 6 is end of life and rewriting WebForms to Blazor.
Frequently asked questions
Is .NET 6 end of life?
Yes. .NET 6 reached end of support on 12 November 2024 and no longer receives security patches; .NET 7 ended even earlier, in May 2024. .NET 8 is the current LTS release, supported until 10 November 2026, which is why it — not 6 or 7 — is the correct migration target.
Can ASP.NET WebForms run on .NET 8?
No. WebForms was never ported to .NET Core / .NET 5+ and there is no compatibility shim or automated converter. WebForms applications must be rewritten, typically to Blazor, Razor Pages or ASP.NET Core MVC. This is the single hardest part of most migrations, so scope it as a rewrite from the outset.
What is the support difference between .NET 8 and .NET Framework 4.8?
.NET Framework 4.8 remains supported for the lifetime of the Windows versions it ships with, but it is in maintenance mode — security and reliability fixes only, with no new features, no performance improvements and no cross-platform support. .NET 8 is actively developed, cross-platform and substantially faster. All new Microsoft investment goes to modern .NET, so Framework is a dead end for new capability even though it will keep running.
Do I still have Windows-only concerns after moving to .NET 8?
Possibly. The runtime is cross-platform but individual APIs are not. Registry access, WMI, System.Drawing.Common, Windows Services, COM interop, Active Directory / DirectoryServices and Windows authentication remain Windows-only. If cross-platform deployment is a goal, audit for the Microsoft.Windows.Compatibility surface and heed the CA1416 platform-compatibility analyzer before committing.
Can I migrate gradually or is it all-or-nothing?
Gradually. Use the strangler-fig pattern: put a YARP reverse proxy in front of the application and move routes or endpoints to the new .NET 8 host one at a time, letting the legacy app serve everything not yet migrated. The system stays shippable throughout and you avoid a risky big-bang cutover.