A .NET 8 → 10 migration that taught me to read the dependency graph first
I just finished a dry run of migrating our Optimizely CMS 12 solutions from .NET 8 to .NET 10. Dozens of projects. Flipping the target framework was the easy bit and went fine. All the real work was hiding in the dependency graph. A few things I want to write down before I forget them.
An old reflection hack finally died
Somewhere in the codebase there was code reaching into the private fields of MemoryCache to enumerate its keys. Someone wrote that years ago because there was no public way to do it. .NET 9 added MemoryCache.Keys, so the whole workaround collapsed into a delete. That felt good.
It’s worth re-checking these old hacks on every upgrade. A surprising number of them exist only because the framework was missing something at the time, and that gap has quietly closed since.
The “obvious” EF Core bump almost took everything down
Moving to EF Core 10 looked like a no-brainer. It wasn’t. EF Core past v6 pulls in Castle.Core 5.x, and the Castle.Windsor build that ships inside the CMS still depends on Castle.Core.Pair<,>, which was removed in Castle.Core 5.0. So the shiny “modern” upgrade would have killed DI at startup.
Left where they were, the older EF Core packages load perfectly fine on the .NET 10 runtime. That was the lesson: the package version mattered far less than whether it runs on the runtime I was actually targeting. New isn’t the goal. Working on the target runtime is.
Central package management earned its keep
If your repo uses ManagePackageVersionsCentrally, most of the version churn comes down to a single edit in Directory.Packages.props. On a solution this size that’s the difference between an afternoon and a week. If you haven’t turned it on yet, this is your sign.
Where it ended up
Clean build, green tests, running on .NET 10 LTS, and with a much smaller dependency diff than a blanket “bump everything to 10” would have produced. One commercial module still has no official .NET 10 build, so it stays off production until it gets one. Running and supported are not the same thing, and I’d rather not find that out in an incident channel.
The thing I keep relearning, upgrade after upgrade: a framework migration is mostly dependency resolution wearing a costume. Read the transitive graph before you trust the restore.