Almost every legacy .NET migration starts with the same proposal: build the new system alongside the old one, switch over when it reaches parity. It is easy to explain, easy to estimate, and it fails more often than it succeeds.
It fails for a reason that has nothing to do with engineering skill. The old system does not stop changing while you rebuild it. Every fix and every new feature has to land twice, so the team is paying a permanent tax while producing nothing shippable. Eighteen months in, the new system is at eighty per cent of a target that has itself moved, the business has seen no return, and the project is cancelled — usually by someone new who inherited it.
The incremental version
The alternative is unglamorous. You move one bounded area at a time, and the system stays deployable after every step. There is never a moment where you are holding two systems and hoping.
The order matters more than the speed:
- Assess before touching anything. A written pass over the code and schema: what the architecture actually is, not what the documentation claims, and where the risk concentrates.
- Find the seam. One area with a clear boundary and low coupling — often a background job, a report, or an integration rather than the screens everyone talks about.
- Characterise before changing. Tests that assert what the system currently does, including the parts that look wrong.
- Move it, ship it, run both briefly. Old path stays until the new one has been live long enough to trust.
- Repeat, and stop when the remainder is not worth it. Some legacy code is stable, understood and cheap to leave alone. Migrating it is engineering for its own sake.
Characterisation tests are the whole game
The hardest part of legacy work is not old syntax. It is that behaviour exists only in code, nobody who wrote it is still there, and some of what looks like a bug is load-bearing — a downstream process depends on it.
A characterisation test does not assert what the code should do. It asserts what it currently does. Then, when the migrated version disagrees, you find out during migration instead of from a customer.
Expect this to be uncomfortable. You will write tests that lock in behaviour you consider wrong. That is correct: changing behaviour and changing platform at the same time means you cannot tell which one broke production. Separate them. Migrate first, fix second.
What actually blocks the move
Language and framework changes are mostly mechanical. The things that stop a migration are almost always dependencies:
- Packages with no .NET 8 equivalent. Find these on day one, not in month four. Each one is either a replacement, a rewrite, or a reason to leave that area on the old runtime.
- Windows-specific APIs. Registry access, COM interop, GDI+ image work, Windows services hosting.
- Web Forms. There is no migration path. Any Web Forms surface is a rewrite of that surface — usually to an API plus a modern frontend — so it should be scheduled as a rewrite and priced as one.
- Configuration and identity.
web.config, machine keys, and anything relying on IIS-hosted Windows authentication need a deliberate plan rather than a translation.
Should SQL Server become PostgreSQL at the same time?
Usually not. Two migrations at once means that when something breaks you have two suspects.
If licensing cost is the reason — and it often legitimately is — treat it as a separate project after the runtime move is stable. It is a real migration in its own right: syntax, types, collation behaviour, identity columns, and any stored procedure logic all need attention. If SQL Server is working and already paid for, the honest recommendation is frequently to leave it and spend the budget on something a user will notice.
Pricing it honestly
Anyone quoting a fixed price for a legacy migration they have not looked inside is guessing, and the guess will be wrong in one of two directions — either they lose money and start cutting corners, or you overpay for risk that never materialised.
The workable shape: fixed price for the assessment, delivered as a document you keep regardless of what happens next, then a monthly retainer for the migration itself with a visible board. You can stop after the assessment. Plenty of people should — sometimes it concludes the system is fine and the real problem is somewhere else entirely.