Case Study: Migration großer C#- und TypeScript-Codebasen ohne Entwicklungsstopp
Erfahrungsbericht aus mehreren Enterprise-Projekten
Diese Case Study beschreibt die kontrollierte Migration großer C#- und TypeScript-Codebasen auf neue Framework- und Sprachversionen, ohne die laufende Entwicklung zu stoppen.
In mehreren Projekten habe ich bestehende Anwendungen migriert – teilweise von kleineren Lösungen mit 10 Projekten, teilweise von großen Monolithen mit über 200 Projekten.
Die zentrale Anforderung war dabei stets:
- Keine Unterbrechung der laufenden Entwicklung
- Kein Big-Bang-Rewrite
- Minimierung von Projektrisiken
- Kontrollierte, nachvollziehbare Migration
Die Migrationen habe ich bislang eigenverantwortlich durchgeführt – mit einer klar strukturierten, phasenweisen Vorgehensweise.
Grundprinzip: Funktionale Identität vor Modernisierung
Die wichtigste Entscheidung in meinen Migrationen war:
In der ersten Phase wird nicht refaktoriert.
Es wird ausschließlich funktionale Identität hergestellt.
Warum?
Wenn während der Migration gleichzeitig Refactoring oder Logikänderungen stattfinden, ist später nicht mehr nachvollziehbar, ob ein unerwünschter Nebeneffekt durch:
- eine fehlerhafte Übertragung
- eine Framework-Änderung
- oder eine neue Logik nach dem Refactoring
entstanden ist.
Deshalb ist die erste Phase immer eine 1:1-Übertragung der bestehenden Funktionalität.
Architektur der Migration
1️⃣ Isolierte Migrations-Codebasis
Die Migration erfolgt in einer separaten Codebasis:
- als Draft-Branch (z. B.
migration/net10) - oder als separates Repository
Das bestehende Team-Repository bleibt unverändert und produktiv nutzbar.
Während ich migriere, arbeitet das Team weiterhin im ursprünglichen Repository.
2️⃣ Phase 1 – Übertragung 1:1
Vorgehen:
- Neues Projekt auf Basis des aktuellen Templates erstellen
- DTOs, POCOs und Domänenobjekte 1:1 kopieren
- Schrittweise so lange übernehmen, bis das neue Projekt kompiliert
Breaking Changes werden:
- temporär auskommentiert
- mit
TODOdokumentiert - ggf. durch Mocks oder Platzhalter ersetzt
In dieser Phase gilt:
- Keine strukturellen Änderungen
- Keine Modernisierung
- Keine Verzeichnisumstrukturierung
Der Compiler dient als Migrationswerkzeug.
Ziel: Kompilierbarkeit bei funktionaler Gleichheit.
3️⃣ Laufende Synchronisierung mit dem Team
Während der Migration:
- arbeitet das Team im ursprünglichen Repository weiter
- ich synchronisiere regelmäßig deren Änderungen in die Migrations-Codebasis
Dadurch entsteht kein großer Merge-Konflikt am Ende.
Der finale Delta bleibt minimal.
Die kontinuierliche Synchronisierung ist entscheidend, um die Migration ohne Entwicklungsstopp zu ermöglichen.
4️⃣ Phase 2 – Konsolidierung & Auflösung der TODOs
Erst nachdem alle relevanten Objekte übertragen wurden:
- werden
TODO-Markierungen systematisch beseitigt - werden Breaking Changes sauber implementiert
- wird Code an neue Sprach- oder Framework-Version angepasst
Neue Sprachfeatures (z. B. strict mode in TypeScript oder neue C#-Features) werden nicht erzwungen, solange sie massive Compile-Fehler verursachen würden.
Stattdessen werden sie dokumentiert und später kontrolliert aktiviert.
5️⃣ Refactoring & Struktur-Anpassung
Erst nach stabiler Kompilierung:
- Anpassung der Verzeichnisstruktur
- Refactoring
- kosmetische Verbesserungen
- Modernisierung von APIs
- Reduktion technischer Schuld
Damit bleibt klar unterscheidbar:
- Was war reine Migration?
- Was ist bewusste Verbesserung?
6️⃣ API-Contracts & Datenbank
In der beschriebenen Code-Migration:
- bleiben API-Contracts unverändert
- bleiben Datenbank-Entities unverändert
- werden weder CI/CD-Pipeline noch Datenbank-Struktur migriert
Diese Themen werden als separate Migration betrachtet.
Dadurch bleibt die Migration technisch kontrollierbar und klar abgegrenzt.
7️⃣ Validierung: Tests & Dry-Run-Strategie
Zur Absicherung der Migration nutze ich:
- Unit-Tests
- Integrationstests
- gezielte manuelle Tests
Ein wichtiger Bestandteil ist die Dry-Run-Strategie.
Dabei wird das System um spezielle Testfunktionen erweitert:
- Erzwingen bestimmter Systemparameter, z.B. das aktuelle Datum in der Zukunft
- Simulation von Edge-Cases
- Erweiterte UI-Funktionen für Tester
- Ausführung kritischer Prozesse ohne Persistierung in der Datenbank
So lassen sich Grenzszenarien testen, ohne produktive Daten zu verändern.
Das ermöglicht:
- realitätsnahe Tests
- Validierung komplexer Prozesse
- Minimierung von Risiko vor Umschaltung
8️⃣ Finalisierung & Umschaltung
Wenn die Migration abgeschlossen ist:
- Änderungsstopp im ursprünglichen Repository
- Letzte Synchronisierung
- Finaler Build und Tests
- Atomare Umschaltung auf die neue Codebasis (meist übers Wochenende)
Am Montag arbeiten alle Entwickler mit dem neuen Repository.
Falls Probleme auftreten:
- wird temporär wieder das alte Repository genutzt
- die Finalisierung wird am nächsten Wochenende wiederholt
Diese Vorgehensweise ermöglicht faktisch eine kontrollierte Zero-Downtime-Migration auf Code-Ebene, da der laufende Entwicklungsprozess bis zum finalen Cut nicht gestoppt wird.
Optional: Paralleler Betrieb
Wenn Infrastruktur vorhanden ist, kann die neue Version:
- parallel betrieben
- auf separater Umgebung validiert
- schrittweise aktiviert
In vielen Fällen erfolgte die Bereitstellung zunächst im Dev-Umfeld und wurde nach erfolgreicher Validierung übernommen.
Ergebnis meiner Migrationen
Mit dieser Strategie konnte ich:
- Codebasen mit 10 bis über 200 Projekten migrieren
- parallele Weiterentwicklung im Team ermöglichen
- Risiken kontrollieren
- Migration und Modernisierung sauber trennen
- Umschaltung atomar durchführen
- Rückfalloptionen offenhalten
Wichtiger Hinweis
Die hier beschriebenen Schritte sind kein allgemeingültiges Kochrezept.
Jede Migration:
- hat eigene Abhängigkeiten
- eigene Risiken
- eigene Architektur-Besonderheiten
Eine erfolgreiche Migration erfordert immer:
- individuelle Analyse
- klare Zieldefinition
- strukturierte Planung
Gerne unterstütze ich bei der Bewertung und Planung komplexer Code-Migrationen.