Every legacy .NET Framework system we've inherited has the same story: it works, it's profitable, and nobody wants to touch it. That's exactly why migrations fail when they're treated as a single heroic rewrite. The teams that succeed treat modernization as a series of small, reversible steps that ship alongside normal feature work.

Key takeaways
  • Inventory dependencies before writing a single line of migration code — third-party package compatibility is usually the real blocker, not your own code.
  • Multi-target your class libraries (net48;net10.0) so you can migrate layer by layer instead of all at once.
  • Use the strangler-fig pattern with a reverse proxy to run old and new side by side in production.
  • Build a characterization test suite before refactoring business logic you don't fully trust.

Why this migration is worth the effort

.NET Framework 4.8 is the last version of its line — there is no .NET Framework 5. It still receives security patches, but no new features, no performance investment, and none of the runtime and tooling improvements that have landed in every release since .NET Core 3.1. .NET 10 is the current long-term support release, and it's meaningfully faster, runs on Linux and in containers, and unlocks years of C# language features your team hasn't been able to use.

None of that matters if the migration takes down production or stalls the roadmap for two quarters. The plan below is the one we use with clients running everything from internal line-of-business apps to customer-facing platforms processing real revenue.

Step 1: Inventory before you touch code

Start with an honest audit of what the application actually depends on:

  • NuGet and GAC dependencies — run the .NET Upgrade Assistant's analysis mode against each project to see which packages already ship .NET Standard 2.0 or .NET 10 builds.
  • Windows-only APIsSystem.Drawing.Common, System.Web, WCF server hosting, and COM interop are the usual suspects that need a replacement strategy, not just a recompile.
  • ConfigurationWeb.config / App.config transforms need to become appsettings.json plus the options pattern.
  • Session and identity — anything built on System.Web.HttpContext.Current, Forms Authentication, or Membership providers needs an ASP.NET Core equivalent.

This step alone usually reshapes the whole plan. A codebase with a handful of Windows-only dependencies concentrated in one module is a very different project from one where HttpContext.Current is scattered through two hundred files.

Step 2: Multi-target your class libraries

Before migrating any web or worker project, get your shared libraries building against both frameworks at once. This lets you fix cross-cutting issues — nullable reference warnings, removed APIs, behavioral differences — while the applications that consume them keep running on .NET Framework.

xml
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFrameworks>net48;net10.0</TargetFrameworks>
    <LangVersion>latest</LangVersion>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup Condition="'$(TargetFramework)' == 'net48'">
    <PackageReference Include="System.Text.Json" Version="8.0.0" />
  </ItemGroup>
</Project>

Class libraries are almost always your quickest win here — no hosting model, no Startup.cs, just a project file change and a compile pass.

Step 3: Use the strangler-fig pattern for the application shell

For anything bigger than a small internal tool, don't attempt an in-place conversion of the web project. Stand up a new ASP.NET Core 8 application next to the existing one, and route traffic between them with a reverse proxy. YARP (Yet Another Reverse Proxy) is the tool we reach for — it's a .NET library, so it fits naturally into a .NET-heavy stack.

json
{
  "ReverseProxy": {
    "Routes": {
      "new-checkout": {
        "ClusterId": "dotnet8",
        "Match": { "Path": "/checkout/{**catch-all}" }
      },
      "legacy-catch-all": {
        "ClusterId": "netframework48",
        "Match": { "Path": "/{**catch-all}" }
      }
    },
    "Clusters": {
      "dotnet8": {
        "Destinations": { "d1": { "Address": "http://localhost:5080/" } }
      },
      "netframework48": {
        "Destinations": { "d1": { "Address": "http://localhost:8080/" } }
      }
    }
  }
}

Each feature area you migrate becomes a new route pointing at the .NET 10 cluster. The legacy application keeps serving everything you haven't migrated yet, and you can roll a route back to the old cluster in seconds if something's wrong.

Watch forShared session state and authentication cookies between the two applications. Standardize on a shared cookie format (or a token-based auth handoff) early — this is the single most common source of “works locally, breaks in the proxy” bugs.

Step 4: Replace Service Locator with constructor injection incrementally

Older .NET Framework apps frequently lean on static service locators or HttpContext.Current lookups. ASP.NET Core's built-in DI container expects constructor injection. You don't need to refactor everything on day one — wrap the old locator behind an adapter that also registers with the new container, then migrate call sites feature by feature as you touch that code anyway.

Step 5: Build a characterization safety net

For business logic you don't fully understand — and in a legacy system, there's always some — write characterization tests before you refactor: capture the current inputs and outputs as they are today, not as you think they should be. These tests aren't about correctness; they're a tripwire that tells you the moment a refactor changes behavior.

Step 6: Parallel run, then cut over

For anything customer-facing or revenue-bearing, run the new .NET 10 path behind a feature flag and mirror a percentage of production traffic to it, comparing outputs without serving the response from the new path yet. Once you trust the parity, flip the flag for a small cohort, then ramp up gradually.

.NET Framework 4.8 vs .NET 10, at a glance

.NET Framework 4.8.NET 10
SupportSecurity fixes onlyLong-term support
PlatformsWindows onlyWindows, Linux, macOS
HostingIISKestrel, containers, IIS
PerformanceBaselineSignificantly faster GC and JIT
LanguageC# 7.3 maxC# 14

Migration isn't free, but it's rarely as expensive as the sunk-cost anxiety makes it feel — especially once you stop thinking of it as one project and start treating it as a standing practice that runs alongside your regular roadmap.

Need help with this in production?

astrivo helps teams design, build, and modernize systems like this. Let's talk about your context.