A .NET Framework application can contain years of business rules that nobody wants to rediscover during a rewrite. The team wants a maintained runtime and a cleaner build. Operations still needs the reports, imports, integrations and scheduled tasks to behave as expected. A useful migration plan connects those goals before anyone starts changing project files.
The first deliverable should be a dependency map and a working test baseline. That tells you which parts can move directly, which need replacement and which should stay behind temporarily. Do not bundle a new user interface, a new database and a new hosting platform into the same release unless there is a clear reason.
Define which migration you are actually doing
Updating an application within .NET Framework is different from moving it to modern .NET. Microsoft describes changes to project format, APIs and sometimes the application model when porting. Libraries and console applications can have a different path from a web application that needs to move from ASP.NET to ASP.NET Core.
Write down the source runtime, intended target, operating system and application type. Choose a target with an appropriate support window and dependency coverage. Verify the support policy when you schedule the work rather than copying a target version from an old tutorial. Keep runtime support and operating system support as separate checks.
| Application component | What to investigate | Evidence needed before release |
|---|---|---|
| Class libraries | Dependencies and available APIs | Tests on the target runtime |
| Windows Forms or WPF | Controls, native libraries and Windows dependencies | Desktop workflows on supported Windows environments |
| ASP.NET application | Application model changes for ASP.NET Core | Requests, identity and background work behave correctly |
| Legacy infrastructure code | Remoting, AppDomains and unavailable technologies | Replacement design and integration tests |
| Build and deployment | Project format, packaging and configuration | Repeatable installation and rollback |
Inventory dependencies before editing code
List every project, package, referenced assembly and native dependency. Record its owner, where it runs and what calls it. Include third-party UI controls, reporting components and internal libraries that are copied into a folder rather than restored through the build. An application is only as portable as the dependencies used by its real execution paths.
Microsoft notes that compatibility mode can allow references to some .NET Framework libraries from modern .NET. That does not guarantee runtime behavior. A library can reference an API that is absent or restricted on your target platform. Test the path that uses it, not only the project that compiles against it.
Classify each dependency as supported, replaceable, removable or unresolved. For unresolved items, identify a small test that answers the compatibility question. Keep a blocked component out of the critical path where possible. A written exception with an owner is better than assuming a package restore proves the problem is solved.
Separate modern .NET from cross-platform work
Windows Forms and WPF are available in modern .NET, but both remain Windows-only. A desktop runtime migration should not be described as a Linux migration. Likewise, a call into a Windows-specific native library still needs Windows even when the surrounding code targets modern .NET.
The Microsoft.Windows.Compatibility package provides a large part of the older API surface. It can help when an application needs to remain on Windows. It does not turn every Windows dependency into a portable implementation. Decide whether the immediate goal is a runtime upgrade on Windows or a broader platform change, and test accordingly.
Find technologies that require a design decision
Microsoft lists .NET Remoting, creating additional application domains, Code Access Security and System.EnterpriseServices among technologies not supported in modern .NET. Windows Workflow Foundation also needs an alternative. These are not ordinary namespace substitutions. Find where the application depends on their behavior and decide what boundary replaces it.
For example, Microsoft points to separate processes or containers for isolation instead of creating additional AppDomains. That replacement changes deployment and communication concerns. Document what was isolated, what data crossed the boundary and what happens when that component fails. Do not silently remove an isolation mechanism just to make the build pass.
Use tooling to produce reviewable changes
Microsoft now marks .NET Upgrade Assistant as deprecated and directs teams to GitHub Copilot modernization. Check the current guidance for your project type before choosing the upgrade workflow. Older examples may still describe the retired tool as the default starting point.
Use automation for assessment, project changes and code fixes that can be reviewed. Keep the resulting changes small enough to understand. Separate dependency updates from unrelated refactoring. An engineer should be able to explain why each change is required and what test covers it. Generated changes need the same review as handwritten changes.
SDK-style projects use a different project format. A successful conversion is a useful milestone, but it is not an acceptance test. Confirm custom build steps, copied resources, configuration files and packaging behavior. A build that succeeds on one developer laptop is not yet a deployable release.
Test business behavior on the target runtime
Create a baseline from the current application before migration. Use representative requests, input files and scheduled operations with sensitive data removed or protected. Record expected outputs and the failure behavior that operators rely on. Include invalid input and interrupted work as well as successful cases.
- Run unit tests for reusable business logic.
- Run integration tests against the actual external services or suitable test environments.
- Compare important reports and exported files with the baseline.
- Check identity, permissions, configuration and environment differences.
- Rehearse deployment, health checks and rollback with operations.
For web applications, test authentication and request behavior rather than checking only the landing page. For desktop applications, walk through representative screens, printing and local integration paths where those features exist. For workers, verify restart behavior and whether a repeated job creates duplicate effects.
Mistakes to watch for
- Equating compilation with runtime compatibility.
- Assuming a Windows desktop application becomes cross-platform.
- Replacing a legacy component without preserving its failure behavior.
- Combining runtime migration with unrelated business changes.
- Accepting generated fixes without tests that exercise the changed path.
Make cutover a separate release decision
Before production, record the exact build, configuration, deployment steps and rollback conditions. Decide who can approve release and who monitors the workflows users depend on. If the new application writes data that the old one cannot read, a code rollback alone may not restore service. Resolve that risk before switching traffic or workloads.
Keep unresolved differences visible. Every exception should have an impact, owner and decision. Release when the team can demonstrate the required behavior, not when the migration tool stops finding edits.
How Remqa helps with .NET migrations
Remqa can help assess dependencies, move code in reviewable steps and test the behavior needed for release. Explore code migration, read the AI-assisted legacy code migration guide, or discuss related database work through data migration. Bring the application structure and a list of the workflows your team cannot afford to break.