"Upgrade to .NET 10" usually arrives as a single line in a roadmap. It sounds like a version bump: change the target framework, fix a few compile errors, redeploy. For an application already on modern .NET, that's roughly true. For an application on .NET Framework 4.x, it is not an upgrade at all. It's a move to a different runtime with a different hosting model, different configuration system, different defaults and a different set of supported APIs.
Treating it as a version bump is how migrations turn into long, unpredictable projects. Here is how I'd approach it instead.
Start with an inventory, not a branch
Before anyone opens a migration branch, you need facts about what the application actually uses. For each project:
- Project format. Legacy
.csprojfiles with explicit file lists, or SDK-style projects? Conversion to SDK-style is a prerequisite, and it's a good, low-risk first change on its own. - Target frameworks of every project and every package, including internal NuGet packages.
- APIs that don't exist on modern .NET. The big ones:
System.Web(and therefore Web Forms and the classic ASP.NET pipeline), server-side WCF hosting, .NET Remoting, creating additional AppDomains, Windows Workflow Foundation, and code-access security. - Hosting model. IIS with
Global.asax, a Windows service, a scheduled console app, a desktop app. - Platform dependencies. COM interop, the registry, Windows-specific file paths, the GAC, machine-wide configuration.
This inventory tells you which of several very different projects you're in.
Not every application can — or should — jump to .NET 10
It's worth saying plainly: some .NET Framework applications cannot be moved to modern .NET without substantial rework, and some shouldn't be moved at all.
| Application shape | Realistic path |
|---|---|
| Class libraries, console jobs, Windows services with few platform dependencies | Usually a direct port: SDK-style project, retarget, replace incompatible packages |
| ASP.NET MVC / Web API | Port to ASP.NET Core. Controllers often move well; the pipeline, filters, auth and configuration need rework |
| Large ASP.NET apps that can't move in one release | Incremental migration: an ASP.NET Core front-end proxies to the old app and takes over routes one by one |
| ASP.NET Web Forms | No modern .NET equivalent. Rewrite the UI (for example in Razor Pages or Blazor), or keep it on .NET Framework |
| WCF services | Clients are supported on modern .NET; server-side hosting is not built in. Options are CoreWCF, moving to gRPC or HTTP APIs, or staying put |
| WinForms / WPF | Supported on modern .NET, Windows-only. Third-party control libraries are often the blocker |
.NET Framework 4.8.x is still a supported part of Windows. For a low-change application with heavy Web Forms or Workflow Foundation, staying on it — isolated and patched — can be the responsible decision. The point of the inventory is to make that a deliberate decision rather than an accident.
A UI migration is a different project from a server upgrade
Teams often estimate a web application migration as one unit of work. It's usually two:
- The server-side upgrade: hosting, middleware, dependency injection, configuration, logging, data access. Mostly mechanical once patterns are established.
- The UI migration: Web Forms pages, server controls, ViewState, postback-driven workflows. This is a rewrite with UX, testing and acceptance implications.
Separating them makes estimates honest and lets the server side move first — often behind the incremental-migration proxy — while the UI is rebuilt at a pace the business can absorb.
Behavior preservation is the real requirement
The compiler will tell you what doesn't build. It won't tell you what behaves differently. Common sources of silent change include:
- Serialization. Moving from
Newtonsoft.JsontoSystem.Text.Jsonchanges defaults for casing, null handling, number handling and polymorphism. Either keep Newtonsoft deliberately or test the contracts explicitly. - Globalization. Culture-sensitive formatting and string comparison can differ between Windows NLS-based behavior on .NET Framework and ICU-based behavior on modern .NET, especially on Linux.
- File systems. Linux paths are case-sensitive. Code that worked on Windows because
Config.xmlandconfig.xmlwere the same file will fail. - Configuration.
web.configtransforms andConfigurationManagergive way to layered providers. Values that came from machine-level config can quietly disappear. - Authentication. Windows authentication, forms-auth cookies and machine keys need explicit replacements, and session sharing during incremental migration needs design.
The defense is evidence, not care. Before migrating, write characterization tests that capture how the existing system behaves today — including behavior that looks wrong but that something downstream might rely on. Where traffic can be captured, replay recorded requests against old and new versions and compare responses.
Testing and release gates
A migrated application should be releasable only when it produces evidence:
- Builds cleanly on the target framework with compatibility analyzers on, and no suppressed warnings without a recorded reason.
- Existing and new characterization tests pass.
- Responses match the old system on replayed traffic, with any accepted differences listed.
- Performance is within an agreed tolerance of a baseline captured before migration.
- Dependency and container image scans are clean or explicitly risk-accepted.
- Health checks, logs, metrics and traces are wired to the platform.
- Rollback has been tested, not just described.
Writing these down before the work starts changes the conversation from "is it done?" to "has it produced the evidence?"
CI/CD and the container platform
Modern .NET opens up Linux containers, and that's often the real prize: denser hosting, faster startup, a common platform for .NET and non-.NET workloads. It also adds work that isn't in the code:
- Build pipelines need new agents or images, new SDK versions and probably new caching.
- Base images need an owner who patches them on a schedule.
- Configuration and secrets move from files on a server to environment variables and a secret store.
- Health probes, graceful shutdown and resource limits need to be set deliberately — a service that ignores
SIGTERMwill drop requests on every deploy. - Anything that wrote to local disk or assumed a long-lived machine needs rethinking.
None of this is hard. All of it is easy to forget in an estimate that only counts code changes.
The hidden risk: shared dependencies
In a portfolio, the hardest constraint is usually a shared internal library. If twenty applications depend on Company.Common, it can't move to net10.0 alone without breaking the nineteen that haven't migrated.
The standard answer is multi-targeting: the library builds for both net48 and net10.0 (or netstandard2.0 as a bridge) until its last .NET Framework consumer is gone. That decision has consequences — conditional compilation, two test matrices, discipline about which APIs the library uses — so it should be planned up front. In a large estate, the dependency graph of shared code, not the list of applications, decides the order of work.
Decisions to make explicitly
- Which applications are in scope, and which will deliberately stay on .NET Framework?
- Linux or Windows hosting, per application?
- Keep
Newtonsoft.Jsonor move toSystem.Text.Json? Keep EF6 or move to EF Core? (Both can be decided separately from the runtime move — and usually should be.) - Incremental migration or a single cutover for each web application?
- What evidence is required before release, and who checks it?
A concise migration checklist
- Inventory projects, packages, APIs, hosting and platform dependencies.
- Classify each application: port, incremental, rewrite UI, rehost, retire.
- Convert to SDK-style projects and central package management where useful.
- Multi-target shared libraries before their consumers move.
- Capture a behavior and performance baseline; write characterization tests.
- Port hosting, configuration, logging and DI; replace incompatible packages.
- Rework authentication and session handling, including during coexistence.
- Containerize with health checks, graceful shutdown and resource limits.
- Run parity, performance and security gates.
- Release progressively with tested rollback; retire the old hosting.
A .NET 10 migration done this way is slower to start and much faster to finish. Most of the risk sits in the questions above, not in the compiler errors.
For how this scales across hundreds of applications, see the reference architecture for modernizing a large .NET portfolio.