| name | migrating-dotnet |
| description | Use when upgrading .NET Framework (3.5, 4.x) to modern .NET (6, 8, 9, 10), migrating between .NET versions, encountering compatibility issues during upgrades, or planning .NET modernization projects |
Migrating .NET Versions
Overview
.NET migration transforms applications from legacy .NET Framework to modern .NET (or between modern versions). With GitHub Copilot app modernization (VS 2022 17.14.16+ / VS 2026), direct jumps from .NET Framework 3.5/4.8 to .NET 10 are now feasible.
Core principle: Prepare meticulously, let AI automate transformations, validate obsessively.
Iron rule: After EVERY step, the application must compile, run, and pass all tests. Never proceed with a broken build. If tests fail, fix before continuing.
First step: Create a dedicated migration branch (or git worktree) before touching any code. This keeps main stable, enables easy rollback, and allows parallel development to continue.
Expected gains: 30-50% performance improvement, 40-60% memory reduction, modern security features.
When to Use
Use when:
- Upgrading from .NET Framework 3.5/4.x to .NET 6/8/9/10
- Moving between modern .NET versions (e.g., .NET 6 → .NET 8)
- End-of-support deadlines approaching
- Need cross-platform deployment, containerization, or cloud-native features
- Performance improvements required (30-50% faster HTTP, 40-60% lower memory)
Don't use for:
- Minor patch updates within same version
- Projects that will be retired soon
Migration Decision Flow
Is app simple with few dependencies?
├─ YES → In-place migration (update TFM directly)
└─ NO → Incremental migration (Strangler Fig pattern with YARP)
│
└─ Start with leaf dependencies, work up to entry point
Prerequisites (Complete Before Migration)
| Step | Action | Why |
|---|
| 1 | Upgrade to .NET Framework 4.7.2+ | Latest APIs compatible with modern .NET |
| 2 | Convert to SDK-style .csproj | Required for modern .NET and tooling |
| 3 | Migrate packages.config → PackageReference | Modern dependency management |
| 4 | Implement Central Package Management | Single source of truth for versions |
| 5 | Replace Newtonsoft.Json → System.Text.Json | Faster, more secure, modern standard |
| 6 | Move config to appsettings.json | Structured configuration model |
| 7 | Decouple business logic into .NET Standard 2.0 libraries | Clean isolation boundary |
| 8 | Ensure all tests pass (green baseline) | Safety net for validation |
Central Package Management (CPM)
Create Directory.Packages.props at solution root:
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageVersion Include="System.Text.Json" Version="8.0.0" />
</ItemGroup>
</Project>
Quick Reference: Breaking Changes by Area
| Category | Removed/Changed | Replacement |
|---|
| AppDomains | Removed | AssemblyLoadContext or separate processes |
| Remoting | Removed | gRPC, REST APIs, or named pipes |
| WCF Server | Removed | CoreWCF (community), gRPC, or REST |
| Web Forms | Removed | Blazor, Razor Pages, or MVC |
| System.Web | Removed | ASP.NET Core middleware |
| app.config/web.config | Changed | appsettings.json + IConfiguration |
| Global.asax | Removed | Program.cs + middleware pipeline |
| HTTP Modules/Handlers | Removed | Middleware |
| Windows Auth | Changed | Azure AD or platform-specific auth |
Migration Strategy
The Step-by-Step Principle
CRITICAL: Migration is NOT a big-bang rewrite. Each step must leave the application in a working state.
For each migration step:
1. Make ONE focused change
2. Build → must succeed
3. Run tests → must pass
4. Commit to Git (checkpoint)
5. Verify application runs correctly
6. ONLY THEN proceed to next step
Why this matters:
- If something breaks, you know exactly which change caused it
- Rollback is always one commit away
- Functionality drift is caught immediately
- Team can review incremental changes
Red flag: If you find yourself making multiple unrelated changes before testing, STOP. You've lost the safety net.
Phase 1: Shared Libraries (2-4 weeks typical)
Migrate ONE library at a time. After each library:
- ✅ Build entire solution
- ✅ Run all tests
- ✅ Commit
- Target .NET Standard 2.0 for dual compatibility
- Multi-target to support both old and new hosts:
<TargetFrameworks>net48;netstandard2.0</TargetFrameworks>
- Add platform guards for Windows-specific code:
if (OperatingSystem.IsWindows())
{
}
Phase 2: Data Access Layer
Migrate ONE repository/context at a time. Verify queries return same results.
- EF6 → EF Core: Different query behavior, especially lazy loading and raw SQL
- Test all queries—don't assume equivalent behavior
- Consider database migration for licensing cost reduction
Phase 3: Business Logic
Migrate ONE service at a time. Keep interfaces stable.
- Usually migrates cleanly
- Watch for: Registry access, Windows paths, COM interop
Phase 4: Application Entry Point
For ASP.NET → ASP.NET Core (incremental with YARP):
- Create new ASP.NET Core project alongside existing
- Set up YARP reverse proxy to route between old/new
- Migrate ONE route/controller at a time
- Test each migrated route thoroughly
- Remove old route only after new one is validated
- Repeat until all routes migrated
Tools
GitHub Copilot App Modernization (Primary Tool)
Requirements:
- Visual Studio 2022 (17.14.16+) or Visual Studio 2026
- GitHub Copilot subscription (Pro, Pro+, Business, or Enterprise)
- .NET 10 SDK installed
Replaces these extensions (uninstall if present):
- .NET Upgrade Assistant
- GitHub Copilot App Modernization — Upgrade for .NET
- Azure Migrate Application and Code Assessment for .NET
Invoke:
- Right-click solution → "Modernize", or
- Copilot Chat (Agent Mode):
@modernize upgrade my entire solution to .NET 10 LTS
Key capabilities:
- Agent-based planning (understands project dependencies)
- Automated code transformations
- Learns from manual interventions
- Git integration with automatic commits at each phase
Other Tools
| Tool | Purpose | When to Use |
|---|
| upgrade-assistant CLI | Analysis and reports | If not using VS with Copilot |
| API Portability Analyzer | Check API compatibility | Early assessment |
| try-convert | Convert .csproj to SDK-style | Pre-migration prep |
dotnet tool install -g upgrade-assistant
upgrade-assistant analyze <project-path>
Third-Party Dependencies
- Check NuGet for .NET 6+ versions of each package
- No compatible version? Options:
- Find alternative package
- Fork and port (if open source)
- Wrap in .NET Standard 2.0 shim
- Replace functionality
- Multi-target during transition to maintain compatibility
Testing Strategy
| Test Type | When | Focus |
|---|
| Unit tests | Each component migration | Logic correctness |
| Integration tests | After layer migration | Cross-component behavior |
| Performance tests | Before/after comparison | No regressions |
| End-to-end tests | Full path validation | User flows work |
Critical: Run existing test suite against migrated code—tests passing immediately indicate coverage gaps.
Common Pitfalls
| Pitfall | Solution |
|---|
| Sync-over-Async (deadlocks) | Never use .Result or .Wait(). Use await throughout. |
| EF Core "Could Not Be Translated" | Call ToList() before client-side filtering (watch for N+1) |
| BinaryFormatter usage | Removed for security. Migrate to System.Text.Json |
| Big-bang migration | Use incremental approach for complex apps |
| Ignoring behavioral changes | Test thoroughly; some APIs behave differently |
| Not updating CI/CD | Update build pipelines to new SDK |
| Skipping performance baseline | Benchmark before migration to detect regressions |
| Secrets in logs/errors | Use structured logging with data masking |
| Binding redirect errors | Use CPM (Directory.Packages.props) to unify versions |
Sync-over-Async Example
var result = MyAsyncMethod().Result;
var result = await MyAsyncMethod();
Version-Specific Notes
.NET 8 (LTS - Current)
- Native AOT compilation available
- Minimal APIs improvements
- Support until November 2026
.NET 9 (STS)
- System.Linq.AsyncEnumerable in core
- OpenAPI 3.1 support
- Short-term support
.NET 10 (LTS - November 2025)
- W3C trace context default
- Generic math shift behavior changes
--interactive defaults to true in CLI
Zero Trust Security (Post-Migration)
Implement during or immediately after migration:
Managed Identity + Azure Key Vault
var builder = WebApplication.CreateBuilder(args);
var keyVaultUrl = new Uri($"https://{builder.Configuration["KeyVaultName"]}.vault.azure.net/");
builder.Configuration.AddAzureKeyVault(keyVaultUrl, new DefaultAzureCredential());
builder.Services.AddDbContext<MyContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
Database Access with Managed Identity
CREATE USER [your-app-name] FROM EXTERNAL PROVIDER;
ALTER ROLE db_owner ADD MEMBER [your-app-name];
Security Checklist
Technology Blockers & Solutions
ASP.NET Web Forms (.aspx) - No Automatic Migration
| Option | Effort | Best For |
|---|
| Blazor Server | Medium | Forms-heavy apps, minimal logic rewrite |
| Blazor WebAssembly | Medium-High | Full client-side SPA |
| Razor Pages | High | Simpler page-focused development |
| ASP.NET Core MVC | High | Full control, complex apps |
WCF Server-Side - No Automatic Migration
| Option | Effort | Best For |
|---|
| CoreWCF | Low | Quick win, existing WCF services as-is |
| gRPC | Medium | Modern, high-performance, long-term |
| ASP.NET Core Web API | High | REST redesign, maximum flexibility |
System.Web Dependencies
| Old | New |
|---|
HttpContext.Current | IHttpContextAccessor (DI) |
Session["key"] | ISession with IDistributedCache |
Server.MapPath() | Path.Combine(env.ContentRootPath, relativePath) |
Application cache | IMemoryCache or IDistributedCache |
Red Flags - STOP and Reassess
- WCF server-side code (significant rewrite needed)
- Heavy System.Web dependencies (middleware rewrite)
- COM+ usage (limited support)
- SQL CLR stored procedures (may need redesign)
- Custom MSBuild targets (review compatibility)
- Application Domains / .NET Remoting (architecture redesign needed)
Migration Checklist
Pre-Migration
Execution (One Step at a Time)
Post-Migration Validation
Deployment
Estimated Timeline
| Size | Projects | Duration |
|---|
| Small | 5-10 | 2-4 weeks |
| Medium | 10-30 | 2-3 months |
| Large | 30+ | 6-12 months |
GitHub Copilot accelerates by 30-50% compared to manual upgrades.