We Moved a 15-Year-Old .NET Framework App to .NET 10 in Days and let an AI assist

I’ve been building software a long time. Long enough to have 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 say the project I’m about to describe had been sitting in the “someday” pile for years, you’ll understand it wasn’t laziness. It was scar tissue.

The app is a large VB.NET WinForms line-of-business application on .NET Framework 4.8. More than a decade of growth: hundreds of forms, powered by DevExpress UI control suite, a couple of in-house libraries, and the usual sediment of dependencies that were sensible in 2012 and a bit embarrassing now.

The reason nobody touched it is the reason these jobs stall everywhere. The migration isn’t hard. It’s vast and fiddly. Thousands of tiny, mechanical, error-prone changes, and one wrong move turns a working app into a non-compiling mystery you’ll spend a day unpicking.

This time I did it. I paired with an AI for the grunt work, kept my hands on the wheel for anything that mattered, and took the thing to .NET 10. It compiled with zero errors and launched. Here’s the honest account, and why I’d do it again.

TL;DR: Legacy .NET migrations aren’t blocked by difficulty. They’re blocked by volume and risk. That’s precisely the shape of problem an AI is good at: the tedious sweep, the obscure gotchas, and putting every risky decision in front of me to approve. I went from “untouchable” to “running on the current LTS” in days, in small reversible steps.

What I was starting with

Nothing exotic. Just a lot of it:

  • An old non-SDK .vbproj with packages.config and a forest of GAC and HintPath references.
  • A commercial WinForms suite, referenced the old way.
  • Two in-house libraries still pinned to .NET Framework.
  • The greatest hits of “won’t run on modern .NET”: LINQ-to-SQL, a legacy zip library, a couple of dead config sections.

If you’ve worked on real enterprise software, you’ve met this codebase. Probably you maintain one.

First call: which .NET?

The first genuinely useful thing the AI did wasn’t code. The obvious target was .NET 8. It pointed out that 8’s LTS window was nearly up, while .NET 10 was the current Long-Term Support release with years of runway. That’s one line in a project file standing between “done” and “doing this again next year.” Thirty-five years in, I know how much a volunteered piece of context like that is worth. Most juniors wouldn’t have raised it.

The migration, in small moves

I’ve learned the hard way to never do these as a big bang. We worked in small, reversible, committed steps:

  1. Convert the project to SDK-style, targeting net10.0-windows. (One the AI caught that I’d have walked straight into: the project sits at the repo root, so default file-globbing would have hoovered up other projects’ source. It turned globbing off and carried the explicit file lists across.)
  2. Move every dependency to PackageReference, and prove the restore worked against the local component feed before trying to build.
  3. Fix the code modern .NET rejects: swap the old zip library for System.IO.Compression, retire the LINQ-to-SQL screen, and so on.
  4. Clear the build, then clear the warnings.

That last step is where the partnership earned its keep. The first compile threw 36 errors, and most of them collapsed to a handful of root causes, including a properly sneaky one where a bare Windows.Forms.… reference stopped resolving thanks to a namespace clash on the new framework. Then roughly 1,795 warnings came down to one, via the sort of fixes I’d normally lose an afternoon to: declaring the assembly’s platform attribute to satisfy the compatibility analyser, and pinning a data-access package to the last version before it was marked obsolete, which cleared 500-plus warnings without touching a line of generated code.

And the “why do the forms look wrong” moment everyone hits on this trip? The app booted but the screens were subtly broken. It was the DPI-awareness default changing between frameworks. One setting put it back exactly as it was. The AI worked it out from a screenshot and a description and pointed me at the DevExpress docs to back it up.

The bit that surprised me

I’d assumed I’d have to migrate the two in-house libraries first. I didn’t. The modernised app compiled and ran on .NET 10 while still referencing the .NET Framework libraries through compatibility shims. So I had a working, current-LTS application straight away, and could book the deeper library work as a separate, non-blocking job instead of a prerequisite that holds everything hostage.

That’s the thing people miss about these migrations. You don’t have to boil the ocean. You need a partner methodical enough to find the incremental path, and the discipline to take it.

Why I’d trust the process

I’ll be precise, because “the AI did it” is doing a lot of dishonest lifting in most posts like this one. I went in a sceptic. Here’s what actually happened, and why it held up:

  • It was a pair, not an autopilot. The AI proposed, I built it in Visual Studio and pasted back the errors, it fixed and explained. Tight loop, evidence at every step. I never once typed “trust me.”
  • It was honest about its limits. It couldn’t run my full build locally, and it said so, then validated what it could and handed the actual compile to me as a checkpoint. Calibrated confidence beats bravado, and I’ve fired contractors for less of the latter.
  • Every risky decision came to me as a choice, not a silent change: which framework, how to handle a feature with no modern equivalent, what to do when the data didn’t line up. I stayed in control of everything that mattered.
  • It brought judgement, not just syntax: LTS timelines, DPI behaviour, analyser internals, version-obsolescence tricks. The stuff that usually lives in a grey-haired engineer’s head and fifteen browser tabs.
  • It worked in commits. Small, described, reversible, sitting in git history like any other change, reviewable by me line by line.

The AI was excellent at the two things that make these jobs miserable: the vast, repetitive sweep across dozens of files with no fatigue and no typos, and total recall of obscure gotchas with the citations to prove them. I was there for the two things that keep a migration safe: judgement and verification. Put those together and a project that had been “someday” for years became “this week.”

The takeaway

If you’re sitting on a large legacy codebase, the blocker probably isn’t capability. It’s the volume, and the very reasonable fear of breaking something that’s been quietly earning its keep for a decade. That’s exactly the problem an AI pair is built for: it carries the tedium, flags the landmines, moves in small reversible steps, and asks before it does anything it can’t take back.

I turned an untouchable 4.8 app into a clean .NET 10 application, and started building new features on the modern foundation the same week. The migration was the part everyone feared. Done properly, with the right help, it turned out to be the easy bit.

After 35 years, I’ve stopped believing in silver bullets. This isn’t one. It’s a sharp tool in steady hands, and on this job that combination did in days what used to take months.

Leave a Reply

Discover more from Paul Usher

Subscribe now to keep reading and get access to the full archive.

Continue reading