Introduction
Moving from .NET Framework to modern .NET feels daunting, but thousands of teams have done it successfully without rewrites. The key is treating migration as a staged process, not a big bang. This article walks through migrating from .NET Framework: best practices explained in a way that lets you ship value incrementally while de-risking the entire effort.
The migration path: a phased approach
The biggest mistake teams make is trying to convert everything at once. Instead, think of migration in layers. Your dependency graph naturally suggests the order: start with the leaves (class libraries that nothing else depends on), then work upward to the roots (your web applications). This approach minimizes blockers and lets you validate each step independently.
Step 1: Inventory dependencies and identify blockers
Before writing any code, audit what you actually depend on. Pull a report of all NuGet packages your solution uses. Check the Microsoft porting guide to see which packages work on modern .NET. Many popular libraries have already been migrated or have modern equivalents.
Common blockers we've encountered:
- Old WCF services (use gRPC or REST instead)
- App.config transformations (move to appsettings.json)
- Custom COM interop (isolate in a wrapper, call via P/Invoke)
- Enterprise-only packages with no .NET Core version
For packages with no modern replacement, decide: build an adapter, find an alternative, or keep that piece on Framework. Document everything. This inventory becomes your migration roadmap.
Step 2: Move class libraries to .NET Standard or modern .NET
Your utility libraries, domain models, and business logic should move first. These are easiest to test and fastest to validate. Target .NET Standard 2.0 if the library needs to be consumed by both Framework and modern .NET during the transition. Once everything else is migrated, you can upgrade these to .NET 6+ or later.
A typical library migration looks like this:
// Old: .csproj (Framework)
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<TargetFramework>net462</TargetFramework>
</PropertyGroup>
</Project>
// New: .csproj (Standard)
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
</PropertyGroup>
</Project>After conversion, you'll spot API breaks immediately at compile time. Methods like ConfigurationManager.AppSettings won't work—migrate configuration to dependency injection. String encoding methods might behave differently. Run your unit tests as your safety net here.
Step 3: Upgrade the web layer last
Your ASP.NET web application stays on Framework until everything below it compiles for modern .NET. This is deliberate. The web layer is where your integration tests live, and you want those tests running against stable, migrated dependencies first. Once class libraries are solid, converting the web project becomes straightforward: it's mostly project file changes and dependency injection wiring.
Step 4: Run old and new side by side
For large applications or mission-critical systems, running both versions in parallel for weeks or months reduces risk to near-zero. Deploy the new version to a separate process, behind a feature flag or separate hostname. Route a small percentage of traffic to it. Once you're confident, flip the switch entirely.
Using the .NET upgrade assistant
Microsoft's Upgrade Assistant automates much of the grunt work. It rewrites project files, updates package references, and flags API changes that need manual fixes. Think of it as a really smart find-and-replace tool that understands .NET semantics.
Here's what it handles well:
- Converting old .csproj format to SDK-style format
- Removing obsolete package references
- Updating NuGet packages to versions compatible with your target framework
- Flagging removed APIs (like
AppDomain,Remoting) - Adding necessary facades or compatibility packages
Install it globally and point it at your solution:
dotnet tool install -g upgrade-assistant
upgrade-assistant analyze
upgrade-assistant upgradeHowever, the tool can't know your business logic. It can't tell you whether to use a new config system, how to wire up dependency injection, or whether a third-party package behaves identically. Expect to spend 20–40% of your migration time fixing what the tool can't automate. That's normal and why it's called an assistant, not a solution.
The strangler fig pattern for web applications
For ASP.NET web apps that serve dozens of endpoints, the strangler fig pattern lets you migrate routes one at a time without downtime. The idea: a router (we typically use YARP) sits in front of both the old Framework app and the new .NET app. Traffic for migrated endpoints goes to the new app; everything else goes to the old one.
As you migrate endpoint by endpoint, traffic shifts. After weeks, the old app is only serving a handful of legacy routes, then zero. You decommission it. The strangler pattern is how we've migrated some 200+ endpoint APIs with zero downtime.
Set up YARP to proxy selectively:
// appsettings.json
{
"ReverseProxy": {
"Routes": {
"legacyRoute": {
"ClusterId": "legacy",
"Match": { "Path": "/api/old-endpoint/*" }
},
"newRoute": {
"ClusterId": "new",
"Match": { "Path": "/api/new-endpoint/*" }
},
"catchAll": {
"ClusterId": "legacy",
"Match": { "Path": "{**catch-all}" }
}
},
"Clusters": {
"legacy": {
"Destinations": { "dest1": { "Address": "http://oldapp:80" } }
},
"new": {
"Destinations": { "dest1": { "Address": "http://newapp:5000" } }
}
}
}
}The beauty of this approach is that both apps can have their own database, cache, and logging setup. You migrate data when you migrate endpoints, not all at once. Rollback is trivial: flip the route back to the old app and investigate.
Test coverage first: your safety net
Before migrating any endpoint, ensure it has solid integration test coverage on the Framework side. These tests validate the old behavior. After migration, the same tests run against the new endpoint and must pass identically.
A typical pre-migration checklist:
- Write or expand integration tests for the endpoint being migrated
- Run tests against Framework version—all pass
- Migrate the endpoint logic to the new .NET app
- Wire up dependencies (database, cache, services)
- Run the same tests against the new endpoint
- Compare response times, error rates, and behavior
- Switch traffic in YARP
- Monitor for a week; roll back if needed
We've found that teams skipping this step tend to discover subtle bugs in production. A user's session handling differs slightly. A calculation uses different rounding. A date field formats differently. Integration tests catch these before users see them.
Use a shared test harness if possible:
public abstract class ApiTestBase
{
protected HttpClient Client { get; set; }
protected abstract string BaseUrl { get; set; }
[Test]
public async Task GetUser_WithValidId_ReturnsCorrectData()
{
var response = await Client.GetAsync($"{BaseUrl}/api/users/123");
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
var json = await response.Content.ReadAsStringAsync();
var user = JsonSerializer.Deserialize<UserDto>(json);
Assert.That(user.Id, Is.EqualTo(123));
Assert.That(user.Name, Is.Not.Null);
}
}
public class LegacyApiTests : ApiTestBase
{
protected override string BaseUrl => "http://oldapp:80";
}
public class ModernApiTests : ApiTestBase
{
protected override string BaseUrl => "http://newapp:5000";
}Run both test classes. If both pass, your migration is behaviorally correct.
Common mistakes
- Migrating the web layer first: You end up debugging web plumbing while also learning new configuration systems. Move libraries first, web last.
- Skipping dependency inventory: You discover missing packages three weeks into migration. Audit upfront.
- Ignoring AppSettings/config migration: Don't try to keep App.config. Modern .NET uses appsettings.json and dependency injection. Budget time for this.
- No feature flags or strangler fig: Big bang migrations eat risk. Use patterns that let traffic flow gradually.
- Trusting the upgrade assistant alone: The tool is excellent but not magical. Review every change it makes, especially package downgrades and framework downgrades.
- Underestimating connection string and secrets migration: Environment variables, Azure Key Vault, or cloud-native secrets managers work differently. Test this thoroughly.
Key takeaways
- Start with an honest inventory of dependencies and blockers; this becomes your roadmap.
- Migrate bottom-up: class libraries first, web applications last. This minimizes downstream cascades.
- Use the .NET upgrade assistant to automate 60% of the work, then budget time for the manual 40%.
- Employ the strangler fig pattern with YARP to migrate endpoints incrementally with zero downtime.
- Write or expand integration tests before migration, then run them against both old and new implementations to validate parity.
- Run both versions in parallel for as long as practical—it's cheap insurance compared to production outages.
- Treat configuration and dependency injection migration as a first-class task, not an afterthought.






Comments
Loading comments…