.NET migration guide

How to migrate .NET Framework to .NET 8

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 CA1416 platform-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.0 or at least netstandard2.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.asaxASP.NET Core Program.cs + middleware pipeline
web.config app settingsappsettings.json + IConfiguration
ConfigurationManagerIConfiguration / options pattern
HttpContext.CurrentIHttpContextAccessor via DI
ASP.NET MVC 5 / Web API 2ASP.NET Core MVC / minimal APIs
WebForms (.aspx)No port — rewrite to Blazor, Razor Pages or MVC
WCF server hostingCoreWCF, or gRPC / REST
WCF client (ServiceModel)System.ServiceModel.* client packages (supported)
.NET RemotingNo replacement — redesign to gRPC / REST / message queue
AppDomain isolation / unloadingAssemblyLoadContext (partial) or process isolation
Entity Framework 6Entity Framework Core 8
BinaryFormatterObsolete/removed — use System.Text.Json or protobuf
System.Drawing.Common (cross-platform)Windows-only in .NET 8 — use ImageSharp / SkiaSharp
Windows Registry, WMI, DirectoryServicesMicrosoft.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.

Don't want to do this by hand?

Mira runs this exact migration for you — detect, modernize, add tests, run a security pass — on your own servers, in hours, not months.

Scan your repo free → Check your version's EOL →