Migrating a 2014 .NET Framework application to .NET 10 + Angular — without a rewrite
A wholesaler's order system (ASP.NET MVC 5, Web API 2, EF6, AngularJS, business rules in stored procedures) migrated route by route behind a YARP facade, with the new pricing proven equal to the legacy stored procedure by differential tests and every switch backed by shadow traffic.
▶ Live demo · Architecture decisions · Français
![]() |
![]() |
| New: an express quote the legacy could not do | The 2014 screens, unchanged, now partly answered by .NET 10 |
![]() |
|
| Migrated: the sign-in screen, while the 2014 application still checks the password (ADR 0010) | |
- Open the demo and sign in as
sophie/comptoir-demoon the new sign-in screen: the password is still checked by the 2014 application, through the facade (ADR 0010). Its old screen is one click away. - You are in the AngularJS application. Open the orders, open one: these reads are in shadow mode — the legacy answers, and the facade replays them on the new API and compares.
- Open
/app/migration(same session, no second login): the plan route by route, and the comparisons you just generated, identical or with the JSON path that differs. - Open the express quote: choose a customer and 120 sacks of flour — volume discount, free-shipping threshold and VAT are computed live by the .NET port. Create the draft: it opens in the 2014 screen, which can confirm it (new code), ship and invoice it (legacy stored procedures), on the same database.
Everything runs on free tiers that sleep: the first request after a quiet period can take 20–40 seconds.
| If you care about… | Look at |
|---|---|
| Proving behaviour is preserved | PricingParityTests: random orders priced by the stored procedure on SQL Server and by the port, equal to the cent, mutation-checked — ADR 0004 |
| Incremental migration | Comptoir.Facade: per-route Legacy / Shadow / New modes from configuration — ADR 0001 |
| Evidence before switching | Shadow replay with semantic JSON diff, bounded queue, live dashboard — ADR 0006 |
| Two systems, one database | The confirmation port keeps the procedure's lock hints; tested by legacy and new confirmations racing — ADR 0003 |
| Identity during a migration | Forms cookie → 5-minute JWT at the facade, legacy cookie stripped — ADR 0005; sign-in screen migrated, password still checked by the legacy — ADR 0010 |
| Not breaking the old UI | The new API reproduces the Web API 2 contract, error shapes included — ADR 0007 |
| Respect for the legacy | Untouched except one endpoint; built on Windows in CI with every Razor view precompiled — ADR 0002 |
| Repository layout | legacy/ and the new side in one repo, so a migration step (rule + differential test + route switch) is one commit — ADR 0009 |
| Hosting | IIS on App Service Windows, Azure SQL free offer, Container Apps, all Terraform — ADR 0008 |
flowchart LR
user([Sales, warehouse]) -->|one origin, one cookie| facade
subgraph facade[Facade — YARP · .NET 10 · Container Apps]
plan[Migration plan<br/>Legacy · Shadow · New]
bridge[Auth bridge<br/>Forms cookie → JWT]
shadow[Shadow worker<br/>JSON diff]
spa[Angular 22 under /app]
end
facade -->|Legacy, Shadow| legacy[2014 application<br/>MVC 5 · Web API 2 · EF6 · AngularJS<br/>IIS · App Service Windows]
facade -->|New, shadow replay| api[New API<br/>.NET 10 · EF Core · internal only]
legacy --> db[(Azure SQL<br/>legacy schema + stored procedures)]
api --> db
legacy/
Comptoir.Web/ the 2014 application (net48): MVC 5, Web API 2, EF6, AngularJS 1.8 — left as it is
Database/ schema, stored procedures (pricing, stock, numbering), demo data — still the schema's owner
src/
Comptoir.DbMigrator/ DbUp: replays legacy/Database in deployments and tests
Comptoir.Domain/ pricing and stock rules ported from the procedures (pure C#)
Comptoir.Api/ .NET 10 API: legacy contract on legacy tables, plus /api/v2
Comptoir.Facade/ YARP: migration plan, auth bridge, shadow traffic, serves /app
Comptoir.AppHost/ Aspire: facade + API + Angular dev server locally
tests/ differential (vs stored procedures) · API on SQL Server · facade
web/ Angular 22: migration dashboard, express quote, catalogue
deploy/terraform App Service Windows, Azure SQL (free), Container Apps
Prerequisites: .NET SDK 10, Node 24, Docker. The legacy itself needs Windows (CI builds it there); locally the facade talks to the integration legacy — see ADR 0008.
dotnet test --solution Comptoir.slnx # differential + API + facade tests (SQL Server in Docker)
dotnet run --project src/Comptoir.AppHost # facade + API + Angular, against the integration legacy (user secrets)
./deploy/azure.sh # everything to Azure (Terraform)The demo scales to zero. comptoir.jassbr.me is a static waiting page on Vercel (deploy/wake,
./deploy/wake.sh): it wakes the facade on app.comptoir.jassbr.me and forwards the visitor once it answers, instead
of letting them meet the platform's bare error page during the ~30 s cold start.
| Gate | |
|---|---|
| Legacy | MSBuild on Windows, every Razor view precompiled, IIS package as artifact |
| New side | analyzers with warnings as errors, dotnet format, tests on real SQL Server with the legacy procedures, coverage gate 80 % |
| Frontend | Prettier, ESLint (OnPush), Vitest with thresholds (98 % statements) |
| Delivery | both container images built, terraform validate, CodeQL, Dependabot (legacy excluded on purpose) |
Comptoir montre la migration d'une application de gestion commerciale de 2014 (.NET Framework 4.8, ASP.NET MVC 5, Web API 2, EF6, AngularJS, règles métier en procédures stockées) vers .NET 10 et Angular 22, sans réécriture : une façade YARP bascule les fonctions une par une, la nouvelle tarification est prouvée identique à la procédure stockée par des tests différentiels, chaque bascule est précédée de trafic en ombre, et les deux applications partagent la base pendant la transition. Essayer la démo.



