Back to blog

Migration

Case study: 52,000 lines of legacy code migrated to .NET 10 with Claude Code — in half a day

Every .NET developer knows them: the legacy application that has been running reliably on a Windows Server for years, whose migration sits "somewhere" on the roadmap — except "somewhere" has the awkward habit of never arriving.

SvK by Sven von Känel 15 min read
  • Migration
  • Claude Code
  • .NET 10
  • Legacy Code

TLDR

A ten-year-old enterprise application (52,300 LOC, .NET Framework 4.7 + AngularJS 1.4) was migrated to .NET 10.0 with Claude Code in less than half a person-day — including the elimination of 13 proprietary NuGet packages, containerisation, and a frontend upgrade. The key pattern: research → plan → execute. Result: 31% less code with identical functionality, cross-platform-ready and CI/CD-ready.

Introduction

Every .NET developer knows them: the legacy application that has been running reliably on a Windows Server for years, whose migration sits "somewhere" on the roadmap — except "somewhere" has the awkward habit of never arriving. TimeWizard was no exception. A ten-year-old enterprise application with 52,000 lines of code, 23 SignalR hubs, and 13 proprietary NuGet packages. The need to migrate was obvious; the manual effort just as discouraging.

The actual trigger wasn't a strategic awakening but Microsoft's calendar: Windows Server 2016 was approaching the end of security updates. Since our hosting infrastructure now runs on Rancher RKE2 Kubernetes, an upgrade to a new Windows Server version wouldn't only have cost money — it would have permanently bloated the parallel tech stack. Migrating with a coding agent was therefore a test — not only because of the time savings, but also of the approach itself.

This article documents how we ran the migration with Claude Code as the coding agent in less than half a person-day — and which patterns turned out to be transferable.

The starting point

TimeWizard is a multi-tenant enterprise application for time tracking and task management (first built in 2015). The system was based on .NET Framework 4.7 with ASP.NET MVC 5 and Web API 2 in the backend, combined with an AngularJS 1.4 single-page application in the frontend. Real-time communication ran via SignalR 2.4, data access via Entity Framework 6.3 against MS SQL Server.

Architecture at a glance

The backend solution comprised 10 C# projects with 151 classes and around 20,300 lines of code. Particularly characteristic: 23 SignalR hubs — almost every entity had its own hub with standardised CRUD operations through a generic base class. All database access ran through a single repository class TWRepository with 29 public methods — a classic god-object antipattern.

The frontend was implemented as an AngularJS SPA with 49 routes, 88 HTML templates, and around 16,200 lines of JavaScript. A separate Windows service with Quartz.NET handled scheduled tasks.

Overall complexity

Metric Value
C# projects in the solution 10
C# classes 151
Entity / model classes 23
Web API controllers / SignalR hubs 12 / 23
Angular controllers / services / directives 44 / 86 / 34
NuGet dependencies (unique) 65
Lines of code, backend (C#) ~20,300
Lines of code, frontend (JS + HTML) ~32,000
Total lines of code ~52,300

Technical debt — the greatest hits

Anyone opening the repository stumbled across an impressive collection of technical debt — every item with the unmistakable scent of "this seemed like a good idea at the time":

  • No automated tests. No unit or integration tests at all. Every change required manual testing — quite a gamble at 52,000 lines of code.
  • Outdated frontend stack. AngularJS 1.4 had reached end-of-life in January 2022. Together with jQuery 2.1 and Bootstrap 3, a trio that no longer saw security updates.
  • 13 proprietary Evanto.Common NuGet packages formed the foundation of the application — from the repository base class through authentication to the SignalR infrastructure. Tight coupling that turned every modernisation into a major project.
  • Windows binding. .NET Framework 4.7 ruled out both containerisation and cross-platform deployment. Configuration ran via Web.config transformations — cumbersome and error-prone.
  • No database migrations. The schema existed only implicitly in the DbContext. Reproducible deployment? More wishful thinking.

Approach

The migration followed a deliberately sequential flow in eleven steps. The basic principle: first migrate the backend completely, then adjust the frontend only enough to talk to the new backend, and finally update the JavaScript dependencies. Each step was formulated as a standalone prompt to Claude Code — the verbatim prompts are documented below as block quotes.

1. Establish project context with CLAUDE.md

Before the agent can work productively, it needs an understanding of the existing codebase. We used the custom command /create-rules for that — an extended variant of the built-in /init command. The command analyses the tech stack automatically, recognises project type and directory structure, extracts naming conventions from the existing code, and generates a CLAUDE.md file in the project root.

This step deliberately comes first, because the quality of every following plan and implementation depends directly on the agent's understanding of the project.

2. Create a migration plan

In the second step we asked Claude Code in /plan mode for a detailed migration plan. The central strategic decision: backend first, leave the Windows service aside for now, and only touch the AngularJS frontend as far as needed for the SignalR connection. The proprietary Evanto.Common libraries were to be eliminated as far as possible.

The application has basically three parts, classic .NET 4.7 backend, scheduler and AngularJS frontend. Please create a detailed plan how to migrate the backend to latest .NET core 10 framework. Don't touch scheduler in first step and the AngularJS UI only as far that it is able to connect to the migrated backend (mostly done via SignalR web sockets connection).
Clarify if all needed dependencies (external libraries) can be migrated too. If not, research alternative solutions. Ask questions if there are things to clarify or implementation options so that we are finally on same track. The source for the Evanto.Common.* libraries is in folder ./common/lib. Basically I do want to eliminate as much as possible of them. Please migrate the absolutely necessary parts to project specific libraries in a ./lib folder. Split the migration in multiple steps if it is to complex to be handled in one step.

Why this prompt works: it sets clear guard rails (backend first, leave the scheduler aside), names the goal for the proprietary libraries explicitly, asks for clarifying questions, and lets the agent assess complexity itself and split the work where needed. Claude Code then produced a multi-step plan, identified non-migratable dependencies with alternatives, and asked clarifying questions about implementation options.

3. Run the backend migration

Executing the migration plan ran fully autonomously — 75 minutes without a single interruption or question on a Mac M4 Pro. In that time, Claude Code migrated the entire backend codebase from .NET Framework 4.7 to .NET 10.0, ported the SignalR hubs to ASP.NET Core SignalR, replaced the Evanto.Common libraries with project-specific implementations, and adjusted the frontend-side SignalR connection.

4. Generate and validate a migration report

After completion, we asked for a detailed report on the current status. This step is for validation: which parts were migrated successfully, what's open? A concrete example: the source project used two connection strings (application database + authentication); the migrated project only one.

Please write a detailed report about the execution of last plan (migration from .NET 4.7 to .NET core). Check open issues and current status, e.g. in user management (source project has two connection strings for app and authentication, target project only one for app database, whats with the authentication?).

5. Modernisation and best practices

With a working backend as the foundation, we pulled current .NET best practices in through a series of focused prompts. Each prompt addressed a specific topic:

Central package managementDirectory.Packages.props, Directory.Build.props, and global.json for centralised version management:

Please introduce a centralized management via introducing Directory.Packages.props, Directory.Build.props, and global.json to enable easier management.

Package updates and SLNX format — updating all NuGet packages and switching to the modern solution format:

Please update all packages to latest versions but no preview versions. Verify with building solution. Please convert the .NET solution file from SLN to SLNX format and verify build.

Containerisation and CI/CD — Dockerfile and GitLab CI pipeline:

Please create me now a Dockerfile and a .gitlab-ci file in root ./ for ./src/TimeWizard.Web website project. Take EXISTING_PROJECT as reference.

Email sending with Coravel Mailables — migration to the Coravel mailable pattern, with an existing project as reference:

Please create me a plan to migrate the email sending in the ./src project to Coravel Mailables. Please identify first all places where mails are sent and update then to Coravel Mailables. See following project for reference: EXISTING_PROJECT

Configuration via environment variables.env-based configuration for sensitive data:

I do want to support configuration by environment variables via an .env file to avoid storing sensitive credentials like connection string and passwords in appsettings.json. Please create me a suitable .env file and adapt configuration loading in Program.cs. Modify also .gitignore to avoid storing the .env file in GIT.

6. Backend test

Since the project had no automated tests at all, we validated the migrated backend by manual testing. The only immediate problem: PDF generation. The original application used Windows system fonts that simply don't exist on macOS or in Linux containers. A typical migration issue when moving to cross-platform .NET. The solution — a custom PdfSharp FontResolver that resolves fonts from embedded resources — Claude Code generated automatically once we passed in the error from the application log as a prompt.

7. AngularJS upgrade

For the frontend modernisation we deliberately chose a three-stage approach: research → plan → execute. In the first step, Claude Code was asked to find out which AngularJS version a safe upgrade was possible to:

The project contains an AngularJS 1.4.x frontend. Latest AngularJS version is 1.8.4. Please make a careful research how we can update AngularJS to a higher 1.x.x version without breaking the JavaScript Dependencies.

The result: version 1.8.3 is the last actually available release (1.8.4 doesn't exist as a complete release). Claude Code then produced an upgrade plan and ran it after approval.

8. jQuery and other JavaScript dependencies

Following the same research → plan → execute pattern, we updated the remaining frontend dependencies. The biggest jump was jQuery from 2.1.4 to 3.7.1. The three-stage approach has proved a reliable pattern for upgrades across multiple major versions, because the agent identifies breaking changes before execution and accounts for them in the plan.

9. Regenerate CLAUDE.md

After all migration steps, we generated CLAUDE.md again with /create-rules. This step is necessary because the project state has changed fundamentally: new framework, new project structure, different dependencies. So that the agent works with current context for the following frontend fixes, the CLAUDE.md must reflect the migrated state.

10. Frontend test and bug fixing

After JavaScript upgrades across multiple major versions, regressions were to be expected. When issues came up, two paths were available: pass screenshots and console logs as a prompt — or, much more efficient, give the agent the URL of the running application and let it investigate the error itself. For the latter, two browser automation tools were used: the claude-in-chrome plugin and Playwright MCP.

Around 10 UI issues had to be fixed in total. Time taken: 90 minutes. An important finding: reading the browser console and fixing JavaScript errors works more reliably and faster with Playwright than with claude-in-chrome.

11. README.md as an onboarding guide

To wrap up, Claude Code generated a developer-focused README.md that documents setup, configuration, and architectural decisions as an entry point for new developers.

Create a user focused README.md as onboarding guide especially for new developers joining the TimeWizard project.

Timeline overview

Step Description Duration Mode
1 Generate CLAUDE.md ~5 min autonomous
2 Create migration plan ~15 min interactive
3 Backend migration ~75 min autonomous
4 Migration report ~10 min autonomous + review
5 Modernisation & best practices ~30 min series of prompts
6 Backend test ~20 min manual + prompt
7–8 AngularJS + JS upgrades ~30 min research → execute
9 Regenerate CLAUDE.md ~5 min autonomous
10 Frontend test & fixes ~90 min manual + agent
11 Generate README.md ~5 min autonomous
Total ~4–5 h < 0.5 person-day

Result

Key metrics

52,300 → 36,300 LOC (−31%) | 65 → 10 NuGet packages | Windows-only → cross-platform | Effort: <0.5 person-day vs. estimated 2–3 person-days manual

Before / after in detail

Metric Before After Delta
C# classes / files 151 / 165 158 / 172 +7
Entity / model classes 23 18 −5
ViewModel classes 55 63 +8
Web API controllers 12 11 −1
SignalR hubs 23 24 +1
NuGet dependencies (unique) 65 10 −55
Lines of code, backend (C#) ~20,300 ~12,800 −37%
Lines of code, frontend (JS + HTML) ~32,000 ~23,500 −27%
Total lines of code ~52,300 ~36,300 −31%

What we gained

The codebase shrank by 31% with identical functionality — mainly through the elimination of the proprietary libraries and the leaner patterns of ASP.NET Core. The application now runs platform-independently in Linux containers on our RKE2 cluster. Autofac was replaced with the built-in Microsoft DI container, email sending was migrated to Coravel mailables, and configuration uses .env files instead of Web.config transformations. The frontend security holes (jQuery XSS, AngularJS prototype pollution) are closed by the upgrades to current versions.

What deliberately stayed open

Not every piece of technical debt was addressed — that was a deliberate scope decision:

  • No automated tests. Validation continues to happen manually. For an application that probably won't see active feature work any more, the ROI for test coverage wasn't there.
  • AngularJS stays. The frontend was updated to 1.8.3, but not migrated to a modern framework. The high complexity from numerous additional components makes a fully automatic migration unrealistic.
  • No scheduler. The Windows service for due-date notifications was removed, but not yet replaced by Coravel Scheduler.

Challenges & lessons learned

Proprietary library dependencies

The biggest challenge was replacing the 13 internal Evanto.Common NuGet packages. These libraries were designed as a shared base for several applications and were correspondingly tightly woven — from the repository base class through authentication to the SignalR infrastructure. Claude Code had to identify the relevant parts, extract them into project-specific libraries, and adjust all references. The decision to dissolve these dependencies completely instead of migrating them along increased the initial effort, but eliminates a substantial maintenance burden long-term.

Lesson learned: proprietary library dependencies aren't a black box for a coding agent — provided the source is available. Naming the path to the source explicitly in the prompt was decisive.

Platform-specific PDF issues

The original application used Windows system fonts for PDFsharp/MigraDoc, which aren't available on macOS or in Linux containers. This issue only became visible during manual testing. The FontResolver was then a matter of a single prompt with the error message.

Lesson learned: platform-specific hurdles when moving from Windows .NET are hard to anticipate up front. A manual test run after the backend migration is essential.

Cascading JavaScript upgrades

AngularJS 1.4 → 1.8.3 and jQuery 2.1 → 3.7.1 across multiple major versions brought around 10 UI issues with them. Identifying and fixing these regressions was the most time-consuming manual share of the migration (90 minutes), but it was supported effectively by Playwright MCP.

Lesson learned: the three-stage research → plan → execute pattern is gold for multi-major upgrades. The agent identifies breaking changes before execution and plans accordingly.

Missing test coverage

No tests in the original project meant: fully manual validation after every step. In a project with existing test coverage, the agent could have run the tests automatically after every change and caught issues earlier.

Lesson learned: tests aren't only for humans — they're the most effective feedback loop for coding agents.

Conclusion & context

Effort and cost

The full migration was completed in less than half a person-day (see timeline above). Done purely manually, at least 2 to 3 person-days would have been realistic — conservatively estimated, since just understanding and rewriting the proprietary library dependencies would have required substantial analysis effort.

The API costs for using Claude Code came in at around €30–40 for the entire migration. Measured against the time saved — even on conservative numbers — an excellent ROI.

Transferable patterns

Three patterns turned out to be applicable across projects:

Research → plan → execute. Especially for upgrades across multiple major versions. The agent researches breaking changes, produces a concrete plan, and only executes after approval. That reduces the risk of bad decisions noticeably compared with a direct "just do it" prompt.

CLAUDE.md as a control instrument. Generated once at the start to convey the current state to the agent, and a second time after the migration to capture the new project state for all follow-up work.

Focused prompts instead of a mega-prompt. The modernisation (step 5) shows: a series of focused prompts — each with a clearly bounded topic — delivers better results than a single prompt that tries to do everything at once.

Limits of the approach

The migration also shows where agent-assisted modernisation currently hits its limits. The AngularJS frontend was deliberately not migrated to a modern framework — AngularJS's missing component architecture and the numerous additional components make a fully automatic migration unrealistic. Similarly, neither automated tests nor database migrations were introduced; both would have widened the scope substantially and reduced the time advantage.

Who is this approach suitable for?

Particularly for projects where the manual migration effort was estimated as too high until now, and where the migration kept getting pushed back — exactly as it was for TimeWizard. The combination of structured planning by the agent and targeted manual validation makes it possible to run extensive legacy migrations with reasonable effort.

The prerequisite: the developer needs to understand the target system and be able to evaluate the agent's results critically. The coding agent doesn't replace subject-matter competence — it accelerates the implementation.

References

NEWSLETTER

Four to six times a year, no marketing noise.

One pattern, one case, one recommendation. Signup with double opt-in, unsubscribe at any time.