Start Debugging

Migrate from .NET Framework 4.8 to .NET 11 in 2026

A version-pinned migration playbook for moving a .NET Framework 4.8 codebase to .NET 11 LTS in 2026, covering the SDK-style csproj rewrite, System.Web to ASP.NET Core, WCF, EF6 to EF Core 11, BinaryFormatter removal, AppDomain replacements, and a realistic rollback plan.

Moving a .NET Framework 4.8 codebase to .NET 11 is not a version bump. It is a re-platforming exercise that touches the project format, the web stack, the data access layer, the hosting model, and a long tail of APIs that quietly disappeared between 2017 and 2026. The official .NET Framework 4.8 servicing window is still open in 2026, but the runtime has not received a feature update since 2019, every modern Azure App Service plan is on the Core stack by default, and almost every NuGet package worth depending on has dropped net48 targets. The realistic effort for a typical line-of-business app is two to six weeks for a small service, two to four months for a medium codebase with WCF, EF6, and a WebForms or MVC 5 front end. WebForms apps stay where they are or get rewritten; there is no in-place port. This post pins net48 as the source and net11.0 as the target, and assumes the project is on Windows.

Why migrate now

If the codebase is a Windows desktop app (WinForms or WPF) that only runs on Windows and has no plans to deploy elsewhere, the question is fair. The answer is still usually yes, because the supported life of net48 ends with Windows 10’s extended support window, and tooling support is already thin.

What breaks

AreaChangeSeverity
Project formatpackages.config and the old csproj XML are unsupported by the .NET 11 SDKhigh
System.WebRemoved entirely. HttpContext.Current, modules, handlers, WebForms have no .NET 11 equivalenthigh
WCF serverSystem.ServiceModel on the server side is unsupported. Use CoreWCF or rewrite to gRPC or HTTPhigh
WCF clientSupported via System.ServiceModel.* 6.x NuGet packages, with limited bindingsmedium
Entity Framework 6Runs on .NET 11 with EF6 6.5.0 or later, but new development should use EF Core 11medium
AppDomainOnly the default AppDomain exists. No CreateDomain, no unloadable plugin containershigh
BinaryFormatterRemoved in .NET 9, no opt-in switchhigh
.NET RemotingGone. No replacement; rewrite to a network protocol you actually wanthigh
Code Access SecurityGone. [SecurityCritical], PermissionSet, sandboxing all removedhigh
web.configConfiguration moves to appsettings.json. system.web sections do not applyhigh
app.configMost settings still work via Microsoft.Extensions.Configuration.Xml, but binding redirects are gonemedium
WPF and WinFormsSupported on .NET 11, Windows only. Most third-party controls need a 6.x or later buildmedium
System.Drawing.CommonCross-platform support removed in .NET 6. Windows-only sincemedium

Read the .NET Framework to .NET porting overview and the .NET 11 breaking changes list once before you touch a .csproj. The first list is the longer of the two by far.

Pre-flight checklist

Run these before you change a single project file.

  1. Install the .NET 11 SDK on every dev machine and CI runner. Verify with dotnet --list-sdks and confirm 11.0.x appears. Keep the .NET Framework 4.8 developer pack installed so the old solution still opens in Visual Studio.
  2. Install the .NET Upgrade Assistant CLI and run it in analysis mode first. It does not migrate code; it produces an actionable report.
    # .NET 11, upgrade-assistant 0.6.x
    dotnet tool install --global upgrade-assistant
    upgrade-assistant analyze MySolution.sln
  3. Run the .NET Portability Analyzer or apiport against the compiled assemblies. Anything flagged as “not portable” is migration work that the upgrade-assistant tool will not do for you.
  4. Capture a baseline. Run the existing test suite on .NET Framework 4.8 and store the result. A clean green on the old runtime means the first red on .NET 11 is unambiguously a migration regression.
  5. Inventory third-party NuGet packages. Anything that ships only net48 or net472 assemblies is a blocker. Replacements: log4net 2.0.16, Newtonsoft.Json 13.x, AutoMapper 13.x, Dapper 2.1.x all multi-target and work on .NET 11. Anything else needs an upgrade ticket against the vendor.
  6. Branch the migration. Plan for at least one PR per project, and a separate PR for the test projects. A single mega-PR for a medium codebase is unreviewable.

Migration steps

  1. Convert every .csproj to SDK style. Replace the old XML with the SDK-style header. The new format infers files, drops most assembly references, and uses PackageReference instead of packages.config. The try-convert tool (bundled with the .NET Upgrade Assistant) handles the mechanical part.

    <!-- src/MyApi.csproj, .NET 11, after conversion -->
    <Project Sdk="Microsoft.NET.Sdk.Web">
      <PropertyGroup>
        <TargetFramework>net11.0</TargetFramework>
        <LangVersion>14.0</LangVersion>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
      </PropertyGroup>
      <ItemGroup>
        <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="11.0.0" />
        <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="11.0.0" />
      </ItemGroup>
    </Project>

    Verification: dotnet build exits with errors that name removed APIs rather than parser errors against the project file itself. Delete packages.config after the conversion succeeds.

  2. Delete BinaryFormatter usage everywhere. The type was removed in .NET 9 with no compatibility switch. Replace it with System.Text.Json, MessagePack, or protobuf-net depending on whether you need a JSON or binary wire format. If you have stored blobs serialised with BinaryFormatter, write a one-shot conversion utility that still runs on .NET Framework 4.8 to translate them into the new format before you decommission the old environment. Doing this conversion from .NET 11 is not possible.

    Verification: grep -r "BinaryFormatter" src/ is empty. Any blob storage that previously held binary-formatted payloads has been re-serialised and the new format round-trips through a unit test.

  3. Rewrite the web stack from System.Web to ASP.NET Core 11. This is the largest single piece of work. MVC 5 controllers map almost one-for-one to ASP.NET Core controllers, but routing attributes, model binding, action filters, and dependency injection all differ. HttpContext.Current is gone; controllers and middleware receive HttpContext explicitly. Application_Start in Global.asax becomes startup code in Program.cs. WebAPI 2 controllers are very close to ASP.NET Core controllers but inherit from ControllerBase rather than ApiController.

    // Program.cs, .NET 11, C# 14
    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddControllers();
    builder.Services.AddOpenApi();
    builder.Services.AddDbContext<AppDb>(o =>
        o.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
    
    var app = builder.Build();
    app.UseAuthentication();
    app.UseAuthorization();
    app.MapControllers();
    app.MapOpenApi();
    app.Run();

    WebForms (.aspx) has no migration path. Either keep the WebForms app on .NET Framework 4.8 for the rest of its life behind a reverse proxy, or rewrite the affected pages as Blazor, MVC, or Razor Pages. The Blazor Server vs Blazor WebAssembly vs Blazor United comparison is the right starting point if Blazor is on the table.

    Verification: every controller has at least one integration test that exercises an HTTP route against WebApplicationFactory<Program> and asserts both status code and response body.

  4. Move web.config to appsettings.json. Connection strings, custom appSettings, and logging configuration move to JSON. system.web sections do not apply. system.webServer sections that configure IIS still apply if you host behind IIS via the in-process module, but most production deployments now use Kestrel directly. Authentication settings move from <authentication> and <authorization> web.config sections to builder.Services.AddAuthentication(...) and the authorization policy API.

    // appsettings.json, .NET 11
    {
      "ConnectionStrings": {
        "Default": "Server=.;Database=App;Trusted_Connection=True;TrustServerCertificate=True;"
      },
      "Logging": {
        "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" }
      }
    }

    Verification: the app reads every previously-config-driven value through IConfiguration or the strongly typed IOptions<T> pattern; no calls to ConfigurationManager.AppSettings survive in production code.

  5. Handle WCF. Server-side WCF is unsupported on .NET 11. Two realistic paths:

    • CoreWCF (the community-maintained port). Add CoreWCF.Primitives, CoreWCF.Http, and the bindings you actually use. Most BasicHttpBinding and NetTcpBinding services migrate with a contract reference and a UseServiceModel configuration call. Streaming, transactions, and message-level security have varying degrees of support; check the CoreWCF compatibility matrix before you commit.
    • Rewrite to gRPC or an HTTP API. Higher ceiling, more work. The right call when the WCF interface was only ever consumed by clients you control.

    Client-side WCF is supported via the System.ServiceModel.* 6.x NuGet packages with BasicHttpBinding, NetTcpBinding (limited), and WSHttpBinding (transport security only). If your client uses WSFederationHttpBinding or message-level security, you will rewrite the consumer.

    Verification: every WCF endpoint has a contract test that runs the .NET 11 client against either the new CoreWCF host or the rewritten replacement, and asserts the same payloads as the old .NET Framework client.

  6. Move Entity Framework 6 to EF Core 11 (when worth it). EF6 runs on .NET 11 via the EntityFramework 6.5.0 package, so a strict lift-and-shift is possible. But EF6 receives no new features, the DbContext API in EF Core is closer to what you want for ASP.NET Core dependency injection, and EF Core 11’s compiled queries and primitive-collection translation are meaningfully faster. For most teams the right call is to ship the migration on EF6 first, then move to EF Core in a follow-up PR. The EF Core compiled queries vs raw SQL vs Dapper post quantifies the hot-path gains.

    Verification: the chosen ORM passes the same integration test suite that ran under EF6 on .NET Framework, including any tests that asserted generated SQL.

  7. Replace AppDomain.CreateDomain plugin loaders. AppDomain is no longer an isolation boundary on .NET 11; only the default domain exists. Plugin systems that previously loaded assemblies into a child AppDomain for unload semantics or fault isolation must move to AssemblyLoadContext with isCollectible: true and call Unload() when finished. Out-of-process plugins via dotnet worker processes are the safer pattern when the plugin is untrusted.

    Verification: a unit test loads a plugin assembly, calls into it, unloads the AssemblyLoadContext, and asserts that a WeakReference to the context becomes null after a GC.Collect() cycle.

  8. Audit C# language version bumps. Going from C# 7.3 to C# 14 is twelve language releases in one step. Most of it is additive and safe, but nullable reference types (introduced in C# 8) will flag thousands of warnings on legacy code if you turn <Nullable>enable</Nullable> on globally. The realistic path is <Nullable>annotations</Nullable> first (annotations only, no diagnostics), then file-by-file conversion using #nullable enable pragmas. C# 14’s overload-resolution change around span overloads is documented in the C# 14 overload resolution breaking change with spans fix post.

    Verification: dotnet build -warnaserror is clean on the agreed nullable scope before merging.

  9. Update CI runner images. Bump GitHub Actions actions/setup-dotnet to dotnet-version: 11.0.x, update any Dockerfile base image to mcr.microsoft.com/dotnet/sdk:11.0 and mcr.microsoft.com/dotnet/aspnet:11.0, and remove the old .NET Framework MSBuild image. If any project still has to build under .NET Framework (for example, the one-shot BinaryFormatter conversion tool from step 2), keep a single windows-2022 runner with the .NET Framework 4.8 developer pack installed and gate it on a path filter.

    Verification: a pipeline run on a feature branch is green end to end, including dotnet publish, container image build, and smoke deploy to a staging environment.

Verification (smoke checklist)

After the steps above, the app should pass every line of this list before the migration PR merges:

If any of those fail, stop. A partial migration to .NET 11 is worse than a clean .NET Framework 4.8 deploy because it commits to both runtimes.

Rollback

The migration is reversible only as long as the database schema and any wire formats have not changed. The runtime swap itself is reversible: revert the .csproj changes and reinstall the .NET Framework 4.8 redistributable on the host. The decisions that make rollback expensive are usually orthogonal:

The pragmatic plan is to keep the .NET Framework deploy warm in a separate slot for one week after cutover, and to gate the cutover behind a feature flag at the load balancer rather than at deploy time. Fix forward after that window.

Gotchas we hit

Sources

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Back