Legacy .NET to Modern .NET Migration Guide
A practical, step-by-step guide to migrating from .NET Framework 4.x to modern .NET -- covering WebForms, MVC 5, WCF, Windows Services, and everything in between.
The .NET Landscape in 2026
Microsoft has drawn a clear line in the sand. .NET Framework 4.8.x is the final version -- it will receive security patches but zero new features. Meanwhile, modern .NET (the unified .NET 5+ lineage) gets annual releases, performance improvements, and the full attention of the ecosystem.
If your organization is still running .NET Framework applications, you are not alone -- but you are on borrowed time. Every month you delay makes the migration harder as the gap between legacy and modern widens and NuGet package authors drop .NET Framework support.
Final version. Security patches only. No new features, no performance work, no new APIs. Effectively in maintenance mode since 2019.
Long-term support until November 2026. The safe migration target for most teams today. Mature ecosystem, proven in production.
Standard-term support (18 months). Good for greenfield projects. Includes AOT improvements, performance gains, and new runtime features.
Expected November 2025. Next long-term support release. If you are starting a migration now, target .NET 10 for maximum support runway.
Key decision: If you are starting a new migration in 2026, target .NET 8 for stability or .NET 10 for the longest support window. Avoid .NET 9 for migration targets since its shorter support cycle means you would need to upgrade again sooner.
Assessment Checklist: What Are You Migrating From?
Before choosing a migration strategy, you need to know exactly what you are working with. Each legacy technology has a different migration path, different pain points, and different timelines.
ASP.NET WebForms
Hardest Migration- .aspx files with server controls
- ViewState and postback model
- Code-behind files (.aspx.cs)
- UpdatePanel for "AJAX"
- Master pages and user controls
Difficulty: No direct upgrade path. Requires a rewrite using Blazor or Razor Pages.
ASP.NET MVC 5
Most Direct Path- Controllers, Views, Models pattern
- Razor views (.cshtml)
- Web API controllers
- Attribute routing
- Dependency injection (often third-party)
Difficulty: Moderate. Controllers and views translate closely to ASP.NET Core MVC.
WCF Services
Significant Rework- Service contracts and data contracts
- SOAP/XML bindings
- .svc files and service host
- Duplex/callback contracts
- Message-level security
Difficulty: No direct equivalent in modern .NET. Replace with gRPC or minimal APIs.
Windows Services
Clean Replacement- ServiceBase class inheritance
- OnStart/OnStop lifecycle
- Timer-based background work
- Windows event log integration
- SC.exe / installutil deployment
Difficulty: Straightforward. Worker Services are the modern equivalent with cleaner DI and hosting.
Migration Paths by Technology
Each legacy .NET technology has a recommended modern replacement. The migration difficulty varies dramatically -- from near-direct ports (MVC 5) to complete rewrites (WebForms).
WebForms to Blazor or Razor Pages
Recommended approach: Strangler Fig pattern
Why You Cannot Upgrade In-Place
- WebForms relies on ViewState and the page lifecycle -- concepts that do not exist in modern .NET
- Server controls (GridView, Repeater, etc.) have no direct equivalents
- The postback model is fundamentally incompatible with modern request handling
- Master pages must be rebuilt as layouts
The Strangler Fig Approach
- Run the legacy WebForms app alongside a new Blazor/Razor Pages app behind a reverse proxy
- Migrate one page or feature at a time, routing new traffic to the modern app
- Share authentication via a common identity provider (Azure AD, IdentityServer)
- Decommission WebForms pages as their replacements are production-ready
ASP.NET MVC 5 to ASP.NET Core MVC
The most direct migration path in the .NET ecosystem
What Translates Directly
- Controllers and action methods (minor signature changes)
- Razor views (.cshtml) -- mostly copy-paste with namespace updates
- Model binding and validation attributes
- Attribute routing
- Filters (action filters, exception filters)
What Changes
-
Global.asaxreplaced byProgram.cs+ middleware pipeline -
web.configreplaced byappsettings.json - Third-party DI (Ninject, Autofac) replaced by built-in DI
-
HttpContext.Currentreplaced by injectedIHttpContextAccessor - Bundling changes (BundleConfig to webpack, Vite, or LibMan)
Pro tip: Microsoft provides the official migration documentation and the .NET Upgrade Assistant tool that automates many of the mechanical changes. Start there and handle the manual work afterward.
WCF to gRPC or Minimal APIs
Choose based on whether clients are internal or external
gRPC (Internal Services)
Best replacement for service-to-service WCF calls. Binary protocol, code-gen from .proto files, streaming support.
- Strongly typed contracts (like WCF)
- Bi-directional streaming (replaces duplex)
- 5-10x faster than SOAP/XML
- Not browser-friendly without gRPC-Web
Minimal APIs / REST (External)
Best for WCF services consumed by browsers, mobile apps, or third parties. JSON over HTTP/HTTPS.
- Universal client compatibility
- OpenAPI/Swagger documentation
- Simpler debugging and testing
- No built-in streaming contracts
CoreWCF note: The community-maintained CoreWCF project provides a compatibility layer for running WCF services on modern .NET. It is a viable stopgap for migrating the hosting model while you plan the full API redesign, but it is not a long-term solution.
Windows Services to Worker Services
Cleanest migration path -- modern .NET Worker Services are a direct upgrade
Remove
ServiceBaseinheritanceOnStart/OnStopoverrides- ProjectInstaller class
- installutil.exe deployment
Add
BackgroundServicebase classExecuteAsyncmethod- Built-in DI container
IHostedServicefor multiple workers
Bonus
- Runs on Linux (containerizable)
- Health checks built-in
- Graceful shutdown handling
- Can still install as Windows Service
Entity Framework 6 to EF Core
Data access migration -- often the biggest hidden effort
What Carries Over
- DbContext pattern (with namespace changes)
- Code-first migrations (recreate migration history)
- LINQ queries (mostly compatible)
- Data annotations for validation
Breaking Changes to Watch
- Lazy loading is opt-in (not default)
-
ObjectContextis gone --DbContextonly - Complex type mapping differences
- EDMX (database-first visual designer) not supported
Migration strategy: If you use database-first with EDMX, scaffold your models using dotnet ef dbcontext scaffold to generate code-first models from your existing database. Then compare with your EF6 models and reconcile differences.
10-Step Migration Process
Regardless of which legacy technology you are migrating, this process gives you a repeatable, low-risk path from old to new.
Inventory and Dependency Map
Catalog every project in your solution. Map dependencies between them. Identify NuGet packages, third-party DLLs, COM references, and system dependencies. You cannot migrate what you do not understand.
Run the .NET Upgrade Assistant
Microsoft's free tool analyzes your project and automates mechanical changes: project file conversion, namespace updates, NuGet package replacements. It handles the tedious work so you can focus on the architectural decisions.
Migrate Shared Libraries First
Convert your lowest-level class libraries to .NET Standard 2.0 or multi-target. This lets them be consumed by both old and new projects during the transition, enabling incremental migration instead of a big-bang switch.
Replace web.config with appsettings.json
Extract connection strings, app settings, and custom configuration sections into the new configuration system. Use the Options pattern for strongly-typed settings. Secrets go to User Secrets or Azure Key Vault -- not into JSON files.
Migrate Authentication
This is often the most complex step. Forms Authentication and Windows Authentication need to be replaced with ASP.NET Core Identity, OAuth 2.0/OIDC, or Azure AD. Plan for a shared auth mechanism during the transition period.
Port the Data Access Layer
Migrate from EF6 to EF Core or from raw ADO.NET to Dapper/EF Core. Rebuild your migrations from the current database schema. Test every query -- EF Core generates different SQL than EF6, and subtle behavior differences can cause data bugs.
Rebuild the Middleware Pipeline
Replace HTTP modules and handlers with ASP.NET Core middleware. The pipeline order matters -- authentication before authorization, error handling at the top, static files early. Map your old Global.asax events to their middleware equivalents.
Parallel Testing with Feature Flags
Run the old and new systems in parallel. Use feature flags to route a percentage of traffic to the new system. Compare responses, measure performance, and catch regressions before you cut over completely.
Cutover and Decommission
Once the new system passes all tests and handles production traffic cleanly, switch 100% of traffic. Keep the old system running in read-only mode for 2-4 weeks as a safety net. Then decommission it -- do not leave zombie legacy apps running.
Post-Migration Optimization
A straight port is not the finish line. Once you are on modern .NET, take advantage of features you could not use before: native AOT compilation, minimal APIs for simple endpoints, source generators for performance, and the latest C# language features.
NuGet Package Compatibility
NuGet packages are the number one source of unexpected migration blockers. A package that works on .NET Framework may not have a modern .NET version -- or may have breaking API changes in the version that does.
Common Package Migration Issues
| Legacy Package | Modern Replacement | Migration Notes |
|---|---|---|
Newtonsoft.Json | System.Text.Json | Built-in, faster, but less feature-rich. Newtonsoft still works on modern .NET if you need it. |
log4net / NLog | Serilog or Microsoft.Extensions.Logging | Built-in logging abstractions with structured logging. NLog/Serilog both support modern .NET. |
Ninject / Unity | Built-in DI | ASP.NET Core has built-in DI. Autofac and Scrutor add advanced features if needed. |
AutoMapper | Mapster or manual mapping | AutoMapper works on modern .NET but has licensing changes. Mapster is faster and free. |
System.Web | Microsoft.AspNetCore.* | The biggest breaking change. Everything in System.Web is replaced by ASP.NET Core equivalents. |
System.Drawing | SkiaSharp or ImageSharp | System.Drawing is Windows-only. Cross-platform alternatives are required for Linux/container deployments. |
Check compatibility early: Run dotnet tool install --global apicompat or use the .NET Upgrade Assistant to scan your packages before you start. Discovering an incompatible package mid-migration is painful -- discovering it early is a planning exercise.
Configuration Migration: web.config to appsettings.json
The .NET Framework configuration system (XML-based web.config with ConfigurationManager) is replaced by a layered JSON-based system. This is more flexible but requires rethinking how you structure settings.
Old: web.config
-
<appSettings>for key-value pairs -
<connectionStrings>section - Custom configuration sections (IConfigurationSectionHandler)
-
ConfigurationManager.AppSettings["key"] - Transform files (web.Debug.config, web.Release.config)
New: appsettings.json
- Hierarchical JSON structure (nested objects)
-
ConnectionStringssection in JSON - Options pattern with POCO classes (
IOptions<T>) -
IConfigurationinjected via DI - Environment-specific overrides (appsettings.Production.json)
Do not forget: web.config in .NET Framework also controlled IIS settings (authentication modes, custom errors, URL rewrite rules). In modern .NET, these are handled by middleware configuration in Program.cs or a separate web.config that only configures the ASP.NET Core Module in IIS.
Authentication Migration
Authentication is frequently the hardest part of a .NET migration because it touches every page, every API call, and every user session. Get this wrong and you lock users out -- get it right and nobody notices.
Forms Authentication
ASP.NET Core Identity + Cookie AuthThe closest equivalent is ASP.NET Core Identity with cookie authentication. The membership database schema is different, so plan a one-time user data migration. Password hashes from the old Membership provider are not compatible -- you will need to implement a custom hasher that verifies old hashes and re-hashes on first login.
Transition tip: During migration, share authentication between old and new apps using a common cookie domain and data protection key store.
Windows Authentication
Negotiate/NTLM or Azure AD/Entra IDFor intranet apps, ASP.NET Core still supports Windows Authentication via the Negotiate handler. For apps moving to the cloud or needing cross-platform support, migrate to Azure AD (now Entra ID) with OpenID Connect. This also enables SSO across your application portfolio.
Custom/Homegrown Auth
OAuth 2.0 / OpenID ConnectIf you rolled your own authentication (custom token validation, hand-built login pages with manual hash comparison), this migration is your chance to move to standards. Use IdentityServer (Duende), Auth0, or Azure AD B2C. The migration effort is higher, but you eliminate an entire category of security debt.
Testing Strategy During Migration
Migration without a testing strategy is a recipe for production incidents. You need to verify that the new system behaves identically to the old one -- and you need to catch differences before users do.
Characterization Tests
Before touching any code, write tests against the legacy system that capture its current behavior -- including bugs. These tests become your safety net. If the migrated system passes them all, you have behavioral equivalence.
Shadow Testing
Route production traffic to both the old and new systems simultaneously. Compare responses. Log differences. This catches edge cases that unit tests miss -- real users produce inputs that test data never imagines.
Performance Benchmarks
Benchmark the legacy system's response times, throughput, and memory usage before migration. The new system should meet or exceed these numbers. Modern .NET is typically 2-5x faster, but a bad migration can erase those gains.
End-to-End Tests
Use Playwright or Selenium to automate critical user journeys: login, CRUD operations, reporting, checkout flows. These tests should work against both the old and new systems without modification.
Data Integrity Checks
After migrating data access code, run both systems against a copy of the production database. Compare query results row by row. EF Core can generate different SQL than EF6 -- especially for complex joins and grouping operations.
Rollback Testing
Practice the rollback procedure before you need it. Can you switch traffic back to the old system in under 5 minutes? Are database changes backward-compatible? Test this in staging -- not during a production incident at 2 AM.
Related Resources
Frequently Asked Questions
It depends on what you are migrating. An MVC 5 app with clean separation of concerns can often be migrated in 2-4 months. A large WebForms application with deep ViewState dependencies and custom server controls typically takes 6-18 months using the strangler fig approach. Windows Services are usually the fastest at 1-2 weeks per service. The biggest variable is not the technology -- it is the quality of the existing codebase and how tangled the dependencies are.
Skip intermediate versions and go directly to the latest LTS release (.NET 8 or .NET 10). There is no benefit to stepping through .NET Core 3.1, .NET 5, .NET 6, and so on. The .NET Upgrade Assistant is designed to handle the full jump. The only exception is if you have a dependency that only supports a specific intermediate version -- in that case, migrate to that version first, then plan a second hop.
Yes, and you should. The strangler fig pattern depends on running both systems simultaneously. Use a reverse proxy (IIS URL Rewrite, YARP, or nginx) to route requests to the appropriate backend. Share authentication state using a common cookie domain, shared database, or an external identity provider. Shared libraries should target .NET Standard 2.0 so both runtimes can consume them.
WinForms and WPF are both supported on modern .NET (they shipped with .NET Core 3.0+), so you can migrate them without switching UI frameworks. The .NET Upgrade Assistant handles most of the project file and namespace changes. .NET MAUI is only worth considering if you need cross-platform (iOS, Android, macOS) support -- it is not a direct replacement for WinForms or WPF, and the migration effort is significantly higher.
Frame it as risk mitigation and cost savings, not technical improvement. Calculate the cost of running on unsupported software (no security patches, increasing CVEs, compliance risk). Show the developer productivity tax (time spent on workarounds, inability to use modern NuGet packages, difficulty hiring developers who want to work on legacy .NET). Quantify the performance gains -- modern .NET is 2-5x faster, which translates directly to infrastructure cost savings. See our Selling to Management guide for a full framework.
Underestimating the scope. The code migration is the visible part, but the hidden work includes: configuration migration, authentication rework, CI/CD pipeline changes, deployment infrastructure updates (IIS configuration, container setup), monitoring and logging reconfiguration, and team training. The code port might be 40% of the total effort -- the remaining 60% is everything around it. Budget for the full picture, not just the code changes.
Ready to Start Your .NET Migration?
Migration is a journey, not a weekend project. Start with an assessment, build your business case, and execute incrementally.