A Web Forms application migrated to Blazor, with the evidence kept.

We took Microsoft’s MIT-licensed eShopLegacyWebForms sample and migrated it to Blazor on .NET 10 using our method. Every step left evidence: the inventory, the behavior recordings, the test results, the accessibility scans and a provenance record for every file.

Code, tests and full report: link coming.

The repository and its report will be linked here once published. Until then, email us and we will send the report.

What the run measured.

Parity tests pass on the new app
28/28
Parity tests pass on the new app
22 identical to the legacy recording, 6 through declared deviations. The legacy app matches its own recordings 28/28.
Automated accessibility violations
57 → 0
Automated accessibility violations
axe-core, WCAG 2.0 and 2.1, levels A and AA, counted by affected element across all 7 pages. Zero automated violations; not a Section 508 conformance claim.
Injected changes caught
9/9
Injected changes caught
The harness is proven able to fail. Run against each other's expectations, the two apps fail exactly the 6 declared deviations.
Unit tests
49/49
Unit tests
xUnit and bUnit, run on every build of the new app.

Automated accessibility violations, axe-core, WCAG 2.0 and 2.1, levels A and AA

Counted by affected element across all 7 pages. Run 8 November 2026.

Microsoft’s sample on Mono
57
Our Blazor migration
0
Zero automated violations

Where the 57 were, in the legacy app

  • Images with no text alternative (image-alt)27
  • Form fields with no label (label)12
  • Links with no readable name (link-name)7
  • Text below the contrast minimum (color-contrast)7
  • Drop-down lists with no label (select-name)4

Automated scans find only part of what WCAG covers. This is a measured floor, not a Section 508 conformance claim.

The catalog page, before and after.

Real captures from the run. The new page is meant to look like the old one: parity covers layout, text and addresses. Even the heading still says “Web Forms”, because the visible text was kept as recorded. The one visible change is the Create New button, darkened to pass colour contrast.

The product catalog list page of Microsoft's eShopLegacyWebForms sample, running as ASP.NET Web Forms on Mono
Before: Microsoft’s sample app, ASP.NET Web Forms on Mono
The same catalog list page after migration to Blazor on .NET 10: same layout and text, with a darker Create New button that passes colour contrast
After: our Blazor migration on .NET 10

What was migrated, and how.

Legacy applicationMicrosoft's eShopLegacyWebForms sample (MIT license): a small product catalog in ASP.NET Web Forms
Inventory7 pages, 2 master pages, 1 user control; 82 server controls, 13 event handlers, 6 routes
Behavior recorded33 recordings taken from the running legacy app before any new code existed
New applicationBlazor on .NET 10, static server-side rendering, no JavaScript
First draftClaude Opus 5.5, running in Claude Code. The unmodified draft was committed before it was built or tested.
URLsEvery URL the legacy app served is served at the same address with the same status. Legacy .aspx?id= addresses, which crashed, now get a 301 redirect to the route URL.
Human reviewPending owner review. No person has reviewed the migrated code yet; every result on this page is automated.
Run8 November 2026, commit a19dfdc, one command that rebuilds both apps and reruns every check

6 differences, each on purpose and written down first.

Recording the legacy app turned up bugs. Each one was declared, with the new behavior expected in its place, before any new code existed. All 6 are pending owner review.

Opening a product before any list page no longer fails

The legacy app returned HTTP 500 if Details, Edit or Delete was the first page opened on a fresh server, because brand and type were only attached while a list rendered. The new page renders as legacy does once the list has been viewed.

An unknown product id returns 404, not 500

Legacy dereferenced a missing product and crashed. A missing resource is now a 404.

Product names are HTML-encoded on the Details page

Legacy bound the name through a control that does not encode, so a stored name containing markup was emitted as markup: a stored cross-site-scripting hole. Razor encodes by default, which also makes Details match the list page.

Old .aspx?id= addresses redirect instead of crashing

Details.aspx, Edit.aspx and Delete.aspx with ?id= in the query string returned HTTP 500, because the pages read the id only from the route. They now get a permanent (301) redirect to the route address the app itself uses. Three deviations, one per page.

The security fix, on screen

The legacy Details page for product 9: the stored name Cup<T> White Mug is shown as Cup White Mug because the markup in it was emitted unencoded
Before: the stored name Cup<T> White Mug, shown as “Cup White Mug”. The markup in it reached the browser unencoded.
The migrated Details page for product 9: the name is HTML-encoded and shown as written, Cup<T> White Mug
After: the same record in our Blazor migration, encoded and shown as written.

Product 9's stored name contains markup. The legacy Details page sent it to the browser as HTML, so the browser swallowed it. Product text containing a script would have run the same way. The new page encodes it, which is what the legacy list page already did.

What this demonstration does not show.

A 7-page sample, not a production system

7 pages, 2 master pages and 1 user control. A system with hundreds of UI components has more state, more roles and more integrations; the demonstration shows the method, not the scale.

No authentication or authorization

The sample has none (the inventory counts 0 authorization checks), so the demonstration does not exercise authorization parity or sign-in. In an engagement those get their own tests.

Mono, not IIS

The legacy app ran on Mono 6.12 with XSP4, in a container (not IIS). 6 small fixes were needed to serve it there; each is documented with its exact error.

Microsoft published its own Blazor port of this sample

Ours is independent and does not use or copy it. The port itself is deliberately ordinary; what the demonstration offers is the test-first, audited process around it.

A first-run pass proves nothing on its own

The draft passed every parity test on its first run, and it was written with the recordings available. That is why the cross-check and the injected-change check exist: they show the tests can fail.

The same process, on one area of your system.

A fixed-price pilot inventories one functional area and migrates two or three representative pages with full parity tests, accessibility results and the provenance log.