Pre-Migration Checklist for Moving from .NET Framework to .NET
· Updated: · Go Komura · .NET, .NET Framework, C#, Modernization, Windows Development, Migration
Revision history (1 updates, last updated Sep 1, 2026)
A log of the changes made to this article. Where a pre-update version was archived, it stays readable at a permanent DOI link.
- Retranslated as a full translation of the Japanese original. The previous English version was an abridgement that carried only part of the source, so sections, tables, Mermaid diagrams, figure captions and FAQ entries were missing. All of them have been restored to match the Japanese original, and the technical claims are the same as in the Japanese version. Read the version before this update (DOI: 10.5281/zenodo.21614503)
- First published
Cite this article(DOI: 10.5281/zenodo.21614502)
This article is archived on Zenodo. Below are both the DOI that always resolves to the latest version and the DOI pinned to the version you are reading.
Go Komura (2026). Pre-Migration Checklist for Moving from .NET Framework to .NET. KomuraSoft LLC. https://doi.org/10.5281/zenodo.21614502 https://comcomponent.com/en/blog/2026/03/15/003-dotnet-framework-to-dotnet-premigration-checklist/
- DOI (latest version)
- 10.5281/zenodo.21614502
- DOI (this version)
- 10.5281/zenodo.22217147
Download the Excel checklist with Japanese and English sheets
The file has two sheets, Checklist-ja and Checklist-en, and turns the pre-start checklist of chapter 13 into 34 items across 7 categories: policy / preparing the current .NET Framework side / app type and technology choices / unsupported technologies and APIs to watch / Windows-only dependencies / shared libraries and data access / operations and build. The Status and Notes columns are left empty, so you can use it as a per-project inventory sheet as is. Skimming it before reading the article should make the flow up through chapter 13 easier to follow.
Change TargetFramework in the .csproj to net10.0, update a few NuGet packages, and once the build passes, you are done.
…If that were the migration, life would be pretty peaceful. In reality, it usually is not.
.NET Framework codebases are full of dormant assumptions you normally never think about: System.Web, WCF, Web Forms, old packages.config files, web.config.install.xdt, native DLLs, COM / ActiveX, third-party controls that only work at design time, implicit x86 assumptions, designer-dependent ResX, old serializers, and more.
So what really matters in a .NET Framework to .NET migration is the inventory work before you start implementing. If you can break the issues down before starting, the migration stops being a big gamble and becomes a list of items to knock down in order.
flowchart TB
accTitle: Taking stock changes the character of the migration
accDescr: A diagram showing that if you take stock before starting implementation and break the issues down, the migration becomes a job of knocking items down in order rather than a big gamble.
hid1["Dormant implicit assumptions"] --> inv1["Break the issues down by taking stock before starting"]
inv1 --> tsk1["It becomes a job of knocking items down in order"]
hid1 -.->|"Skip taking stock"| bet1["The migration becomes a big gamble"]
Figure 1: Whether you can break the issues down before starting decides whether the migration is a gamble or ordinary work.
This article summarizes what you should verify before migrating an existing .NET Framework 4.x line-of-business application to current .NET. The main targets are around here:
- Class libraries
- Console applications
- Windows services
- WinForms / WPF
- ASP.NET Framework (MVC / Web API / Web Forms)
- Applications that use WCF
- Applications that use EF6
This was written as of 2026-03-15. Support windows and official tooling recommendations change, so if you are reading this much later, check the official information as well.
1. The Conclusion First
First, the conclusions that are hard to get wrong.
- Clean up the .NET Framework side before migrating. Microsoft’s official guide also recommends upgrading to .NET Framework 4.7.2 or later before porting, and completing
PackageReferenceconversion, SDK-style conversion, and dependency updates first. - Difficulty is determined by app model, not by code volume. Class libraries and console apps are relatively light; ASP.NET Framework, Web Forms, WCF servers, and WF (Workflow Foundation) tend to be heavy.
- WinForms / WPF can migrate to .NET but remain Windows-only. Misunderstand this and you step on the classic landmine of migrating only to find the app will not run in a Linux container.
- ASP.NET Framework to ASP.NET Core is effectively an architecture migration. Small apps can sometimes go in one shot, but for large production systems it is safer to plan on an incremental migration.
- WCF and EF6 can sometimes be decoupled from the runtime migration. WCF clients have supported packages for .NET, and EF6 can be migrated to EF Core separately after the move to modern .NET.
- On the other hand, AppDomain creation, .NET Remoting, CAS (Code Access Security), COM+, Workflow Foundation, and BinaryFormatter dependencies are red lights. Find them early, or the effort explodes later.
packages.config/install.ps1/XDT(XML Document Transform, the mechanism that transforms files such asweb.config) /contentassets / native DLLs / COM / ActiveX / x86 assumptions tend to fail at runtime or design time even when the build passes, so flush them out before starting.- As of 2026-03, .NET 10 is the LTS. For new migrations, it is natural to target the current LTS as the landing point.
- A migration without tests, measurements, and a rollback plan is dangerous. Migration is less an implementation task than a job of making assumptions visible, one at a time.
In the diagram a solid line marks a relation that always holds and a dashed line marks a conditional one (the conditions are given per relation on the detail page). The full list of relations (30 in total, with evidence and certainty) and the definitions of the main concepts are collected on the knowledge map detail page (in Japanese). Data: JSON-LD / Turtle
2. First Decide Whether You Should Migrate Right Now at All
The first thing to decide is not “how to migrate” but whether this app really should be migrated right now.
Leave this vague and you tend to end up with a migration that is technically correct but too heavy for the business, or the opposite: postponing far too long when migration is clearly the right call.
flowchart TB
accTitle: The question to settle first
accDescr: A diagram showing that before deciding how to migrate you should decide whether this app really should be migrated now, and that leaving it vague tends to produce either a migration too heavy for the business or too much postponement.
q1["Should this really be migrated now"] -->|"Decide this first"| how1["How to migrate"]
q1 -.->|"Proceed while it stays vague"| bad1["Too heavy a migration or too much postponement"]
Figure 2: Unless you settle whether to migrate now before asking how to migrate, the decision swings to one extreme or the other.
2.1 Staying on .NET Framework Is a Perfectly Valid Decision
.NET Framework 4.8.1 continues to be supported as long as it runs on a supported Windows. In other words, it is not as simple as “if you do not move everything to modern .NET right now, you are immediately at risk.”
However, staying has clear constraints.
- You cannot escape being Windows-only
- You keep carrying ASP.NET Web Forms and old server stacks
- It is harder to benefit from new .NET performance improvements, language features, and the ecosystem
- You drift away from the modern assumptions of cloud, containers, and CI/CD
Conversely, if dependencies like the following are strong, it is rational to consolidate on .NET Framework 4.8.1 for stable operation for now, while planning the replacement on a separate track.
flowchart TB
accTitle: Staying put as a rational choice
accDescr: A diagram showing that with strong legacy dependencies such as Web Forms, WCF server compatibility, or COM+, it is rational to consolidate on .NET Framework 4.8.1 for stable operation for now while planning the replacement on a separate track.
dep1["Strong legacy dependencies exist"] --> stay1["Consolidate on 4.8.1 and run it stably for now"]
stay1 --> plan1["Plan the replacement on a separate track"]
stay1 -.-> lim1["Constraints such as Windows-only remain"]
Figure 3: When the dependencies run deep, stable operation on 4.8.1 plus a replacement plan becomes the realistic pair.
- A large stock of Web Forms screens
- Strict WCF server compatibility must be preserved
- Deep Workflow Foundation or COM+ dependencies
- Third-party design-time components do not support modern .NET
- The business does not allow major specification changes
2.2 What Changes with Each Option
| Option | What gets better | What remains / what you give up | Good fit |
|---|---|---|---|
| Stay on .NET Framework 4.8.1 | Easy to keep existing assets intact and operate stably | Windows-only, old app models, limits on modernization | Strong legacy dependencies; the business priority is stable operations for now |
| Migrate to modern .NET but stay on Windows | Runtime and toolchain modernized. Big gains in performance, developer experience, SDK style | Windows API dependencies remain. Not cross-platform | WinForms / WPF, Windows services, business apps using Windows APIs |
| Migrate to modern .NET with Linux / containers / cloud in future scope | More freedom in deployment targets. Easier to modernize the operations model | Windows-only APIs and app models must be peeled off first | You want to shift the server side toward the cloud and refresh infrastructure too |
What matters is deciding not whether you want to migrate but where you want to land after the migration, first.
flowchart TB
accTitle: What to decide is the landing point
accDescr: A diagram showing that what matters is deciding where you want to land after the migration rather than whether you want to migrate.
ask1["Do you want to migrate"] -.->|"Not enough as a question"| dec1["What to decide first"]
land1["Where you want to land after the migration"] -->|"Decide this"| dec1
dec1 --> path1["The options and the issues to examine fall into place"]
Figure 4: Change the question from whether you want to migrate to where you want to land, and the options fall into place.
3. Four Policies to Decide Up Front
3.1 The Target .NET Version
At the time of writing, under Microsoft’s support policy, .NET 10 is the LTS. Meanwhile, .NET 8 LTS and .NET 9 STS are both scheduled to reach end of support on 2026-11-10. An STS release lands on the same date as an LTS one because the STS support window was extended from 18 months to 24 months. Counting with the older assumption that STS means 18 months makes .NET 9 look like it ends on 2026-05-12, so when you use a date as the basis for a decision, confirm it against the official lifecycle information.
So if you are now starting a fresh migration off .NET Framework, barring exceptional circumstances, landing on the current LTS is the natural choice.
The practical reasoning here is simple.
- For a small migration you want done quickly, land directly on the current LTS
- For a core system intended for long-term operation, still treat the current LTS as the baseline
- “We want the previous LTS because of an existing library” is a legitimate reason, but judge it by checking the actual maintenance end date
flowchart TB
accTitle: How to think about the target version
accDescr: A diagram showing that both small migrations and core systems treat the current LTS as the baseline, and that if an existing library pushes you toward the previous LTS you decide by checking the maintenance end date.
tgt1["Choose the target .NET version"] -->|"Baseline"| lts1["Land on the current LTS"]
tgt1 -->|"Constraints from existing libraries"| old1["Consider the previous LTS too"]
old1 --> dt1["Check the maintenance end date and decide"]
Figure 5: The current LTS is the default landing point, and choosing an older LTS has to be justified by the end-of-support date.
3.2 Stay Windows-Only, or Aim for Cross-Platform Later?
This decision dramatically changes which issues you need to look at.
- If you stay Windows-only, you can take the pragmatic route of modernizing the runtime first, using WPF / WinForms and the Windows Compatibility Pack.
- If you are aiming for Linux / containerization later, you need to inventory Windows-bound APIs early:
System.Drawing.Common, the registry, WMI, EventLog, Windows services, COM, Office Interop, and so on.
Start migrating without deciding this, and midway through, the conversation gets twisted: “Was Windows-only fine after all?” “No wait, we wanted to run in containers.”
flowchart TB
accTitle: The Windows-only versus cross-platform fork
accDescr: A diagram showing that staying Windows-only lets you take the pragmatic route of modernizing the runtime first with the compatibility pack, while aiming at Linux or containers later requires inventorying Windows-bound APIs early.
aim1["Which are you aiming for"] -->|"Stay Windows-only"| wn1["Modernize the runtime with the compatibility pack"]
aim1 -->|"Containers later as well"| xp1["Inventory Windows-bound APIs early"]
aim1 -.->|"Start without deciding"| twist1["The conversation gets twisted midway"]
Figure 6: Without settling this fork first, the issues to examine never firm up and the plan gets twisted midway.
3.3 One Shot or Incremental?
There are broadly three migration shapes.
- Near in-place, all-at-once migration
- Side-by-side migration running old and new in parallel
- Incremental migration, moving over route by route or library by library
Especially for ASP.NET Framework apps, Microsoft’s guidance explicitly recommends incremental migration. If you cannot stop production, have many features, and have many surrounding dependencies, it is less strained to design for incremental migration from the start.
flowchart TB
accTitle: Three shapes of migration
accDescr: A diagram showing the three migration shapes of a near in-place all-at-once migration, a side-by-side migration running old and new in parallel, and an incremental migration by route or library, and that incremental migration is the premise when production cannot be stopped.
typ1["Choose the migration shape"] --> t1["All-at-once migration (near in-place)"]
typ1 --> t2["Side-by-side, old and new in parallel"]
typ1 --> t3["Incremental migration by route / library"]
t3 -.-> fit1["The premise when production cannot stop"]
Figure 7: There are three shapes, and a large production system you cannot stop starts from incremental migration.
3.4 What to Exclude from This Migration
Migrations tend to fail because too much gets piled on.
For example, doing all of these at once tends to get heavy:
- .NET Framework to .NET
- ASP.NET Framework to ASP.NET Core
- EF6 to EF Core
- Windows servers to Linux containers
- Changing the authentication infrastructure
- Changing the logging / monitoring infrastructure
- Migrating the database
Of course, all of them may eventually be necessary. But whether they must be done at the same time is a separate question.
In reality, separating things like this works better:
- First modernize the runtime and project structure
- Then move the app model
- Finally update the ORM, authentication, cloud, and monitoring
flowchart TB
accTitle: An order of separation that avoids piling everything on
accDescr: A diagram showing the order of separation in which the runtime and project structure are modernized first, then the app model is moved, and finally the ORM, authentication, cloud, and monitoring are updated.
d1["Modernize the runtime and structure"] --> d2["Move the app model"]
d2 --> d3["Update ORM, authentication, cloud, monitoring"]
d1 -.->|"Pile them on at once"| hv1["The migration gets heavy and fails easily"]
Figure 8: Instead of doing everything at once, separating runtime, app model, and the surrounding systems keeps the work from getting heavy.
4. The Foundation to Prepare Before Starting
Microsoft’s pre-porting guide is quite practical. In essence: before migrating, bring your current .NET Framework projects up to a modern entry point.
4.1 Upgrade to .NET Framework 4.7.2 or Later
The official guide recommends targeting .NET Framework 4.7.2 or later before porting. The reason is that even when .NET Standard does not carry an existing API as is, it becomes easier to move toward newer API alternatives.
In practice, it is clearer to use 4.8.1 as the baseline if possible.
- Straightforward from a support standpoint
- Easy to treat as the final stable point on the .NET Framework side
- Makes the policy of “first clean up on the current Framework side” easy to establish
What changes if you do this first
- Handling of
.NET Standard 2.0shared libraries stabilizes more easily - Noise originating from the old runtime is reduced up front
- It becomes easier to tell whether a compatibility issue is caused by “the Framework being old” or by “the move to modern .NET”
4.2 Move to PackageReference
The pre-porting guide recommends converting references to the PackageReference format.
Doing this first significantly improves visibility into dependency management.
What changes with PackageReference
- Package references are consolidated into the
csproj - Transitive dependencies become easier to see
- The restore assumptions line up with the modern .NET side
- Better compatibility with the CLI / CI
However, there are landmines here.
Typical landmines
The official NuGet documentation explicitly lists these constraints for migrating from packages.config to PackageReference:
- Visual Studio’s built-in migration cannot be used for ASP.NET projects
- Packages that depend on
install.ps1/uninstall.ps1may not work as expected - Assets in the
contentfolder may be ignored - XDT transforms such as
web.config.install.xdtare not applied - Packages with old assembly layouts directly under
libmay not resolve well
In other words, do not think of it as merely changing the package format.
Classic ASP.NET in particular had a strong culture of rewriting web.config at NuGet package install time, so implicit assumptions surface easily during migration.
flowchart TB
accTitle: Implicit assumptions exposed by the move to PackageReference
accDescr: A diagram showing that migrating from packages.config to PackageReference can leave install.ps1, XDT transforms, and content folder assets unapplied, which exposes the implicit assumption that web.config was rewritten at install time.
cv1["Convert to PackageReference"] --> np1["install.ps1 may not run"]
cv1 --> nx1["XDT transforms are not applied"]
cv1 --> nc1["content assets may be ignored"]
np1 --> exp1["Implicit assumptions get exposed"]
nx1 --> exp1
nc1 --> exp1
Figure 9: Even when you mean to change only the format, everything that relied on install-time machinery surfaces at once.
What to use for the conversion
Manual work is not the only option. Each tool covers a different range, though.
| Tool | What it does | Assumptions and caveats |
|---|---|---|
| The Visual Studio conversion feature | In Solution Explorer, right-click References or packages.config and choose Migrate packages.config to PackageReference... to run the conversion |
Visual Studio 2017 15.7 or later. Not available for ASP.NET and C++ projects. If the menu item does not appear, open NuGet restore or the package manager once and then right-click again |
| .NET Upgrade Assistant | Analyzes and upgrades projects in one flow | The official documentation marks it as deprecated and points you to GitHub Copilot app modernization |
| GitHub Copilot app modernization | Supports assessment, planning, code fixes, and validation. It is the center of the current official guidance | As covered in 4.5, it assumes Visual Studio, Copilot, and C# code |
| Manual SDK-style conversion | Create a new csproj and move over only the entries you need |
For projects with few files, this is sometimes the fastest route |
The Visual Studio conversion feature backs up the project before it runs and produces a conversion report at the end, covering top-level dependencies, transitive dependencies, and detected compatibility issues. To roll back, the documentation describes copying csproj and packages.config back from the backup folder and running update-package -reinstall in the Package Manager Console.
In other words, you can try it in a form you can undo. For a pre-start estimate, converting a single project and reading the report is the fastest way in.
flowchart TB
accTitle: Trying the conversion in a form you can undo
accDescr: A diagram showing that the Visual Studio conversion feature creates a backup before converting and produces a conversion report, so you can convert one project first, read the report, and restore from the backup if needed.
bk1["A backup is created"] --> cv2["Convert one project only"]
cv2 --> rp1["Read the conversion report"]
rp1 -->|"If there are problems"| rv1["Restore the files and reinstall the packages"]
rp1 -->|"If there are none"| go1["Roll it out to the other projects"]
Figure 10: The conversion comes with a backup and a report, so start with one project to gauge the work.
4.3 Move to SDK-Style Projects
The pre-porting guide also recommends converting to the SDK-style project format.
This pays off substantially.
What changes with SDK style
- The
csprojbecomes dramatically more concise - Plays well with
PackageReference - Multi-targeting becomes easy
- Easier to center CI/CD on
dotnet build/dotnet test/dotnet publish - The configuration approaches the modern .NET side, reducing the diff in the later phase
Put the other way around: jumping straight to modern .NET with old csproj files and old NuGet management produces too large a diff.
flowchart TB
accTitle: Reducing the diff with SDK-style conversion
accDescr: A diagram showing that jumping straight to modern .NET with old csproj files and old NuGet management produces too large a diff, so converting to SDK style first brings the configuration closer to the modern .NET side and reduces the later diff.
oldp["Old csproj plus old NuGet management"] -.->|"Jump straight across"| big1["The diff is too large"]
oldp -->|"Convert to SDK style first"| sdk1["The configuration approaches the modern side"]
sdk1 --> small1["The later diff shrinks"]
Figure 11: Inserting the SDK-style conversion makes the jump to modern .NET one step smaller.
4.4 Update Dependencies First
This too follows the official guide: move dependencies to the latest available versions, and where possible to .NET Standard-compatible versions.
Why do this first
- You find out early whether “this package works on modern .NET”
- Prevents old dependencies from becoming noise
- Makes it easier to convert shared libraries to
netstandard2.0 - Lets the later migration work focus on “porting the code”
flowchart TB
accTitle: Why updating dependencies first pays off
accDescr: A diagram showing that updating dependencies toward their latest versions first tells you early whether they work on modern .NET, removes noise from old dependencies, and lets the later migration work focus on porting the code.
upd1["Update dependencies to the latest first"] --> know1["Modern .NET support is known early"]
upd1 --> noise1["Noise from old dependencies is reduced"]
know1 --> foc1["The later phase focuses on porting code"]
noise1 --> foc1
Figure 12: The more dependency updates you finish up front, the more the migration itself is just porting.
4.5 Check the Assumptions of the Official Tooling Too
As of 2026-03, the center of gravity of Microsoft’s guidance has shifted toward GitHub Copilot app modernization. Rather than assuming only the traditional migration assistant tools, it matches practice better to view it as an end-to-end support flow covering assessment, planning, code fixes, and validation.
However, the current documentation assumes Visual Studio 2026 or a supported Visual Studio 2022 line, GitHub Copilot, and C# code.
Why this check is needed
- It changes what you can expect from the official tooling
- You can align the team’s assumptions about IDEs / build agents / extensions
- You can make the call to not over-rely on automation for VB.NET solutions
Codebases with VB.NET mixed in are not rare. So it is worth confirming, at the very start, “how far the latest official tooling will actually help.”
flowchart TB
accTitle: Where the official tooling stands and what it assumes
accDescr: A diagram showing that .NET Upgrade Assistant is deprecated and the guidance now points to GitHub Copilot app modernization, which assumes Visual Studio, Copilot, and C# code, so VB.NET solutions call for not over-relying on automation.
ua1[".NET Upgrade Assistant"] -->|"Deprecated, points here"| cp1["GitHub Copilot app modernization"]
cp1 --> pre1["Assumes Visual Studio plus Copilot plus C#"]
pre1 -.->|"If VB.NET is in the mix"| vb1["Do not over-rely on automation"]
Figure 13: The center of gravity has moved to Copilot app modernization, and codebases outside its assumptions need manual work.
5. Estimating Difficulty per Project Type
Migration tends to be discussed as one lump, “.NET Framework to .NET,” but in reality each project type is a different game.
5.1 A Rough Sense of Difficulty
| Type | Difficulty | Main issues |
|---|---|---|
| Class library | Low to medium | API compatibility, dependencies, target splitting |
| Console / batch / some Windows services | Low to medium | Distribution method, native dependencies, configuration |
| WinForms / WPF | Medium | Remains Windows-only; designer, third-party UI, BinaryFormatter-related issues |
| ASP.NET MVC / Web API | Medium to high | App model migration to ASP.NET Core, authentication, session, configuration, DI |
| ASP.NET Web Forms | High | Large gap in page model; assume UI layer replacement |
| WCF client | Medium | Package replacement, contracts, configuration |
| WCF server | High | CoreWCF, or redesign as gRPC / HTTP API |
| EF6 to EF Core done simultaneously | High | The ORM is a different beast; behavioral differences; migration history |
5.2 For Class Libraries, the Key Is How You Cut the “Shared Boundary”
Class libraries are relatively easy to move. But only when the library is genuinely separated like a library.
Dependencies like the following raise the difficulty:
- Touching
System.Web - Reading
HttpContext.Currentdirectly - Exposing WPF / WinForms types in the public API
- Leaning heavily on Windows APIs such as the registry, WMI, EventLog
- Depending on
AppDomainor Remoting
A useful framing: if you can extract pure business logic, it is light; if the library has swallowed the app model, it is heavy.
flowchart TB
accTitle: How to judge the weight of a class library
accDescr: A diagram showing that a class library from which pure business logic can be extracted is light, while one that has swallowed the app model through System.Web, UI types, Windows APIs, or AppDomain is heavy.
lib1["Look at the class library"] -->|"Only the logic needs extracting"| lt1["The migration is light"]
lib1 -->|"It has swallowed the app model"| hv2["The migration is heavy"]
hv2 -.-> sm1["System.Web, UI types, Windows API dependencies, and so on"]
Figure 14: A library’s difficulty comes not from line count but from how much of the app model it carries.
5.3 WinForms / WPF Migrate Easily, but Stay Windows-Only
WinForms and WPF can be migrated to .NET. However, both remain Windows-only frameworks.
Getting expectations wrong here is dangerous.
- What improves
- You get the modern .NET runtime, language, and SDK style
- CI/CD and package management can be brought up to date
- Some performance and maintainability improvements come along
- What does not change
- It is still Windows-only
- Compatibility issues with UI controls and design-time components remain
- ActiveX / COM / native DLL problems do not go away
Also, WinForms / WPF can require a BinaryFormatter impact review. Especially when custom types are involved in the clipboard, drag & drop, ResX, or design-time serialization, the issue tends to surface when you raise the target to .NET 9 or later.
flowchart TB
accTitle: Expectations for a WinForms / WPF migration
accDescr: A diagram showing that WinForms and WPF can migrate to .NET and pick up the modern .NET runtime, language, and SDK style, but stay Windows-only and may require a BinaryFormatter impact review.
ui1["Migrate WinForms / WPF"] --> gain1["Pick up the benefits of modern .NET"]
ui1 --> keep1["Still Windows-only"]
ui1 -.-> bfc1["A BinaryFormatter review is needed"]
Figure 15: Desktop UI can migrate, but the Windows-only nature carries straight over.
5.4 ASP.NET Framework Is an “App Model Migration,” Not a “Runtime Migration”
Migrating from ASP.NET Framework to ASP.NET Core is explicitly described as non-trivial in Microsoft’s guidance. Not merely because API names change, but because the underlying architecture is different.
The differences tend to show up around here:
- Hosting model
- Middleware pipeline
- Request processing model
- Session / Cache
- Authentication / Authorization
- Configuration
- Dependency Injection
- Logging / Monitoring
For an ASP.NET Framework app, the things to check first are around here:
- Which routes / endpoints can be moved first
- Whether
System.Webdependencies can be peeled out of shared libraries - How to align authentication / session / exception handling / logging
- Whether to migrate incrementally without stopping production
For large apps in particular, it is more realistic to assume incremental migration from the start.
flowchart TB
accTitle: The ASP.NET migration is an app model migration
accDescr: A diagram showing that moving from ASP.NET Framework to ASP.NET Core is not a replacement of API names but an app model migration in which hosting, middleware, authentication, configuration, and DI all change, so large apps realistically plan for incremental migration.
an1["ASP.NET Framework to Core"] --> am1["The underlying architecture changes"]
am1 --> pts1["hosting, middleware, auth, configuration, DI"]
am1 -->|"Large apps"| inc1["Plan on incremental migration"]
Figure 16: The ASP.NET migration moves the app model rather than the runtime, and the larger it is the more incremental it becomes.
5.5 For Web Forms, Start with “Decomposing Responsibilities,” Not “Migrating Assets”
Web Forms is not the same app model as ASP.NET Core. So for estimation purposes, it is safer not to assume the screen assets can be carried over as is.
In practice, the usual entry point is this decomposition:
- Separate screen logic from business logic
- Break apart the responsibilities buried in
Page/UserControl/ViewState - Move business logic and data access out into shared libraries
- Rebuild the UI on a different model: Razor Pages / MVC / Blazor, etc.
In other words, for Web Forms projects, whether there is a responsibility-decomposition plan matters more than the runtime migration itself.
flowchart TB
accTitle: Web Forms starts from decomposing responsibilities
accDescr: A diagram showing that because Web Forms is not the same app model as ASP.NET Core, you start by separating screen logic from business logic, moving the logic out into a shared library, and rebuilding the UI on a different model.
wf1["Web Forms screen assets"] --> sep1["Separate screen logic from business logic"]
sep1 --> mv1["Move the logic out into a shared library"]
mv1 --> re1["Rebuild the UI on a different model"]
wf1 -.->|"Assuming the assets carry over as is"| ngw1["The estimate falls apart"]
Figure 17: Web Forms is not an asset migration; expect to decompose the responsibilities and rebuild on another model.
5.6 Treat WCF Clients and WCF Servers Separately
Do not lump these together.
WCF clients
The WCF Client has supported NuGet packages for modern .NET. So if you only call WCF, the work can be lighter than it looks.
WCF servers
Hosting WCF services, on the other hand, is a different story. Microsoft’s guidance presents two broad modernization paths:
- Use CoreWCF to preserve compatibility with existing clients
- Move to modern RPC / HTTP-based stacks such as gRPC
Note that CoreWCF is a subset; it does not bring over all of WCF as is. It is suited to preserving compatibility with existing clients, but code changes and testing are a given.
flowchart TB
accTitle: Evaluate WCF separately on the client and server sides
accDescr: A diagram showing that WCF clients have supported packages for modern .NET and can be lighter than they look, while the server side requires choosing between CoreWCF for compatibility and a redesign around gRPC or HTTP APIs.
wcf1["Which side of WCF do you use"] -->|"Client"| cl1["Migrate with the supported packages"]
wcf1 -->|"Server"| sv1["Choose a path"]
sv1 -->|"Preserve compatibility"| cw1["CoreWCF (a subset)"]
sv1 -->|"Redesign"| gr1["gRPC / HTTP API"]
Figure 18: Calling and hosting differ in both difficulty and options, so do not estimate them as one.
6. Flush Out Technologies That Do Not Work on .NET, or Get Stuck Easily
This is something you absolutely want to do before starting. Microsoft maintains a list of technologies available on .NET Framework that are unavailable on .NET 6+.
6.1 Technologies That Tend to Be Red Lights
| Technology | Status on .NET | How to think about it |
|---|---|---|
AppDomain creation such as AppDomain.CreateDomain |
Unsupported | Think of isolation via separate processes / containers / AssemblyLoadContext |
| .NET Remoting | Unsupported | Redesign around IPC, HTTP, gRPC, sockets, pipes |
| CAS / Security Transparency | Unsupported as a security boundary | Think in terms of OS / container / privilege separation |
System.EnterpriseServices (COM+) |
Unsupported | Isolate and replace COM+-based designs |
| Workflow Foundation | Unsupported | Estimate separately, including alternatives such as CoreWF |
| WCF server | Not available as is in the box | Choose CoreWCF or gRPC |
| BinaryFormatter | On .NET 9+ the implementation always throws | Migrate serializers; audit ResX / clipboard / drag & drop |
6.2 AppDomain: “Some APIs Remain, but Creation Is a Different Matter”
The AppDomain area is a bit tricky.
Some of the API surface remains on .NET, but the usage pattern of creating a new AppDomain for isolation is not supported.
So if you used AppDomains for the following purposes, a redesign is needed:
- Plugin isolation
- Unloading dynamically loaded code
- Isolating partially trusted code
- Temporary separation of execution environments
What you should look at before migrating is not just whether the word AppDomain appears, but what the AppDomain was being used for.
flowchart TB
accTitle: What you did with AppDomain changes the verdict
accDescr: A diagram showing that because creating a new AppDomain for isolation is unsupported even though some APIs remain, you distinguish between merely using the type and using it for isolation, and redesign around a separate process or AssemblyLoadContext in the latter case.
ad1["AppDomain shows up"] -->|"Only the type or its info is referenced"| okc1["It often carries over as is"]
ad1 -->|"It created domains for isolation"| rd1["A redesign is needed"]
rd1 --> alt1["Separate process / container / AssemblyLoadContext"]
Figure 19: What decides whether a redesign is needed is not the keyword but what it was used for.
6.3 Remoting Is “Deeper Than It Looks”
Remoting itself, obviously, but asynchronous delegate calls like BeginInvoke() / EndInvoke() on delegates can also fall into the affected area. This is not Remoting per se, but it is unsupported on modern .NET, so it needs to be flushed out before migrating.
So when searching, it is safer to look at this set together:
System.Runtime.RemotingMarshalByRefObjectRealProxyBeginInvoke(/EndInvoke(
6.4 BinaryFormatter Suddenly Comes to the Fore Depending on the Target Version
The older the codebase, the more likely BinaryFormatter is being used “without awareness.”
- Persisted data
- Caches
- Session storage
- Plugin state
- Clipboard / drag & drop
- ResX
- WinForms / WPF designer serialization
On .NET 9 and later, BinaryFormatter’s implementation is not included in the runtime and the API always throws PlatformNotSupportedException.
So this is not a “think about it later” item; it is an audit item the moment you decide the target version.
flowchart TB
accTitle: How BinaryFormatter comes to the fore
accDescr: A diagram showing that BinaryFormatter tends to be used without awareness in persisted data, ResX, and the clipboard, and because the API always throws on .NET 9 and later it becomes an audit item the moment the target version is decided.
hid2["Unaware usage such as ResX and clipboard"] --> tg1["Set the target to .NET 9 or later"]
tg1 --> ex1["The API always throws"]
ex1 --> aud1["Audit it as soon as that is decided"]
Figure 20: BinaryFormatter starts to bite the moment you pick the target, so audit it up front instead of deferring it.
6.5 Search Terms to Grep for First
Before starting, just running searches over the whole solution with the following terms changes the picture considerably.
System.Web
HttpContext.Current
System.Runtime.Remoting
MarshalByRefObject
AppDomain
BinaryFormatter
ServiceHost
ChannelFactory
System.EnterpriseServices
Workflow
packages.config
web.config.install.xdt
install.ps1
DllImport
AxInterop
Microsoft.Office.Interop
Finding even one of these does not mean instant failure. It is a map for knowing which parts can migrate on the standard route and which become a separate track.
The actual search commands
The tool can be rg (ripgrep) or PowerShell; either is fine.
# PowerShell. Search everything under the solution in one pass
Get-ChildItem -Recurse -Include *.cs,*.vb,*.config,*.csproj,*.vbproj |
Select-String -Pattern 'BinaryFormatter|System\.Runtime\.Remoting|MarshalByRefObject|AppDomain\.CreateDomain' |
Select-Object Path, LineNumber, Line
# ripgrep. When you want a rough count first
rg -n --stats "BinaryFormatter|System\.Runtime\.Remoting|AppDomain\.CreateDomain" --glob "*.cs" --glob "*.vb"
What to do once you find something
The search is the entry point; the judgment that follows is the real work. For each term, the next place to look is already determined.
| Term found | What to check next | Where it lands |
|---|---|---|
System.Web / HttpContext.Current |
Whether it can be pushed out of the shared library. Whether the caller can repackage it into DTOs | Peel it off with the strategies in 8.5 |
System.Runtime.Remoting / MarshalByRefObject / BeginInvoke |
What the remote call was for: cross-process, cross-machine, or just an asynchronous call | Redesign around IPC, HTTP, gRPC, or Task |
AppDomain |
Whether it only uses the type name, or isolates with CreateDomain |
If it is for isolation, a separate process or AssemblyLoadContext (6.2) |
BinaryFormatter |
Whether the written data has to be read back later. Whether it is used indirectly through ResX or the clipboard | Move to a different serializer and plan how to reread the existing data (6.4) |
ServiceHost / ChannelFactory |
Host side or client side | Evaluate them separately, as in 5.6 |
packages.config / install.ps1 / web.config.install.xdt |
What was being rewritten at install time | The landmines in 4.2. Hold the settings explicitly instead |
DllImport / AxInterop / Microsoft.Office.Interop |
The bitness assumption, and the full set of files needed at runtime | On to the bitness check in 9.4 |
Take it this far and you no longer have a question of “can we migrate” but a list of which parts follow the standard route and which need a separate estimate. That list is exactly what you want before starting.
flowchart TB
accTitle: From search to a list
accDescr: A diagram showing that searching is only the entry point and that following the next thing to check for each term turns the results into a list of what migrates on the standard route and what needs a separate estimate.
grep1["Search the solution for the terms"] --> nxt1["Follow the next check for each term"]
nxt1 --> map1["It becomes a list of standard route versus separate estimate"]
grep1 -.-> note2["A hit does not mean instant failure"]
Figure 21: The search itself is only the entry point; carry it through to judgment and it becomes the list you want before starting.
7. Decide How Much Windows-Only Dependency to Accept
A common misunderstanding in migrations is “once we are on .NET, we are cross-platform.” There is no such magic. If the app is deeply tied to Windows, it remains Windows-only after the migration, plainly.
7.1 Migrating While Staying Windows-Only Is Entirely Realistic
Microsoft provides the Windows Compatibility Pack, a way to use many Windows-oriented APIs from modern .NET: the registry, WMI, EventLog, Windows services, Directory Services, and so on.
Its existence matters a lot in practice.
- We want to move to modern .NET first
- But we are not leaving Windows for now
- So we want to accept the Windows API dependencies for the time being
For teams in that situation, it is a strong option.
The first goal of a migration does not have to be going cross-platform.
7.2 But Windows-Only APIs Are Also “Debt That Bites Later”
The Windows Compatibility Pack existing does not make everything safe.
- You want to run in Linux containers
- You want to run on Kubernetes
- You want macOS / Linux developers running the same build
- You want to reduce Windows VMs in the cloud someday
If you have goals like these, it is better to make the Windows API dependencies visible now.
flowchart TB
accTitle: Where the compatibility pack fits, and the debt it leaves
accDescr: A diagram showing that if you are not leaving Windows for now the Windows Compatibility Pack lets you accept Windows API dependencies and move to modern .NET, but that if containers or cloud are a future goal those dependencies are debt that bites later and should be made visible now.
now1["Move to modern .NET first"] -->|"Stay on Windows for now"| pack1["Accept the dependencies for now via the compatibility pack"]
pack1 -.->|"If containers or cloud are the goal"| debt1["The dependencies are debt that bites later"]
debt1 --> vis1["Make them visible now"]
Figure 22: The compatibility pack is a realistic bridge, but the dependency turns into debt depending on your goal, so keep it visible.
7.3 System.Drawing.Common Is Especially Often Misunderstood
System.Drawing.Common is a Windows-only library on .NET 6 and later.
If you have code using it for image processing or text rendering, you first need to decide which way to go.
- Keep operating on Windows?
- Or want it running on Linux / macOS in the future?
In the former case, leaving it as is for now can be fine. In the latter, a replacement such as SkiaSharp or ImageSharp needs to be in the migration plan from the start.
flowchart TB
accTitle: The System.Drawing.Common fork
accDescr: A diagram showing that because System.Drawing.Common is Windows-only from .NET 6 on, staying on Windows can leave it as is for now, while running on Linux or macOS requires putting a SkiaSharp or ImageSharp replacement in the plan from the start.
sd1["You use System.Drawing.Common"] -->|"Keep operating on Windows"| ok4["Leaving it as is can be fine for now"]
sd1 -->|"Also run on Linux / macOS"| repl1["Replace with SkiaSharp / ImageSharp"]
repl1 --> plan2["Include it in the migration plan from the start"]
Figure 23: System.Drawing.Common is Windows-only, so how you treat it forks on the landing point from day one.
7.4 Typical Smells That Indicate Windows Lock-In
When references or APIs like the following are present, it is safer to estimate on the assumption of “migrating while staying Windows-only, at least at first.”
Microsoft.Win32.RegistrySystem.ManagementSystem.Diagnostics.EventLogSystem.ServiceProcessSystem.DirectoryServicesSystem.DrawingDllImport/ P/Invoke- COM references
AxInterop.*Microsoft.Office.Interop.*
8. How You Carve Out Shared Libraries Changes the Difficulty
For larger solutions, it is no exaggeration to say the success of the migration is determined by how you cut the shared libraries.
8.1 Classify First
Libraries are easiest to organize when split into three broad kinds.
- Pure business logic / domain logic
- Middle layers with some app model dependency
- Layers tightly coupled to UI / Web / Windows APIs
Of these, the first to migrate should be kind 1.
- Calculations
- Rule evaluation
- DTOs / contracts
- Domain services
- Simple data transformations
If you can extract this cleanly, the difficulty drops sharply.
flowchart TB
accTitle: Three library categories and the order to move them
accDescr: A diagram showing that libraries split into pure business logic, middle layers with some app model dependency, and layers tightly coupled to UI or Windows APIs, and that pure business logic is the first to move.
c1["Pure business logic"] -->|"Move this first"| ez1["Extracting it cleanly lowers the difficulty"]
c2["Middle layer closer to the app model"] -->|"Next"| ez1
c3["Layer tight to UI and Windows APIs"] -->|"Last"| ez1
Figure 24: Sort libraries into three layers and rescue them starting from the pure logic.
8.2 netstandard2.0 Is Still a Valid Bridge
Per Microsoft’s guidance, for a shared library that must also coexist with the .NET Framework side, the baseline is to consider .NET Standard 2.0 first.
Two points matter here.
- .NET Framework does not support
.NET Standard 2.1 - If you want a shared library referenced from both old and new, 2.0 tends to be the practical answer
flowchart TB
accTitle: netstandard2.0 as a bridge
accDescr: A diagram showing that because .NET Framework does not support .NET Standard 2.1, .NET Standard 2.0 tends to be the practical answer for a shared library that must be referenced from both the old and the new side.
fw1[".NET Framework side"] -->|"Can reference it"| ns1["netstandard2.0 shared library"]
mn1["modern .NET side"] -->|"Can reference it"| ns1
ns21["netstandard2.1"] -.->|"Framework does not support it"| fw1
Figure 25: As a bridge referenced from both sides, 2.0 rather than 2.1 is the practical answer.
8.3 What Changes with netstandard2.0
| Policy | What changes | Good fit | Caveats |
|---|---|---|---|
Convert to netstandard2.0 |
Easy to reference from both old and new | Pure business logic, common contracts, utilities | App-model-specific APIs cannot live here |
Multi-target (e.g. net48;net10.0) |
Keep common code while holding per-environment differences | Libraries with a few environment differences | More conditional compilation and build management |
Go straight to net10.0-only |
Cleanest in the long run | New layers that do not need old/new coexistence | Cannot be referenced from .NET Framework |
8.4 Compatibility Mode Is Not a Cure-All
.NET Standard 2.0 has a compatibility mode that allows referencing .NET Framework libraries. However, this is not magic that makes everything work transparently.
For example, libraries that assume app-model-specific APIs like WPF are plainly difficult. In other words, even when you say “shared library,” it is important to narrow it down to the responsibilities that can truly be shared.
8.5 For ASP.NET-Related Libraries, the Battle Is Whether You Can Peel Off System.Web
When incrementally migrating ASP.NET Framework, having shared libraries hang directly off HttpContext.Current or System.Web is quite painful.
The basic strategies at that point are one of these:
- Push the
System.Webdependency outside the interface - Change the code to receive
HttpContext-derived information as DTOs - Use adapters during the transition period
- If that still does not work, peel it off gradually with multi-targeting
8.6 Raise Libraries Leaf-First
The ASP.NET incremental migration guide explicitly says to upgrade supporting libraries postorder depth-first, that is, from the leaves up.
This is quite effective for general solutions too, not just web.
- Because dependencies are upgraded first, the upper layers become easier to see through
- Compatibility issues are easier to localize
- Testing per library becomes easier
flowchart TB
accTitle: Upgrading leaf-first
accDescr: A diagram showing that upgrading supporting libraries from the leaves of the dependency graph upward makes the upper layers easier to see through and keeps compatibility issues localized and testable per library.
lf1["Upgrade the leaf libraries first"] --> lf2["Upgrade the middle layers whose dependencies are done"]
lf2 --> lf3["Finally the upper application layer"]
lf1 -.-> loc1["Problems stay localized and testable"]
Figure 26: Go leaf-first and the ground is already settled by the time you reach the upper layers.
9. Take Inventory of NuGet / External Dependencies / Third-Party Components
Do this sloppily and you suffer the worst pain in the second half of the migration.
9.1 Dependencies Are Easier to Organize in 4 Categories
- Public NuGet packages
- In-house private packages / internal libraries
- Local DLL references
- COM / ActiveX / native DLLs / SDKs
Looking only at category 1 is not enough. The really dangerous ones are 3 and 4.
flowchart TB
accTitle: Four dependency categories and their risk
accDescr: A diagram showing that dependencies split into public NuGet packages, in-house packages, local DLL references, and COM or ActiveX or native DLLs, that looking only at public NuGet is not enough, and that the genuinely dangerous ones are local DLLs and the COM and native side.
dp1["Take inventory of the dependencies"] --> k1["Public NuGet and in-house packages"]
dp1 --> k3["Local DLL references"]
dp1 --> k4["COM / ActiveX / native DLLs"]
k3 --> dg1["This is the really dangerous side"]
k4 --> dg1
Figure 27: Counting public NuGet packages is not enough; local DLLs and the COM and native side are what to flush out first.
9.2 What to Verify for Each Dependency
For each dependency, verify at least this much:
- Does it target modern .NET?
- Does it support
PackageReference? - Is it fine with SDK-style projects?
- Any x86 / x64 / ARM64 constraints?
- Does it depend on design-time tooling or Visual Studio extensions?
- Does it assume install scripts / config transforms?
- Is it still supported?
9.3 Estimate Third-Party UI / Reporting / Design-Time Components Separately
For WinForms / WPF / ASP.NET migrations, this area matters a lot.
- Grids
- Reporting engines
- PDF output components
- Charting components
- Designer-integrated UI libraries
- ActiveX wrappers
These involve design-time support, not just the runtime. If your migration estimate only checks “does it compile,” you will plainly miss these.
9.4 Always Check Native DLLs and Bitness
Even if things seemed to run as AnyCPU in the .NET Framework era, in reality they may depend on things like:
- COM fixed to
x86 - 32-bit-only ActiveX
- A specific version of the VC++ Runtime
- Signed native DLLs
These are not problems that suddenly sprouted with the move to modern .NET; constraints that were always there simply surface. Which is exactly why it is worth making them visible before the migration.
flowchart TB
accTitle: Bitness constraints surfacing
accDescr: A diagram showing that code that appeared to run as AnyCPU may depend on x86-fixed COM, 32-bit-only ActiveX, or a specific VC++ Runtime version, and that the move to modern .NET merely surfaces constraints that were always there.
any1["It looked like it ran as AnyCPU"] -.-> hid3["Depends on x86-fixed COM, 32-bit ActiveX, and the like"]
hid3 --> surf1["The migration surfaces the pre-existing constraints"]
surf1 --> pre2["Make them visible before migrating"]
Figure 28: The migration does not create the bitness problem; it only makes an existing constraint visible.
10. Treat EF6, Serializers, and the Data Layer as Separate Problems
The runtime migration and the redesign of data access and serialization work out better when treated as separate problems wherever possible.
10.1 EF6 to EF Core Is Not a Direct Upgrade
Microsoft’s EF guidance also states that EF Core is a total rewrite of EF6 and there is no direct upgrade path.
So for apps using EF6, this order is realistic:
- First move to modern .NET
- Keep EF6 if needed and run the app
- Then migrate to EF Core as a separate project
Do not combine the runtime migration with the ORM migration. That alone lowers the difficulty considerably.
flowchart TB
accTitle: Separating the EF6 and runtime migrations
accDescr: A diagram showing that because EF Core is a total rewrite of EF6 with no direct upgrade path, the realistic order is to move to modern .NET first, keep EF6 running if needed, and migrate to EF Core later as a separate project.
e1["Move to modern .NET first"] --> e2["Keep EF6 running if needed"]
e2 --> e3["Migrate to EF Core later as a separate project"]
e1 -.->|"Do both at once"| mix1["ORM behavior differences mix in and add weight"]
Figure 29: Do not move the runtime and the ORM at the same time; leaving EF6 in place while the runtime moves is the realistic order.
10.2 What Changes if You Keep EF6
- Upsides
- The data access layer diff can be deferred
- You can focus on migrating business logic and the app model
- “EF Core behavioral differences” do not get mixed in
- Caveats
- For new development, EF Core is the main line
- EF6 Designer / EDMX usage patterns have separate constraints
10.3 For EDMX-Based EF6, Look All the Way to “Design Time”
The EF6 documentation states that the EF Designer is not directly supported in .NET / .NET Standard projects or SDK-style .NET Framework projects.
For EDMX-based apps, you need to consider these three points separately:
- Does it run at runtime?
- Can the Designer be used?
- How will the generated code be handled?
If you use EDMX heavily, it is safer to put this in the estimate from the start.
10.4 BinaryFormatter and Custom Serialization Easily Become “Hidden Dependencies”
Serializers are easy to miss with code search alone.
- Persistence formats
- Messaging
- Caches
- Old WCF / SOAP contracts
- ResX
- Clipboard / drag & drop
This area also involves data compatibility. That is, you need to verify not just “does it build” but whether it can still read the old data.
11. Include Configuration, Distribution, Operations, and CI/CD in the Migration Scope
The migration target is not just source code.
11.1 Configuration Files
On the .NET Framework side, quite a lot can be riding in app.config / web.config.
- Connection strings
- Custom config sections
- WCF endpoint configuration
- Binding redirects
- Diagnostics
- Various ASP.NET settings
- The results of transforms applied at package install time
On the modern .NET side, how configuration is held and loaded changes in places. So “configuration files later” is dangerous.
The first thing to do is an inventory of the configuration.
- What lives in the configuration files
- Which entries are required at app startup
- Which are per-environment differences
- Which were auto-injected by NuGet or installers
11.2 Distribution Method
Look at the distribution shape before starting too.
- Hosted under IIS?
- A Windows service?
- A Scheduled Task?
- ClickOnce / MSI / a custom installer?
- On-premises servers assumed?
- Is self-contained or framework-dependent the better fit?
Even if the executable itself migrates, you get stuck at the end if the distribution and startup machinery still assumes the old world.
11.3 Logging, Monitoring, and Operational Procedures
The operations side is easy to overlook as well.
- Is the Windows Event Log assumed?
- Are Performance Counters being watched?
- WMI-based monitoring?
- Are service accounts and permissions fixed?
- Do logs assume local file output?
“The code runs after the migration, but operations cannot function” happens, plainly.
11.4 CI/CD and Build Agents
Before migrating, also check:
- Can the required .NET SDK be installed on the build agents?
- What to do with pipelines that assume
nuget.exe/msbuild.exe - Will you move to the
dotnetCLI? - How to update the test, coverage, and publish jobs
- Are in-house templates or reusable pipelines assuming the old formats?
“It works on my machine but CI dies” is a migration classic.
flowchart TB
accTitle: Source code is not the only migration target
accDescr: A diagram showing that even when the executable migrates, configuration files, the distribution method, logging and monitoring and operational procedures, and CI/CD with build agents will trip you up at the end if they still assume the old world, so they belong in the migration scope.
code1["Migrating the source code"] --> also1["That is not the end of it"]
also1 --> cfg1["Configuration files and the distribution method"]
also1 --> ops1["Logging, monitoring, operational procedures"]
also1 --> ci1["CI/CD and build agents"]
ci1 -.-> gotcha1["It works locally but CI dies"]
Figure 30: Only once configuration, distribution, operations, and CI/CD are included have you actually counted the migration scope.
12. A Realistic Way to Run the Migration
Given everything above, a realistic approach usually settles into roughly this shape.
12.1 First, Put the Current Framework Side in Order
- Move to .NET Framework 4.7.2 or later, ideally 4.8.1
- Upgrade dependencies
- Review
packages.config - Move to
PackageReferenceand SDK style where possible - Confirm the current app genuinely works in that state
Just doing this reduces the diff in the later phase considerably.
12.2 Rescue the Shared Libraries First
Next, move business logic and common contracts toward netstandard2.0 or multi-targeting.
The order of upgrading is, as a rule, leaf-first.
12.3 For the Application Itself, Vary the Strategy per App Model
- Class libraries / console / some services Relatively straightforward
- WinForms / WPF Modernize while staying Windows-only
- ASP.NET MVC / Web API All at once if small; incremental if heavy
- Web Forms Assume screen replacement; move shared logic out first
- WCF servers Decide first: stay with CoreWCF or redesign with gRPC
12.4 Stick to “Not Everything at Once”
The combinations to particularly avoid:
- Runtime migration + full ORM swap
- Runtime migration + authentication infrastructure change
- Runtime migration + full cloud migration
- Runtime migration + monitoring infrastructure change
- Runtime migration + UI framework refresh
Even if all of them are needed, not stacking them in the same sprint usually works out better.
12.5 Take Tests and Baselines Before Moving
At minimum, you want this much in place before moving:
- Unit tests
- Integration tests for the main business flows
- Snapshot-style verification of representative screens / APIs
- A performance baseline
- A way to check the main logs
- A rollback procedure
Proceeding in a state where you cannot identify “what broke” after the migration is quite dangerous.
flowchart TB
accTitle: What to prepare before you move
accDescr: A diagram showing that preparing tests, a performance baseline, a way to check the logs, and a rollback procedure before starting the migration lets you identify what broke afterward, and that proceeding without them is dangerous.
prep1["Prepare tests and baselines"] --> mv2["Then start the migration"]
mv2 --> det1["You can identify what broke"]
non2["Proceed without preparing"] -.-> risk1["Breakage cannot be identified, which is dangerous"]
Figure 31: The migration is decided by the preparation before you move; only a baseline lets you locate what broke.
13. Pre-Start Checklist
Here it is in a form you can paste straight into your project management tool.
13.1 Policy
- You can state in one sentence why you are migrating
- The landing point is decided: Windows-only modern .NET or future cross-platform
- The target .NET version is decided
- What is excluded from this scope (EF Core conversion, auth refresh, full cloud migration, etc.) is decided
13.2 Preparing the Current .NET Framework Side
- Moved to .NET Framework 4.7.2 or later, ideally 4.8.1
- Upgraded dependencies toward the latest
- Checked for
packages.config - Verified feasibility of converting to
PackageReference - Verified feasibility of converting to SDK style
- The current app builds, starts, and passes tests in that state
13.3 App Types and Technology Choices
- Split difficulty by type: class library / desktop / web / WCF, etc.
- Understood that WinForms / WPF remain Windows-only
- Understood that ASP.NET Framework means an app model migration to ASP.NET Core
- Included Web Forms UI layer replacement in the estimate
- Evaluated WCF clients and servers separately
13.4 Unsupported Technologies / APIs Requiring Attention
- Flushed out
AppDomaindependencies - Flushed out Remoting /
MarshalByRefObject/BeginInvoke/EndInvoke - Flushed out CAS / Security Transparency / COM+ / WF
- Flushed out BinaryFormatter dependencies
- Flushed out
System.Webdependencies
13.5 Windows-Only Dependencies
- Flushed out use of the registry, WMI, EventLog, Windows services, Directory Services
- Flushed out use of
System.Drawing.Common - Flushed out COM / ActiveX / Office Interop / P/Invoke / native DLLs
- Verified x86 / x64 / ARM64 constraints
13.6 Shared Libraries and Data Access
- Classified shared libraries into business logic / app-model-coupled layers
- Flushed out what can be converted to
netstandard2.0 - Flushed out libraries that need multi-targeting
- Decided whether the runtime can move first while keeping EF6
- Verified EDMX / Designer dependencies
13.7 Operations and Build
- Took inventory of the configuration files
- Verified the distribution method (IIS / service / MSI / ClickOnce, etc.)
- Verified logging / monitoring / permissions / execution account assumptions
- Verified whether CI/CD and build agents need updating
- Created a rollback procedure
14. Summary
What matters in migrating from .NET Framework to .NET is not “which command to migrate with” but seeing, before you start, what carries over as is and what is a separate problem.
Narrowed to the essentials, it is these six points:
- Clean up the .NET Framework side before migrating
- Split difficulty by app model
- Flush out the unusable technologies first
- Decide how much Windows-only dependency to accept
- Decide how to carve out the shared libraries
- Do not pile on ORM, authentication, and cloud migrations at the same time
The accuracy of the estimate changes dramatically in the first week of a migration. Put another way, if you can sort out the issues in that week, the second half can be steered fairly close to ordinary development.
flowchart TB
accTitle: How to use the first week
accDescr: A diagram showing that sorting out in the first week what carries over as is and what is a separate problem raises the accuracy of the estimate and lets the second half be steered close to ordinary development.
wk1["Sort out the issues in the first week"] --> est1["The estimate gets more accurate"]
est1 --> nor1["The second half moves closer to ordinary development"]
wk1 -.-> pts2["Separating what carries over from what is a separate problem"]
Figure 32: Whether you can spend the first week taking stock decides the accuracy of the whole migration.
“Let’s just try changing it to net10.0” is not bad as exploration.
But as a production migration, there are things to look at before that. What this article organized is exactly that.
15. References
- Prerequisites to porting code
- Overview of porting from .NET Framework to .NET
- .NET Framework technologies unavailable on .NET 6+
- What is GitHub Copilot app modernization
- Install GitHub Copilot app modernization
- .NET Upgrade Assistant overview - now deprecated, with GitHub Copilot app modernization recommended instead.
- Official .NET support policy
- Microsoft .NET and .NET Core lifecycle - the list of support start and end dates for each version.
- .NET Framework official support policy
- Use tooling to migrate ASP.NET Framework to ASP.NET Core
- Migrate from ASP.NET Framework to ASP.NET Core
- Get started with incremental ASP.NET to ASP.NET Core migration
- Use the Windows Compatibility Pack to port code to .NET
- .NET Standard
- Cross-platform targeting for .NET libraries
- Migrate from packages.config to PackageReference
- PackageReference in project files
- BinaryFormatter migration guide
- BinaryFormatter migration guide for Windows Forms applications
- WCF Client Support Policy
- CoreWCF Support Policy
- Why migrate WCF to ASP.NET Core gRPC
- Port from EF6 to EF Core
- What’s new in EF6
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
How Long Will VB6 Apps Keep Running? — Runtime Support Status and a Practical Path to .NET Migration
How long will VB6 applications keep running? This practical guide clarifies the asymmetry between the VB6 runtime support policy (still s...
Versioning Your Business App's Database Schema — Migration Practices to Prevent 'Every Customer Has a Different DB'
A practical guide to versioning the database schema of a business app whose databases are scattered across customer sites. Covers PRAGMA ...
Why Arguments Break — The Rules of Windows Command-Line Arguments
Windows passes CreateProcess a single string that the receiver splits. Covers the CommandLineToArgvW, CRT, and .NET rules, ArgumentList, ...
End of Servicing for Windows Printer Drivers — How Business Apps Should Prepare Their Report and Label Printing
Microsoft is phasing out v3/v4 printer drivers. What Windows protected print mode removes, and how to inventory and prepare report and la...
Using WMI/CIM from C# and PowerShell — A Practical Guide to Hardware Inventory, Process Monitoring, and Remote Queries
WMI/CIM is the standard way to read a PC serial number, watch disk space, and detect process starts. Covers CIM cmdlets, Get-WmiObject mi...
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
Where This Topic Connects
This article connects naturally to the following service pages.
Legacy Asset Reuse & Migration Support
This is about taking stock of existing assets involving .NET Framework, Web Forms, WCF, COM / ActiveX, and old NuGet workflows, so it fits well as a legacy asset migration consultation topic.
Technical Consulting & Design Review
If you want to sort out the migration scope, how to slice an incremental migration, and how much Windows-only dependency to accept before starting, this topic works well as a technical consulting / design review engagement.
Frequently Asked Questions
Common questions about the topic of this article.
- What should I do first before migrating from .NET Framework to .NET?
- Finish cleaning up the .NET Framework side before you start implementing. Microsoft's official guide recommends upgrading to .NET Framework 4.7.2 or later (in practice, 4.8.1 where possible) before porting, converting packages.config to PackageReference, moving to the SDK-style project format, and updating dependencies toward their latest versions. Doing this first cuts the diff of the later move to modern .NET considerably, and it becomes much easier to isolate whether a compatibility problem comes from the age of the Framework or from the move to .NET. Take stock of unsupported technologies such as AppDomain, Remoting, and BinaryFormatter before starting as well.
- Is deciding to stay on .NET Framework a realistic option?
- Quite normally, yes. .NET Framework 4.8.1 stays supported as long as it runs on a supported version of Windows, so it is not the case that everything must move to modern .NET right now or you are immediately at risk. If you have a large stock of Web Forms screens, must preserve WCF server compatibility strictly, have deep Workflow Foundation or COM+ dependencies, or use third-party design-time components that do not support modern .NET, it is rational to consolidate on 4.8.1 for stable operation for now and plan the replacement on a separate track. The constraints do remain, though: you cannot get out of being Windows-only, and it is harder to benefit from the performance and language features of newer .NET.
- Which technologies cannot move to .NET, or tend to get stuck?
- AppDomain creation, .NET Remoting, CAS (Code Access Security), COM+ (System.EnterpriseServices), and Workflow Foundation are unsupported on modern .NET and are red lights that call for a redesign. WCF servers do not run as they are in the box, so you choose between CoreWCF and a redesign around gRPC or HTTP APIs. On .NET 9 and later, BinaryFormatter has no implementation in the runtime and always throws, so you need an audit that reaches persisted data, ResX, the clipboard, and drag & drop. Beyond those, install.ps1 and XDT transforms from packages.config, native DLLs, COM / ActiveX, and x86 assumptions are points to watch: the build can pass and the app can still fail at runtime.
- Will migrating a WinForms or WPF app to .NET make it cross-platform?
- No. WinForms and WPF can migrate to .NET, but they remain Windows-only frameworks. What the migration buys you is the modern .NET runtime, language features, SDK style, and better fit with CI/CD; it does not make the app run in a Linux container. If Linux or containers are a future goal, you need to inventory Windows-bound APIs such as System.Drawing.Common, the registry, WMI, EventLog, Windows services, COM, and Office Interop early. If you are staying Windows-only, on the other hand, you can take the pragmatic route of using the Windows Compatibility Pack to modernize the runtime first.