A development lead I worked with, let's call him Diego because that's not his real name, sent me a message that started with "we think we're ready to start the migration." His team had spent two weeks mapping their own codebase: which pages did what, which business logic lived where, what order to tackle things in. Reasonable, careful work.
Then someone opened the order-entry screen to plan its rewrite and found it was built almost entirely around a commercial data grid control. Sorting, inline editing, export to Excel, keyboard navigation, all of it handled by a vendor component licensed years earlier. The vendor's website still existed. Their "modern .NET" version did not. The product had been quietly discontinued, and the newest version anyone could find was built for .NET Framework and nothing else.
That single control was now the reason the whole screen couldn't move forward on schedule, and nobody had put it on the original two-week map, because it isn't code anyone wrote. It's easy to forget about something you've never had to think about.
Why this blocker hides so well
When a team plans a modernization, they instinctively audit what they built. Business logic, data access, authentication, the structure of their own classes and services. That's the code they wrote, the code they understand, the code that feels like "the system."
Third-party controls don't feel that way. You bought a license once, dropped the control onto a page, and it's been quietly working for a decade. It's not in your mental model of "our codebase" because it never felt like something you built. It feels more like a tool, closer to how you'd think about the operating system underneath everything, than like a piece of the application you're responsible for maintaining.
That's exactly why it gets missed in planning. It isn't hidden on purpose. It's hidden by familiarity, by having worked without complaint for so long that nobody has had a reason to open its folder and check what it actually is.
The three ways this plays out, and they are very different costs
Once you find one of these dependencies, there are really three outcomes, and the gap between the best and worst of them is enormous.
The good outcome: the vendor is still around, still maintains the product, and ships a version compatible with modern .NET. The conversion is mostly mechanical, updating references, adjusting a few API calls, re-testing the screen. A real task, but a bounded, predictable one.
The painful outcome: the vendor exists, but never shipped a version for modern .NET, and probably never will. There's no straight-line upgrade path. That screen needs to be rebuilt using a current library or a native browser approach, which usually means re-deriving the behavior the original control gave you for free, like sortable columns or complex validation, and sometimes the original scope of "upgrade" quietly becomes "redesign."
The worst outcome: the vendor is gone entirely. Acquired, shut down, or simply vanished, and nobody on the current team has the installer, the license key, or the source. I've seen this with controls that were central to a screen used every single day, where the only remaining copy of the working DLL was sitting in a bin folder on a production server, with no way to even confirm what version it was or get a replacement if that server ever needed to be rebuilt from scratch.
Why finding this during the Audit changes everything
The difference between these outcomes isn't something you discover by guessing. It's something you discover by actually opening every reference in the project and checking, vendor by vendor, control by control: is this still maintained, does a modern-.NET version exist, and is the license even still valid.
That check is exactly the kind of work that belongs in an Audit, before a single line of code gets touched. Finding out in week one that a screen needs a full rebuild changes the roadmap and the estimate, but it changes them on paper, where the only cost is a conversation. Finding out in month three, after a team has already committed a sprint to "just converting" that screen, changes the roadmap and the budget too, but now it also costs morale, a missed internal deadline, and an uncomfortable conversation with whoever is waiting on the project.
A known cost up front is a line item. A surprise mid-project is a crisis, however small the control itself seemed on the day someone first dropped it onto a page.
What this actually looks like in practice
The check itself isn't exotic. It's a deliberate inventory: every third-party assembly referenced in the project, every one checked against its vendor's current site, every license checked for validity, and every screen that depends heavily on one of them flagged with a note about which of the three outcomes applies. It takes real attention, but it's finite, boring work, which is exactly why it's worth doing on purpose rather than hoping nobody runs into it by accident.
I do this check on every Audit now, as a direct consequence of that message Diego sent me. It's one of the clearest examples I know of a small piece of due diligence buying you an enormous amount of certainty later.
The anchor you can't see is still an anchor
Legacy risk doesn't only live in code you wrote badly ten years ago. Sometimes it lives in a component you chose well ten years ago, that simply outlived the company that sold it to you. Either way, it anchors the project to the past until someone goes looking for it on purpose.
If you're planning a modernization and haven't yet inventoried every third-party dependency your screens actually rely on, that's worth doing before the roadmap gets written, not after.