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
- 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 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.


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