Aggiornare un portale enterprise senza cambiarne il comportamento
Frontend da Angular 13 a 22 e backend da .NET 6 a 10, in produzione, senza cambiare il comportamento. Progetto di lavoro sotto NDA, raccontato senza codice.
- Ruolo
- Migrazione di frontend e backend con Claude Code, in un team di tre sviluppatori
- Periodo
- aprile → settembre 2026
- Stato
- In produzione
- Pubblicato
- Autore
- Elia Raguso
- Angular 22
- NgRx
- TypeScript 6
- .NET 10
- EF Core 10
- SQL Server
- Azure DevOps
In breve
È un portale interno con cui un grande gruppo gestisce progetti IT, budget e acquisti: contratti, milestone, ordini, firma dei verbali di consegna. Frontend Angular con NgRx, backend ASP.NET Core con EF Core e SQL Server, job pianificati su Azure.
Nel 2026 ho portato il frontend da Angular 13 a 22 e il backend da .NET 6 a 10. Entrambe le migrazioni sono in produzione. La richiesta era: nessuna funzione nuova e nessuna differenza visibile. La parte più lunga è stata dimostrare che il comportamento era rimasto lo stesso.
È un progetto di lavoro coperto da riservatezza: qui non ci sono codice, nomi o numeri interni, solo le decisioni e il metodo di verifica.
Il punto di partenza
Le due versioni di partenza erano fuori supporto da tempo. Il portale non ha una suite di test automatici, quindi non c'era una rete di sicurezza già pronta: ogni affermazione del tipo «funziona come prima» andava costruita apposta.
Le regole, scritte in un documento di stato nel repository prima di toccare il codice:
- la logica non cambia, si tocca solo quello che la migrazione impone;
- una fase alla volta, rivista prima di passare alla successiva;
- nessun commit automatico, committa sempre una persona;
- ogni eccezione a queste regole viene scritta, con il motivo.
Due eccezioni ci sono state, ognuna decisa esplicitamente e annotata con il motivo: chi rivede il branch deve sapere che il diff non è più solo migrazione.
Angular, da 13 a 21: riusare invece di rifare
Mesi prima esisteva già un branch che portava il frontend ad Angular 21 e toglieva Kendo UI, sostituito da Bootstrap 5 e ng-select. Non era mai andato in produzione, e nel frattempo lo sviluppo era andato avanti su Angular 13 con molte funzionalità nuove.
La domanda era se rifare i salti di versione da capo o riusare quel lavoro. Ho simulato il
merge in sola lettura (git merge-tree) per misurarne il costo prima di deciderlo: i
conflitti erano pochi rispetto ai file toccati, quasi tutti nei template. Rifare tutto avrebbe significato riscrivere da zero
una conversione già scritta e già provata. Ho scelto un merge a tre vie vero, che
conserva la storia di entrambi i rami.
Per i template in conflitto ho confrontato le due versioni dopo averle normalizzate,
togliendo la sintassi del control flow (*ngIf contro @if) e gli spazi. Nella maggior
parte dei casi la migrazione non aveva cambiato niente di sostanziale, solo la sintassi:
lì ho tenuto la versione in produzione e ho rimandato la conversione allo schematic
ufficiale. Ho fatto il merge a mano solo dove c'era contenuto reale.
Alcune modifiche del vecchio branch le ho rifiutate apposta, per esempio un footer dei totali disattivato su due tabelle: in produzione c'è, e il criterio era quello.
Angular 22: i default che cambiano senza errori
Il salto da 13 a 21 toccava molti file, ma gli errori erano di compilazione e quindi facili da trovare. Il salto a 22 tocca pochi file ma cambia tre comportamenti predefiniti a runtime: l'app compila, parte e si comporta in modo diverso, senza errori.
La change detection passa a OnPush per default. Nessun componente del portale la
dichiarava, quindi tutti dipendevano dal default. Un componente rimasto indietro smette di
aggiornare la schermata, senza un errore in console. La migration di ng update scrive la
vecchia strategia in modo esplicito su ogni componente; non mi sono fidato del risultato e
l'ho controllato con git grep, contando i componenti con e senza la dichiarazione:
nessuno era rimasto senza.
Le route figlie ereditano i data del padre. Il breadcrumb legge i data della route
foglia, e il guard dei permessi legge da lì i claim richiesti. Con il nuovo default una
pagina senza breadcrumb avrebbe ereditato quello del padre, e una route senza claim quelli
del padre: un cambio di comportamento sull'autorizzazione. Non esiste una migration
automatica per questo: ho rimesso a mano paramsInheritanceStrategy: 'emptyOnly'.
HttpClient passa da XMLHttpRequest a Fetch. Il portale scarica molti file come blob,
e uno usa un vecchio trucco sul tipo di risposta che con Fetch non è garantito. Ho
lasciato withXhr(), così il backend HTTP è identico a prima per costruzione.
.NET, da 6 a 10: una fase alla volta
Salto diretto, senza tappe intermedie: .NET 9 era già fuori supporto e .NET 8 lo sarà a novembre 2026, quindi una tappa sarebbe costata lo stesso lavoro senza dare niente.
L'ordine delle fasi segue un principio: tutto quello che si può fare ancora su .NET 6 si fa prima del cambio di runtime, perché con l'app funzionante ogni differenza ha una causa sola.
- Versioni centralizzate (Central Package Management), confrontando i pacchetti risolti prima e dopo: nessuna differenza.
- Il fix dei claim JWT anticipato. Da .NET 8 il gestore dei token ignora la mappa dei
claim che il portale svuotava all'avvio: senza intervento ogni endpoint protetto avrebbe
risposto 403, senza un errore nei log.
MapInboundClaims = falseesiste già sulla versione vecchia, quindi l'ho messo lì e verificato, compreso il caso negativo: un utente senza permesso deve continuare a ricevere 403. - I pacchetti fermi a vecchie versioni, provati eseguendoli. Che fossero netstandard non bastava a sapere che girassero. Ho scritto un progetto usa e getta, fuori dal repository, che usa le librerie come le usa il portale (PDF con campi firma, Excel, Microsoft Graph, serializzazione JSON) e gira su .NET 6 e .NET 10. Gli output sono stati confrontati byte per byte.
- AutoMapper tolto, non aggiornato. La versione in uso va in crash all'avvio da .NET 8 in poi. Le versioni successive o lasciano aperta una vulnerabilità nota, o richiedono una licenza commerciale. Ho sostituito i mapping con classi scritte a mano, seguendo una convenzione già usata in un altro progetto del team. Il rischio è che una proprietà dimenticata non dia errori ma un campo vuoto nel JSON: per questo ho prima catturato le risposte dell'API, poi le ho confrontate dopo il refactor.
- Una vulnerabilità critica chiusa. La libreria di ordinamento dinamico aveva una vulnerabilità nota raggiungibile dal campo di ordinamento delle liste. Aggiornata, e verificato che l'ordinamento venga ancora applicato davvero, in entrambe le direzioni.
Come si dimostra che non è cambiato niente
Una baseline catturata settimane prima non basta: nel frattempo il database di sviluppo cambia, e ogni differenza resta ambigua tra «colpa della migrazione» e «sono cambiati i dati». Per togliere la variabile dati ho avviato le due versioni insieme: l'ultimo commit su .NET 6 e quello su .NET 10, sulla stessa macchina.
L'esito: tutte le risposte identiche byte per byte, tranne gli export Excel, che differivano solo per un identificativo interno al file che la libreria genera a caso a ogni export. Ho aperto i file per controllarlo: fogli e stili identici.
Il confronto ha trovato anche due differenze reali, entrambe del framework e non del codice:
- Le rotte della SPA rispondevano 404 invece di 200. Il corpo era giusto e l'utente non se ne sarebbe accorto, ma in produzione ogni navigazione sarebbe finita in Application Insights come errore: falsi allarmi a coprire quelli veri. Ho ripristinato lo status di prima con una riga, poi ho verificato che le chiamate API senza token rispondano ancora 401 e che i file mancanti rispondano ancora 404.
- Un errore di autenticazione scritto nei log a ogni richiesta. Funzionalmente innocuo: la richiesta va a buon fine e l'autenticazione si comporta come prima. Ho deciso di lasciarlo così, scrivendo quando andrà ripreso e qual è la leva per silenziarlo.
Come ho lavorato con Claude Code
Ho fatto la migrazione con Claude Code. La divisione del lavoro era questa:
- le decisioni sono mie: perimetro, ordine delle fasi, cosa accettare e cosa rifiutare, le due eccezioni alla regola «la logica non cambia»;
- ogni fase è stata rivista prima di passare alla successiva, e nessun commit è partito in automatico;
- build, avvio dell'app e test funzionali li lanciavo io, sull'interfaccia vera e, per le scritture, controllando direttamente i dati sul database;
- il documento di stato era aggiornato e committato a ogni sessione: cosa è fatto, cosa è verificato e come, cosa resta. È anche quello che ha permesso di riprendere il lavoro dopo giorni senza perdere il filo.
La regola più utile è stata non accettare affermazioni non verificate, né mie né del modello. Quando nel piano c'era scritto che una libreria «dovrebbe funzionare», la verifica era farla girare. Più di una volta il piano si è rivelato sbagliato, e la correzione è finita nel documento di stato.
Risultato
Frontend su Angular 22 e TypeScript 6, backend su .NET 10, in produzione. Per chi usa il portale non è cambiato niente, che era l'obiettivo.