Case study / Parking operations

A 10-year-old .NET desktop uploader rebuilt as a Linux web app, with the Kerberos problem solved

45/45 legacy behaviors reproduced exactly, proven by a dual test harness

Book a 20-minute callDraft · client permission pending
Client
A national parking operator (employer, 2023–present)
Industry
Parking operations
Timeline
5 months, Mar–Jul 2026, sole author
Services
Legacy modernization, DevOps / infra, Compliance tooling
Stack
ASP.NET Core 8C#React 19TypeScriptViteIBM i / DB2unixODBCJumpCloud SAMLnginxFlaUI
Results
45/45 upload definitions with identical pass/fail outcomes on legacy and web (37 pass, 8 fail, both sides)22 special column-validation rule types reproduced from the decompiled legacy app8 min full definition sweep on the web app, against about 47 minutes on the desktop app30+ users online at once on a typical day, with no client install94% upload success rate on a typical day (130+ uploads, filterable by environment)
Status
internal
The web uploader: upload definitions with their AS/400 authority levels, an environment switcher, and an admin 'Act as' control
The web uploader: upload definitions with their AS/400 authority levels, an environment switcher, and an admin 'Act as' control

The problem

Finance and operations teams at a national parking operator pushed month-end data into an IBM i (AS/400) system of record through a Windows desktop uploader. General-ledger journals, revenue, payroll accruals, asset retirements, and HR data all went through it. The app was roughly ten years old, WPF on the .NET Framework, deployed by ClickOnce from a file share, installed on every user’s machine. The original developer was gone. No documentation, no test suite, and nobody who could say what its 22 column-validation rules did.

The business could not stop using it and could not safely change it.

Constraints

  • Behavior had to match exactly. A journal the old app accepted had to pass, and a bad one had to fail with the same message. Finance would not sign off on “close enough.”
  • No client install. Browser only, with single sign-on, across dev, UAT, and production.
  • Linux host, Kerberos-only mainframe. The desktop app rode each user’s Windows identity to the AS/400. A Linux server has none to ride. AD passwords are not AS/400 passwords, and per-user ODBC logins were locking accounts.
  • Per-user authorization and audit had to survive. Each upload definition is secured by an AS/400 authority level, so every write had to run under the real person’s profile.
  • One engineer. Sole author, March through July 2026, alongside the rest of the job.

What I built

A browser-based replacement on ASP.NET Core 8 and React, running on Ubuntu Linux behind an nginx TLS proxy. Users sign in through the company identity provider (JumpCloud SAML) or direct LDAP; group membership drives roles. The home page lists every upload definition with its authority level and a template action, with an environment switcher for Development, UAT, Production, and Local Test. The upload flow is a five-step state machine, and a local preview produces the converted spreadsheet without writing to the database.

I decompiled the legacy app to get an exact behavioral reference, about 4,470 lines of recovered C#, and cited legacy line numbers in the rewrite. All 22 validation types were reproduced: date-format variants, auto-populated columns, justify and leading-zero rules, yes/no variants, location and GL-account lookups, a cross-row net-zero balance check, and email and domain checks. A checksum guard ties validation to execution so a validated file cannot be swapped before upload.

The identity design is the part worth paying for. All database access runs through one AD service account and one AS/400 service account that holds profile-swap authority. Each request resolves the signed-in user to their AS/400 profile from a directory attribute, then swaps the job to that profile before any work happens. The upload runs under the real user’s authority, the real user appears in the AS/400 audit trail, and no user ever types an AS/400 password. Admins get an “Act as” control that surfaces the same swap for support work, logged like everything else. This is what made a Linux host viable against a Kerberos-only system.

To prove parity, I built two test harnesses. On the legacy side, a .NET console app drives the running WPF app through Windows UI Automation (FlaUI): pick the environment, walk the definition list, save a template, load, validate, upload, record. On the web side, an Automated Testing page runs the same sweep of all 45 definitions against Dev or UAT using four template sets (live, current, known-good, known-bad) and keeps every run’s pass and fail counts. Both emit the same JSON and Markdown, so results diff cleanly.

Architecture

A layered .NET solution: a core project with the domain model and validation engine, an infrastructure project for ODBC access and the upload and preview services, a REST API, and a React 19 + TypeScript + Vite front end. The IBM i connection uses unixODBC with three connection modes matching the system’s naming semantics. The service runs as a non-root systemd unit with secrets in a root-only file, a health-check gate on deploy, structured logging, and log rotation, on a host in a private subnet with no public IP. A System Usage page shows active sessions, the day’s upload history by environment, and live server resources.

The mainframe side took the most debugging: temporary-library handling, a wrapper procedure that silently swallowed nested calls, and CL versus SQL procedure calling conventions. Each root cause is documented with job-log evidence.

System Usage on a typical day: concurrent users, upload history by environment, success rate, and live server resources

Automated Testing: every upload definition run against the legacy app and the web app with four template sets, with months of run history

Results

  • 45 of 45 upload definitions behave identically on the legacy and web apps: 37 pass and 8 fail on both sides, zero outcome mismatches, byte-identical error messages.
  • The full sweep takes about 8 minutes on the web app against about 47 minutes on the desktop app.
  • On a typical day, 30+ users online at once and 130+ uploads at a 94% success rate, with zero client installs.
  • Zero AS/400 passwords handled by the browser or the user, and every upload logged under the real user’s profile.
  • About 5,500 lines of C# and 2,000 of TypeScript, xUnit coverage, and a tracked list of remaining gaps before every upload type is production-ready.

What this proves

  • Legacy Modernization Assessment: I can take an undocumented, orphaned application, recover its real behavior, and prove the replacement matches it with numbers instead of assurances.
  • Fractional CTO: the identity design and the parity harness are architectural decisions, not features. Choosing them early is the job.
  • Internal Tool in a Week: the parity harness was a small, self-contained tool that paid for itself on the first comparison run.

Similar project?

Have a desktop or mainframe-facing app that nobody dares touch? Book a 20-minute call.

Published Oct 8, 2026

Next step

Similar project?

Twenty minutes to see if this pattern fits your problem.