Migrating WinForms & WPF from .NET Framework to .NET 10
What This Guide Covers
I've hand-migrated more legacy codebases than I care to count, back when "modernisation" meant a fortnight of find-and-replace, a wall of compiler errors, and a knot in your stomach every time you hit Build. So when I recently took a fifteen-year-old WinForms line-of-business app from .NET Framework 4.8 to .NET 10 in a matter of days, with an AI agent doing the grunt work and me keeping my hands on the decisions, I paid attention to what actually worked. This guide is that method, written up as steps you can follow.
It assumes a real codebase: a non-SDK project, packages.config, a commercial control suite referenced the old way, and a decade of dependencies that were sensible in 2012 and a bit embarrassing now. If you've worked on real enterprise software, you've met this codebase. Odds are you maintain one. The app I'll keep referring to is WinForms, so the specifics lean that way, but the process is the same for WPF and I'll flag where the two part company.
The migration is rarely hard. It's vast and fiddly: thousands of small mechanical changes where one wrong move turns a working app into a non-compiling mystery you'll spend a day unpicking. It's tedious and easy to get wrong, which is exactly the kind of work worth automating, as long as you keep checking what it produces. Follow the steps below in order, commit after each one, and a job that's sat in the "someday" pile for years turns into this week's work.
Before you start: you need Visual Studio 2026 (or Visual Studio 2022 17.14.17+), your source in Git so every step is reversible, access to your DevExpress and NuGet feeds, and a paid GitHub Copilot plan if you want to run the modernization agent. With DevExpress v26.2 setting .NET 10 as the minimum supported target, this is the version to aim at.
Step 0: Pick the Right Target (LTS vs STS)
Decide the target framework before you touch anything, because it's one line in a project file that decides whether you do this again next year. Microsoft alternates its releases. The even-numbered ones (8, 10, 12) are Long-Term Support with a three-year window; the odd numbers in between are Standard-Term Support and shorter-lived (24 months now).
The first useful thing the AI did on my migration wasn't code. My obvious target was .NET 8. It pointed out that 8's support window was nearly up (November 2026), while .NET 10 was the current LTS with years of runway ahead of it. That's one line in a project file standing between "done" and "doing this again next year," and most juniors wouldn't have raised it. For an app that has to sit stable for years, you want the LTS. Right now that's .NET 10, which also happens to be the minimum DevExpress sets in v26.2.
Step 1: Run an Assessment
Before changing code, get an honest picture of what stands between you and .NET 10. This is the job of GitHub Copilot app modernization for .NET, the Copilot agent that took over from the old Microsoft .NET Upgrade Assistant. In Visual Studio, right-click the solution in Solution Explorer and choose Modernize, or open Copilot Chat and invoke the agent:
@Modernize upgrade this solution to .NET 10
In VS Code or the Copilot CLI the agent is @modernize-dotnet. However you start it, the first thing it produces is an assessment.md in your repo: outdated NuGet packages, APIs with no modern equivalent, and the likely blockers. Read it before you agree to anything. That's the difference between planning a migration and discovering one.
Two caveats. Running the agent needs a paid GitHub Copilot plan, and if the Modernize option isn't there you'll need the GitHub Copilot app modernization component from the Visual Studio installer, so factor both in. And treat the assessment as a briefing, not a verdict: it's very good at finding the mechanical work, but the judgement calls are still yours.
Step 2: Review and Edit the Plan
Once the assessment's done, the agent writes a plan.md that sequences the upgrade into steps. The thing that matters about this file is that it's yours to edit. Reorder it, pull out steps you'd rather do by hand, and split anything that looks too big into smaller moves.
This is where you impose the one rule that keeps migrations safe, and I've learned it the hard way: never do it as a big bang. Work in small, reversible, committed steps, so a bad change is a git revert away rather than an afternoon of unpicking. A sensible plan for a DevExpress desktop app usually runs in this order:
- Convert the project to SDK-style, targeting
net10.0-windows. - Move dependencies from
packages.configtoPackageReference. - Prove the restore works against your feeds before attempting a build.
- Fix what modern .NET rejects, then clear the build, then clear the warnings.
Approve the plan only when it reads like something you'd have written yourself. Then let the agent start executing, one step at a time.
Step 3: SDK-Style, PackageReference and CPM
The first executed step converts the old project format and moves your references. A couple of things are worth watching, because they catch you out.
If your .NET Framework project relied on assembly references, DevExpress assembly names do not match the DevExpress NuGet package names (for example, for the DevExpress.XtraGrid assembly there is a DevExpress.Win.Grid NuGet package). If you do not want to trust AI for this important conversion, we recommend that you use our Project Converter and its deterministic Convert DevExpress assembly references to NuGet packages option before you trigger actual project conversion.
If the project sits at the repository root, default file-globbing in an SDK-style project can hoover up source from other projects nearby. This is one the AI caught that I'd have walked straight into: my project sat at the repo root, so the default globbing would've pulled in source from the projects around it. It turned globbing off and carried the explicit file lists across. Keep an eye out for it, because it's easy to miss and a pain to debug.
If your solution has more than one project, this is the moment to adopt Central Package Management (CPM), so every project shares one set of versions. Drop a Directory.Packages.props at the repository root:
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="DevExpress.Win" Version="26.2.*" />
</ItemGroup>
</Project>
That example pins the WinForms meta-package; for a WPF app you'd use DevExpress.Wpf instead. Individual projects then reference the package without a version, and your DevExpress and framework versions stop drifting apart across the solution. Prove dotnet restore succeeds against your component feed before you try to compile. Get restore green first and you skip a whole category of confusing build errors.
For more information on how your projects can benefit from CPM, please also review a related article Application Security — Project Dependency "Version Bumps" Demystified or Modern Security Realities in .NET / NuGet Ecosystem.
Step 4: Fix What Modern .NET Rejects
Now the code itself. On my migration the first compile threw 36 errors and roughly 1,795 warnings. That sounds alarming, and mostly it isn't: the errors collapsed to a handful of root causes, and the warnings came down to one. Clear them in that order, errors first and then warnings, committing as you go. These are the ones you're most likely to meet on a DevExpress desktop app.
Retire APIs with no modern equivalent
Swap out the usual suspects: an old zip library for System.IO.Compression, LINQ-to-SQL screens for a supported data path, dead app.config sections for their modern equivalents. The assessment will have flagged most of them.
Watch for namespace clashes
A properly sneaky one on my app: a bare Windows.Forms… reference stopped resolving on the new framework, thanks to a namespace collision that didn't exist on Framework. Fully qualify it and the error vanishes. One line, but a baffling one until you spot it.
The screens look wrong: fix DPI awareness
The app compiled and launched, but the forms came up subtly wrong: controls a touch out of place, spacing not quite right. That's the DPI-awareness default changing between frameworks, and it's the sort of thing you can stare at for a while before the penny drops. The AI worked it out from a screenshot and a description, pointed me at the DevExpress high-DPI docs to back it up, and one setting put the screens back exactly as they were. That trap is a WinForms one; WPF scales differently and won't hit it, though it has its own breaking changes to watch for, the removal of BinaryFormatter being the one that bites most often (learn more).
Collapse the warning storm
The warning count after a big migration can look terrifying and usually is anything but. Two moves cleared most of mine: declaring the assembly's supported-platform attribute to keep the compatibility analyser happy, and pinning a data-access package to the last version before it was marked obsolete. That second change alone dropped 500-plus warnings without touching a line of generated code.
Step 5: Lean on Compatibility Shims
You don't have to migrate everything before you can run. I'd assumed I would need to move my two in-house .NET Framework libraries first. I didn't. The modernised app compiled and ran on .NET 10 while still referencing those libraries through the .NET Framework compatibility shim. One thing to be clear about: that only holds while those libraries stick to APIs modern .NET still provides. You'll get a build warning, and any code path that calls an API .NET 10 has dropped will fail at runtime rather than at compile time, so exercise the paths that go through them before you call it done. Even so, if you've got in-house libraries pinned to Framework, leave them where they are for now. Get the main application onto .NET 10 first, confirm it runs, and book the deeper library work as a separate job that blocks nothing.
This is the single biggest reason these migrations stall for no good reason. Teams assume every dependency has to move first, and give up. It doesn't, and they don't. You don't have to boil the ocean. Get a working, current-LTS application in place, then chip away at the rest without holding the whole project hostage.
Where AI Fits, and Where You Stay in Charge
The reason this workflow holds up is that the agent proposes and you dispose. The assessment.md and plan.md aren't decoration; they're your checkpoints. You read the assessment, you edit the plan, you approve each step, and every change lands as a described commit you can review or revert. The AI carries the vast, repetitive sweep across dozens of files with no fatigue and no typos. The judgement and the checking stay with you. I never once typed "trust me," and that's the whole point.
Assess, plan, execute. The agent does the work; you sign off each stage.
Keep the Component Code Current: AI Skills and the MCP Server
The modernization agent is built to move your framework, not to know your component vendor's latest API surface. That gap does the most damage during a migration, because a migration is when you touch the most component code in the shortest time. Ask a general-purpose model to update a DevExpress grid or report and it'll often reach for an older namespace it saw more of in training. The code looks right, compiles, and sits there on a superseded API. Two DevExpress tools close that gap, and they're worth adding to the same IDE for the fix-up work.
DevExpress AI Skills hand the assistant current DevExpress namespaces, entry points and configuration patterns before it writes a line. They install into the coding assistant you already use (GitHub Copilot, Claude Code, Cursor or JetBrains), they work even without a live documentation connection, and they keep the component code on supported APIs rather than remembered ones. On a migration that pays off directly: fewer convincing-but-stale suggestions to catch in review, and a first answer you can build on instead of unpick. Call the relevant skill explicitly at the start of a task rather than trusting auto-detection, with # in Copilot or / in Cursor and Claude Code.
The DevExpress MCP Server adds the other half: live access to current documentation and version-sensitive detail when a task gets specific. Where a Skill sets the right starting point, the MCP Server supplies the authoritative reference to finish the job, so the assistant spends its lookups on real implementation questions instead of working out which API it should've used in the first place. Adding it is a one-line job in most assistants; in Claude Code, for example:
claude mcp add --transport http dxdocs https://api.devexpress.com/mcp/docs
Put them together and the split is clean: the modernization agent carries the framework move, the Skills keep your DevExpress code correct, and the MCP Server fills in the detail on demand. It's the same partnership principle as the rest of this guide, pointed at your component layer. I went deeper on why this matters, and how the two work together, with worked before-and-after examples in an earlier post: Beyond Prompts: Teaching AI How to Build with DevExpress. If you haven't read it, that's the companion piece to this guide.
Step 6: Verify You Are Actually Done
"It compiles" isn't the finish line. Before you call it migrated, confirm the app launches and the main workflows behave, the screens render correctly at your users' DPI settings, the warning count is down to something you actually understand, and each step is a clean commit in history. Only then start building new features on the modern foundation, which you can now do this week rather than someday.
The Takeaway
The blocker on most legacy migrations isn't capability. It's the sheer volume of small changes, and the fear of breaking an app that's quietly worked for years. This workflow deals with both. The modernization agent carries the tedium and surfaces the landmines as an assessment you approve, DevExpress AI Skills keep the component code on current APIs, compatibility shims stop a single old dependency holding everything hostage, and small reversible commits keep the risk low at every step.
With DevExpress v26.2 aiming to make .NET 10 the minimum, now is the time to plan this migration, not a reason to panic. The app I did this on was fifteen years old and threw 36 errors and getting on for 1,800 warnings on its first .NET 10 compile. It was running clean within days, and I was building new features on it the same week. The migration is the part everyone dreads. Worked in small steps, with the tooling doing the legwork and you making the calls, it turns out to be the manageable bit.
Further Reading
- GitHub Copilot app modernization for .NET (overview)
- How to upgrade a .NET app with Copilot modernization
- .NET Support Policy: LTS vs STS release cadence
- Central Package Management (NuGet)
- High DPI support in Windows Forms
- DevExpress AI Skills for coding agents
- Beyond Prompts: Teaching AI How to Build with DevExpress (companion post)