Quanto costa la migrazione da .NET Framework a .NET 10?
La scelta si fa su tre numeri: gli sviluppatori che ci lavorano (19.376 euro l'anno di risparmio con tre), gli anni di vita richiesti e il costo di un fermo.

Un distributore di componenti idraulici in provincia di Bergamo, 140 dipendenti e 48 milioni di fatturato, prende gli ordini di 900 rivenditori attraverso un portale web. L'hanno scritto nel 2014 in ASP.NET MVC 5 su .NET Framework 4.8: quattordici progetti, circa 310.000 righe di C#, Entity Framework 6, un servizio WCF verso il gestionale, un servizio di Windows che sincronizza i listini ogni notte e i DDT stampati con Crystal Reports. Gira su due macchine Windows Server 2016 con IIS. Lo mantengono tre sviluppatori: uno interno e due esterni.
Il responsabile IT ha letto in tre articoli diversi che .NET Framework è finito, che Windows Server 2016 esce dal supporto a gennaio 2027 e che le librerie nuove non lo supportano più. Il titolare ha una domanda sola: quanto costa la migrazione da .NET Framework a .NET 10 e se si può rimandare di qualche anno.
La risposta onesta ha due metà. La prima: .NET Framework 4.8 non è "finito". Microsoft lo supporta finché è supportato il Windows su cui gira, quindi il portale non smette di funzionare il giorno in cui scade qualcosa. La seconda: ogni anno di attesa ha un costo che si vede soltanto se lo si misura, e nel caso di Bergamo vale 29.580 euro. Con questa cifra e con quattro ipotesi scritte una per una si arriva a un risultato che sorprende chi si aspetta il solito consiglio di migrare tutto: conviene migrare un pezzo per volta da 98.700 euro, ma il punto di pareggio rispetto a restare dov'è sta a 5,9 anni, e in alcuni casi non arriva mai.
Vedrai che cosa significa davvero "fine del supporto" nel 2026, come si fa l'inventario di una soluzione in una settimana, che cosa passa a .NET 10 senza toccare una riga e che cosa no, le quattro strade con i loro costi, il conto a uno, tre, cinque, otto e dieci anni, il codice per migrare un'area per volta con un proxy e per far convivere i due mondi, i test che proteggono i calcoli, le differenze di comportamento che fanno sbagliare i totali, il ruolo dell'intelligenza artificiale e gli errori che si ripetono.
Una premessa. Scrivo software dal 1999 e faccio anche questo lavoro, quindi so da che parte sta il mio interesse. Il caso è composto, le ipotesi sono dichiarate e nel risultato c'è anche la via in cui non spendi niente. Questo articolo fa parte di una serie sulla modernizzazione del software legacy: se vuoi il quadro generale, parti da lì.
.NET Framework è davvero finito nel 2026?
No. Ma la frase "è supportato" nasconde tre date diverse, e confonderle porta a decisioni sbagliate in entrambe le direzioni.
Che cosa dice Microsoft
.NET Framework 4.8 e 4.8.1 sono componenti di Windows. Il loro supporto segue il ciclo di vita del sistema operativo che li contiene. Non c'è una data di fine propria. Finché esiste una versione di Windows supportata che li include, ricevono correzioni di sicurezza. Nessuna nuova funzione, però: .NET Framework è in manutenzione e basta.
Il lato opposto è .NET moderno. .NET 10 è uscito a novembre 2025 ed è una versione a supporto lungo (LTS): tre anni, fino a novembre 2028. Le versioni 8 e 9 finiscono entrambe il 10 novembre 2026, cioè fra pochi giorni. Se oggi qualcuno ti propone di migrare a .NET 8 o a .NET 9, la proposta è già vecchia.
Le date che contano davvero
Per il portale di Bergamo non importa quando muore un runtime. Importa quando muore ciò che gli sta intorno.
- Windows Server 2016: il supporto esteso termina a gennaio 2027. Da quel momento niente correzioni di sicurezza, salvo pagare gli aggiornamenti estesi.
- SQL Server 2016: il supporto è terminato a luglio 2026. Se il database sotto è questo, il problema è già qui.
- Windows Server 2019: supporto fino a gennaio 2029. Windows Server 2022 fino a ottobre 2031. Su questi .NET Framework 4.8 continua a ricevere correzioni.
- Librerie di terzi: molte nuove versioni escono soltanto per .NET moderno. Quelle vecchie restano come sono, con le vulnerabilità che nel frattempo si scoprono.
Quindi la scadenza vera non è il runtime. È il sistema operativo e il database, che comunque dovrai cambiare. E il momento in cui cambi la macchina è il momento più economico per chiederti se il programma deve restare sullo stesso runtime.
Che cosa ti costa restare
Restare su .NET Framework 4.8 su un Windows supportato non è un reato né un rischio immediato. Costa in tre modi, tutti misurabili.
- Attrito di sviluppo. Compilazioni lente, pacchetti che non si aggiornano, strumenti che non funzionano con i vecchi file di progetto, rilasci a mano. Nel caso di Bergamo sono 18 giornate all'anno per sviluppatore.
- Infrastruttura. Windows con licenza, IIS, macchine virtuali grandi. Su .NET 10 in container Linux lo stesso carico costa meno della metà.
- Persone. Chi sa scrivere C# moderno preferisce lavorare su .NET moderno. Trovare uno sviluppatore per un MVC 5 del 2014 costa di più ogni anno.
Se conosci già la storia, questo è lo stesso schema che descrivo nel costo vero del software legacy: la voce più grossa non è mai quella che compare in fattura.
Che cosa c'è dentro la tua soluzione .NET Framework?
Prima di scegliere una strada serve un inventario. Si fa in due o tre giornate, e si fa leggendo i file, non intervistando le persone. L'obiettivo è una tabella con una riga per progetto e cinque colonne: tipo, dipendenze, cosa usa che non esiste in .NET moderno, chi lo usa, quanto vale.
Tipi di progetto e che cosa succede a ciascuno
Nel portale di Bergamo i quattordici progetti si dividono così.
- Librerie di classi (5 progetti): modelli, accesso ai dati, regole di prezzo, utilità, integrazione col gestionale. Sono il caso facile. Se non usano API specifiche di Windows o di
System.Web, passano a .NET con poche modifiche. - Applicazione web MVC 5 (1 progetto): il cuore del lavoro. Controller, viste Razor, filtri, bundle di script. Si porta ad ASP.NET Core, ma non in automatico:
Global.asax,Web.config, i moduli HTTP e i filtri hanno equivalenti diversi. - API Web API 2 (1 progetto): usata dall'app per i rappresentanti. Stessa storia dell'MVC, più la differenza fra il vecchio
HttpResponseMessagee il nuovo modello di risultato. - Servizio WCF (1 progetto): è l'interfaccia verso il gestionale. WCF non esiste in .NET moderno. Esiste CoreWCF, una libreria comunitaria sostenuta da Microsoft, che accetta gli stessi contratti.
- Servizio di Windows (1 progetto): la sincronizzazione notturna. Diventa un Worker Service, e il codice di dominio resta quasi identico.
- Report Crystal (3 progetti con 41 report): il punto più doloroso. Il componente per .NET moderno non c'è nella forma che serve.
- Test (2 progetti): 212 test, di cui 160 sulle regole di prezzo. Valgono oro.
Le tre domande di ogni progetto
Per ogni riga dell'inventario rispondi a tre domande.
- Usa API che esistono soltanto in .NET Framework?
System.Web,System.Configuration,System.Drawingsu Windows,AppDomainper i plugin,Remoting,BinaryFormatter. Si cerca con una ricerca di testo e con lo strumento di analisi di Microsoft. - Dipende da pacchetti NuGet che non hanno una versione per .NET moderno? Se sì, quale sostituto esiste e quanto costa cambiarlo.
- Chi lo chiama? Un progetto chiamato da quattro altri si migra prima e si migra per primo come libreria condivisa. Un progetto in cima, che non chiama nessuno, si migra per ultimo.
Gli strumenti che ci sono già
Microsoft offre l'Upgrade Assistant, un'estensione e uno strumento da riga di comando che analizza la soluzione e produce un rapporto: quali progetti si convertono, quali pacchetti vanno aggiornati, quali API non esistono più. Lo usi in sola lettura, per l'analisi. Come strumento di conversione fa bene il lavoro meccanico (il file di progetto, i riferimenti) e lascia a te il resto. Il Portability Analyzer storico è stato assorbito da questo.
Sul portale di Bergamo l'analisi ha trovato 1.940 punti da toccare. Il 61% sono riferimenti a System.Web nel solo progetto MVC. Altri 280 sono in due librerie che usano HttpContext.Current per leggere l'utente, una cattiva abitudine che trovi in quasi ogni soluzione di quegli anni.
Che cosa passa senza toccare niente e che cosa no
Una regola pratica, per orientarsi prima dell'analisi.
- Passa quasi gratis: librerie di calcolo, modelli, logica di dominio, accesso ai dati con Entity Framework 6 (che gira anche su .NET moderno dalla versione 6.3), Dapper, la maggior parte dei pacchetti con una versione
netstandard2.0. - Passa con lavoro: ASP.NET MVC e Web API, la configurazione, l'autenticazione, i servizi di Windows, WinForms e WPF (che si portano bene, ma restano solo per Windows; se ti interessa la parte desktop, la guida a WPF spiega la base su cui ragionare).
- Non passa: WCF lato server (si sostituisce con CoreWCF o con API), Remoting, Web Forms (che ha un articolo a sé: migrazione da ASP.NET WebForms a Core), i report Crystal nel formato attuale, ogni componente COM a 32 bit senza versione a 64.
Quali strade esistono e quanto costano?
Le strade vere sono quattro. Le altre sono varianti. Il costo giornaliero è di 420 euro e le giornate sono quelle che prevedo per il portale di Bergamo dopo l'inventario. Il modello completo ha un solo costo di gestione annuo per ogni strada, e lo trovi dopo l'elenco.
A. Restare su .NET Framework 4.8, in regola
Trentuno giornate, 13.020 euro. Non si cambia il runtime. Si mettono a posto le cose che rendono il portale rischioso: sorgenti nel controllo di versione con una pipeline che compila e rilascia da sola, un ambiente di sviluppo ricostruibile in un'ora, i pacchetti NuGet portati all'ultima versione compatibile, la macchina spostata da Windows Server 2016 a un sistema con supporto, il database aggiornato. Si scrivono i primi test sulle regole che oggi non ne hanno.
Non riduce il costo di gestione annuo: l'attrito resta e l'infrastruttura Windows anche. Riduce il rischio, e questo non si vede in euro finché il rischio non si realizza.
B. Aggiornare tutto in blocco
Il piano dice 190 giornate. Sommo il 45% di sforamento, che è quanto vedo sulle migrazioni in blocco senza una rete di test, e arrivo a 276 giornate: 115.710 euro in dodici mesi. Si crea un ramo, si converte la soluzione intera e la si porta in produzione in un'unica uscita. Nel frattempo il portale vecchio continua a essere modificato dai rivenditori, dagli ordini e dal reparto commerciale, e tutto quello che cambia sul ramo vecchio va rifatto sul nuovo.
Il 45% è la mia ipotesi più discutibile, e l'ho tenuta in vista: se la stima è esatta, senza sforamento questa strada costa 79.800 euro e pareggia con la A in 4,4 anni; con il 60% di sforamento costa 127.680 euro e pareggia in 6,9. Cambia la graduatoria, ed è per questo che il modello lo dichiara.
C. Migrare un pezzo per volta
Duecentotrentacinque giornate, 98.700 euro in diciotto mesi. Si mette davanti al portale un proxy in .NET 10, si porta una sola area alla volta (il catalogo, poi il carrello, poi gli ordini) e si cambia una regola del proxy per spostare quell'area dal vecchio al nuovo. Gli utenti non vedono mai un taglio. Sotto, un solo database SQL Server. È l'applicazione del pattern strangler fig al caso di una soluzione .NET Framework.
Costa meno della B perché non ha sforamento fisso: ogni area esce quando è pronta, si misura e si corregge la stima della successiva. E perché parte dai test.
D. Riscrivere
Cinquecentosessanta giornate, 235.200 euro in trenta mesi. Si ricomincia da zero con un'architettura nuova. Ha senso quando il vecchio è così intrecciato che non si può dividere o quando cambia il modello di business. Per il portale di Bergamo non c'è motivo, e il confronto con la C lo mostra: stesso risultato finale, più del doppio di spesa. Il ragionamento completo sta in riscrivere o rifattorizzare il software legacy.
Il modello in cinque ipotesi
- Il costo giornaliero è di 420 euro, sia per il lavoro di migrazione sia per quello che si perde in attrito.
- L'attrito vale 54 giornate l'anno (tre sviluppatori, 18 giornate ciascuno) cioè 22.680 euro. Sono compilazioni lente, pacchetti bloccati, rilasci a mano, strumenti che non funzionano.
- L'infrastruttura costa 6.900 euro l'anno su Windows, e 3.400 su .NET 10 in container Linux.
- Dopo la migrazione resta il 30% dell'attrito: non sparisce, si riduce.
- Restando su .NET Framework, il costo annuo è 29.580 euro (22.680 più 6.900). Dopo la migrazione scende a 10.204 euro (6.804 più 3.400). Il risparmio è di 19.376 euro l'anno.
Sono numeri piccoli di proposito. Non c'è un fermo da centomila euro e non ci sono penali. Se nel tuo caso un fermo del portale costa tanto, il modello cambia, e lo vedi più avanti.
Dopo quanti anni conviene migrare da .NET Framework a .NET 10?
Il costo cumulato è la somma di quello che spendi per cambiare e di quello che continui a pagare ogni anno. Le quattro strade, più "non fare niente", a distanza di uno, tre, cinque, otto e dieci anni.
| Anni | Non fare niente | A. Restare in regola | B. In blocco | C. Un pezzo per volta | D. Riscrivere |
|---|---|---|---|---|---|
| 1 | 29.580 € | 42.600 € | 145.290 € | 128.280 € | 264.780 € |
| 3 | 88.740 € | 101.760 € | 165.698 € | 158.376 € | 314.252 € |
| 5 | 147.900 € | 160.920 € | 186.106 € | 178.784 € | 334.660 € |
| 8 | 236.640 € | 249.660 € | 216.718 € | 209.396 € | 365.272 € |
| 10 | 295.800 € | 308.820 € | 237.126 € | 229.804 € | 385.680 € |

Che cosa dicono i numeri
- Sotto i cinque anni conviene non migrare. Chi resta su 4.8 spende meno in euro: a tre anni 88.740 contro 158.376. Qui il consiglio più onesto è non spendere.
- Il pareggio sta fra 5,9 e 6,6 anni. La strada C pareggia con la A a 5,9 anni e con il non fare niente a 6,6. La B pareggia con la A a 6,3. Oltre quel punto ogni anno aggiunge 19.376 euro di vantaggio.
- A otto anni la C costa 27.244 euro meno di restare fermi (209.396 contro 236.640), e a dieci 65.996 euro meno.
- La riscrittura non pareggia mai con la C. A regime costano uguale, ma la D parte da 136.500 euro più in alto.
- La messa in regola (A) non si ripaga in euro. Costa 13.020 euro e non cambia il costo annuo. Si giustifica con il rischio: se tra tre anni il server si rompe e nessuno sa ricostruirlo, quei 13.020 euro sono il prezzo più basso che pagherai.
Il messaggio per il titolare di Bergamo è questo. Se pensa di tenere il portale cinque anni o meno, spende i 13.020 euro della A e va avanti. Se pensa di averlo ancora fra dieci, la C è la scelta, e conviene cominciare presto, perché il risparmio di 19.376 euro l'anno parte soltanto quando la prima area è in produzione.
Che cosa sposta il pareggio
Il pareggio fra C e A, nel caso base, è di 5,9 anni. Cambiando una sola ipotesi per volta:
- Gli sviluppatori scendono da tre a uno: l'attrito vale 18 giornate e il pareggio sale a 11,2 anni. Con un solo sviluppatore quasi mai conviene.
- Gli sviluppatori salgono da tre a sei: l'attrito è di 108 giornate e il pareggio scende a 3,9 anni.
- Dopo la migrazione resta il 50% dell'attrito invece del 30%: il pareggio sale a 7,3 anni.
- La strada C costa 300 giornate invece di 235: pareggio a 7,3 anni. Con 180 giornate scende a 4,7.
- L'infrastruttura non costa meno (6.900 anche dopo): pareggio a 6,9 anni.
- Un fermo o un incidente che costa 20.000 euro l'anno a chi resta: pareggio a 3,7 anni.
Due numeri pesano più di tutti: quanti sviluppatori lavorano sul portale e quanto costa un fermo. Sono anche i due che puoi misurare senza un consulente.
Come si migra .NET Framework senza fermare l'azienda?
La strada C funziona in quattro mosse. Prima si prepara la soluzione, poi si porta ciò che è condiviso, poi si mette il proxy davanti, infine si sposta un'area per volta.
Mossa 1: portare le librerie condivise a più target
Le librerie di classi non si convertono: si compilano per entrambi i mondi. Un file di progetto nel formato moderno può avere più target framework contemporaneamente. Lo stesso codice produce una DLL per .NET Framework 4.8, che il portale vecchio continua a usare, e una per .NET 10, che userà quello nuovo.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net48;net10.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Nullable>disable</Nullable>
<RootNamespace>Idraulica.Prezzi</RootNamespace>
</PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net48'">
<Reference Include="System.Configuration" />
</ItemGroup>
</Project>Due dettagli che risparmiano giornate. Il primo: si tiene Nullable spento nella libreria condivisa, perché accenderlo in una base di codice del 2014 produce migliaia di avvisi che nascondono quelli veri. Si accende progetto per progetto, a migrazione finita. Il secondo: per le impostazioni comuni (versione del linguaggio, regole di analisi, pacchetti di sicurezza) si usa un solo file alla radice, Directory.Build.props, invece di ripeterle in quattordici progetti.
<Project>
<PropertyGroup>
<LangVersion>latest</LangVersion>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<ImplicitUsings>disable</ImplicitUsings>
<Deterministic>true</Deterministic>
</PropertyGroup>
</Project>Il formato moderno dei file di progetto (quello "SDK-style") serve anche per i pacchetti: i riferimenti NuGet vivono nel file con PackageReference, non più in un packages.config a parte. Si può convertire anche un progetto che resta su .NET Framework. È un passaggio sicuro e a costo quasi zero: lo fai per primo, sul portale vecchio, prima ancora di decidere la strada.
Se la libreria usa System.Web per leggere l'utente o la sessione, la riga HttpContext.Current non compila su .NET 10. Si risolve con un'interfaccia, IContestoUtente, che ha due implementazioni: una nel portale vecchio e una in quello nuovo. Il codice di dominio parla all'interfaccia e non sa dove gira.
Mossa 2: i test sul comportamento, prima di toccare il resto
Le 212 prove esistenti non bastano: coprono i prezzi e non le stampe, le sincronizzazioni, gli arrotondamenti dei totali di fattura. Prima di spostare qualsiasi cosa si scrivono i test di caratterizzazione. Un test di caratterizzazione non dice che cosa il codice dovrebbe fare: registra che cosa fa oggi. Si prendono 400 casi reali di ordini passati, si fa calcolare al sistema vecchio e si salva il risultato in un file. Poi lo stesso file si fa verificare al sistema nuovo.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using Xunit;
namespace Idraulica.Prezzi.Test
{
public static class CalcolatorePrezzi
{
public static decimal Calcola(decimal listino, int quantita)
{
decimal sconto = quantita >= 100 ? 0.12m : quantita >= 20 ? 0.06m : 0m;
return Math.Round(listino * quantita * (1 - sconto), 2, MidpointRounding.AwayFromZero);
}
}
public static class CasiStorici
{
public static IEnumerable<object[]> Righe()
{
foreach (string riga in File.ReadLines("casi-storici.csv").Skip(1))
{
string[] c = riga.Split(';');
yield return new object[]
{
decimal.Parse(c[0], CultureInfo.InvariantCulture),
int.Parse(c[1], CultureInfo.InvariantCulture),
decimal.Parse(c[2], CultureInfo.InvariantCulture)
};
}
}
}
public class CalcoloPrezzoTests
{
[Theory]
[MemberData(nameof(CasiStorici.Righe), MemberType = typeof(CasiStorici))]
public void Il_prezzo_e_identico_a_quello_del_sistema_vecchio(decimal listino, int quantita, decimal atteso)
{
Assert.Equal(atteso, CalcolatorePrezzi.Calcola(listino, quantita));
}
}
}Il progetto di test usa anche lui net48;net10.0 e lo stesso file casi-storici.csv, copiato nella cartella di uscita. Lo stesso test gira due volte: sul vecchio e sul nuovo. Se i risultati coincidono, si passa. Se non coincidono, si ha un elenco preciso di che cosa è cambiato, prima che lo scopra un rivenditore.
Il file dei casi lo si produce una volta sola, con il sistema vecchio, e non si ritocca più. Se un caso cambia perché la regola è sbagliata, lo si decide con chi conosce l'azienda e lo si scrive in un registro, non lo si corregge in silenzio.
Mossa 3: il proxy davanti al portale vecchio
Ora il pezzo che fa la differenza. Si crea un'applicazione ASP.NET Core su .NET 10 e la si mette davanti a quella vecchia. Gli utenti puntano sempre allo stesso indirizzo. Tutto ciò che il nuovo non conosce, il nuovo lo inoltra al vecchio. Lo fa YARP, il reverse proxy che Microsoft pubblica per .NET.

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddHttpForwarder();
var app = builder.Build();
app.UseStaticFiles();
app.UseRouting();
// Le aree già migrate rispondono qui.
app.MapControllerRoute(
name: "catalogo",
pattern: "catalogo/{action=Index}/{id?}",
defaults: new { controller = "Catalogo" });
// Tutto il resto va al portale vecchio, con priorità minima.
string vecchio = app.Configuration["PortaleVecchio:Url"]!;
app.MapForwarder("/{**catch-all}", vecchio)
.Add(static endpoint => ((RouteEndpointBuilder)endpoint).Order = int.MaxValue);
app.Run();La riga che conta è l'ultima: una rotta "prendi tutto" con l'ordine più basso possibile. Le rotte del nuovo vincono, le altre passano al vecchio. Per spostare l'area del carrello basta aggiungere una rotta e togliere il traffico dal vecchio. Si può fare in un pomeriggio, e si può annullare con una riga.
Mossa 4: sessione e autenticazione condivise
Il punto in cui i progetti si bloccano è l'utente. Se l'utente entra dal vecchio e passa al nuovo, deve restare collegato e con lo stesso carrello. Microsoft pubblica per questo una libreria, Microsoft.AspNetCore.SystemWebAdapters: nel nuovo espone HttpContext.Session e l'identità leggendoli dal vecchio attraverso una chiamata protetta da una chiave condivisa. Nel vecchio si aggiunge il pacchetto corrispondente e un endpoint.
Alcune cose da sapere prima di cominciare.
- La chiave condivisa è un segreto: sta in un gestore di segreti, non in un file di configurazione nel repository.
- La sessione si serializza fra i due mondi, quindi gli oggetti messi in sessione devono essere semplici. Quelli che contengono riferimenti complessi vanno ridotti a identificativi. Sulle 38 chiavi di sessione del portale di Bergamo, 6 contenevano oggetti interi.
- Il costo del "ponte" è una chiamata in più per richiesta nelle aree nuove che leggono la sessione. Con il carico di un portale B2B è trascurabile, ma va misurato.
- Alla fine, quando l'ultima area è passata, si toglie il ponte e il vecchio si spegne.
Un'alternativa più pulita, quando si può, è eliminare la sessione condivisa: se il carrello vive nel database e non nella sessione, il nuovo e il vecchio leggono la stessa tabella e non devono scambiarsi niente. È il primo lavoro che farei in assoluto, perché semplifica tutto il resto.
L'ordine delle aree
Si parte da dove il rischio è basso e il guadagno si vede.
- Una pagina di sola lettura (il catalogo, le schede prodotto). Se qualcosa va storto, nessun ordine si perde.
- Le aree che cambiano spesso (il carrello, le promozioni), perché lì si recupera subito l'attrito.
- Gli ordini e i pagamenti, con i test di caratterizzazione completi.
- L'area amministrativa, che ha pochi utenti e molta logica.
- Le stampe e i report, per ultimi, perché dipendono da Crystal.
Ogni area esce da sola, con la sua prova in produzione per una settimana. Nel piano di Bergamo sono sei aree in diciotto mesi, circa tre mesi per area, compreso il tempo in cui l'area gira su entrambi.
Che cosa si cambia nel codice quando passi a .NET 10?
La parte meccanica la fa lo strumento. La parte che richiede un giudizio è un elenco di differenze di comportamento, e conviene conoscerle prima.
Web.config, Global.asax e i moduli HTTP
Non esistono più. La configurazione passa a appsettings.json e alle variabili d'ambiente, letta con IConfiguration. Le sezioni personalizzate del Web.config si trasformano in classi di opzioni. Gli eventi di Global.asax (avvio, errore, inizio richiesta) diventano codice in Program.cs e middleware. Un modulo HTTP è un middleware. Un filtro di azione resta un filtro, con un'interfaccia diversa.
La regola: non tradurre riga per riga. Elenca che cosa fa ogni modulo, poi decidi se serve ancora. Nel portale di Bergamo, sette moduli su undici erano rimasti lì da quando c'era Windows 2008.
Entity Framework 6 ed Entity Framework Core
Entity Framework 6 gira su .NET moderno dalla versione 6.3. Quindi il livello dati non si riscrive il primo giorno e puoi tenerlo mentre sposti il resto. Il passaggio a Entity Framework Core è un secondo progetto, con un suo valore: query più veloci, migliore supporto di SQL Server, caricamento esplicito.
Le differenze che fanno danni sono tre.
- Niente file .edmx. Il modello grafico non esiste in Core. Si genera il codice dal database una volta e poi si mantiene a mano.
- Il caricamento pigro non è attivo di default. Il codice che leggeva
ordine.Righesenza accorgersi che partiva una query ora trova una lista vuota. Si cercano le proprietà di navigazione e si decide: caricamento esplicito conIncludeo proxy di caricamento pigro. - Alcune query LINQ si comportano diversamente. Il raggruppamento, le sottoquery e le conversioni di stringhe a volte producono SQL diverso. I test di caratterizzazione prendono questi casi.
Per confrontare i due approcci in generale, trovi la spiegazione in Entity Framework Core per accedere ai dati.
WCF diventa CoreWCF
Il servizio verso il gestionale espone contratti WCF, con [ServiceContract] e [OperationContract], a client che non puoi toccare. CoreWCF accetta gli stessi contratti e lo stesso formato SOAP, e il client non si accorge del cambio. È la scelta giusta quando la controparte è un software di terzi. Quando invece puoi modificare anche il client, si sostituisce con un'API HTTP con JSON: meno codice e più strumenti.
Il servizio di Windows diventa un Worker Service
La sincronizzazione notturna dei listini è un servizio di Windows. In .NET moderno si scrive come servizio in background con host generico e lo si installa ancora come servizio di Windows, oppure lo si esegue in un container.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddWindowsService(opzioni => opzioni.ServiceName = "SincronizzazioneListini");
builder.Services.AddHostedService<SincronizzatoreListini>();
builder.Build().Run();
public sealed class SincronizzatoreListini : BackgroundService
{
private readonly ILogger<SincronizzatoreListini> _log;
public SincronizzatoreListini(ILogger<SincronizzatoreListini> log)
{
_log = log;
}
protected override async Task ExecuteAsync(CancellationToken arresto)
{
using var timer = new PeriodicTimer(TimeSpan.FromHours(24));
do
{
try
{
_log.LogInformation("Sincronizzazione listini avviata");
await SincronizzaAsync(arresto);
}
catch (Exception errore) when (errore is not OperationCanceledException)
{
_log.LogError(errore, "Sincronizzazione listini fallita");
}
}
while (await timer.WaitForNextTickAsync(arresto));
}
private static Task SincronizzaAsync(CancellationToken arresto) => Task.CompletedTask;
}Il pacchetto necessario è Microsoft.Extensions.Hosting.WindowsServices. Il corpo di SincronizzaAsync è la libreria di dominio che già avevi, non cambia. La differenza vera è nella gestione degli errori: un servizio di Windows che muore a mezzanotte e nessuno se ne accorge è una delle cause più frequenti di listini sbagliati il mattino dopo, quindi qui l'errore va nel registro e va a una notifica.
I report Crystal
È il punto che fa saltare la stima. Per i 41 report esistono quattro vie, tutte con un costo: tenere un piccolo processo separato su .NET Framework che genera i PDF e che il nuovo chiama via HTTP; sostituire Crystal con un generatore di PDF per .NET; riscrivere i report come pagine HTML con stampa in PDF; comprare la versione del fornitore per .NET moderno, dove esiste. Nel piano di Bergamo ho scelto il processo separato per i primi due anni: sono 15 giornate, e i report migrano uno alla volta alla fine, quando il resto è passato.
Le differenze di comportamento che falsano i totali
Qui il compilatore non ti aiuta. Il codice compila, gira, e dà un risultato diverso. Le quattro che ho incontrato più spesso:
- Stringhe di numeri a virgola mobile. .NET Core 3.0 e successivi stampano il numero più corto che torna esattamente lo stesso valore.
(0.1 + 0.2).ToString()dà0.30000000000000004, dove .NET Framework dava0.3. Se qualcuno confronta stringhe, scrive file o costruisce chiavi con deidouble, il risultato cambia. Gli importi vanno indecimalcomunque. - Confronto e ricerca di stringhe. Su Linux .NET usa la libreria ICU.
"a\r\nb".IndexOf("\n")con la cultura corrente può restituire -1 invece di 2. Si usaStringComparison.Ordinalquando si cerca un carattere tecnico, e si porta l'ordinamento delle liste in italiano sotto test. - Cultura e formato di date e numeri. Il separatore dei migliaia, il formato della data breve e i nomi dei mesi dipendono dalla cultura e dai dati ICU. Un portale che stampa "1.234,50" deve poterlo continuare a fare: si passa sempre la cultura
it-ITin modo esplicito e si controlla con un test. - Serializzazione.
BinaryFormatterè stato rimosso: chi lo usava per salvare oggetti in sessione, in cache o su disco ha dati che il nuovo non sa leggere. Si legge una volta con il vecchio e si riscrive in JSON. Il serializzatore JSON di default è cambiato da Newtonsoft aSystem.Text.Json, con regole diverse su nomi delle proprietà, valori nulli e date.
Altre tre che costano meno ma vanno guardate: i fusi orari (su Linux gli identificatori sono quelli IANA, come Europe/Rome, e non quelli di Windows), la gestione dei percorsi di file (maiuscole e minuscole contano) e HttpClient, che va creato con una fabbrica e non a ogni chiamata.
Un test per ognuna di queste, scritto sul vecchio e rieseguito sul nuovo, vale più di dieci riunioni.
Che cosa può fare l'intelligenza artificiale in una migrazione .NET?
Può fare molto del lavoro noioso e quasi niente del lavoro che decide se il progetto riesce. Serve distinguere i due.
Dove aiuta
- Lettura del codice. Riassumere in italiano che cosa fa un controller di 900 righe, trovare tutti i punti in cui si legge
HttpContext.Current, elencare i moduli HTTP e le loro dipendenze. Lavoro da ore, fatto in minuti. - Traduzioni meccaniche. Convertire un file
Web.configinappsettings.json, una vista Razor di MVC 5 in una di Core, un filtro in un middleware. Il risultato va letto, non incollato. - Scrittura di test. Dato un metodo, proporre i casi limite. Il test di caratterizzazione lo genera bene, se gli dai i dati reali.
- Documentazione. Il registro delle regole, che nessuno aveva scritto.
Dove non ci si può fidare
- Le differenze di comportamento. Un assistente traduce il codice in modo plausibile e non sa che
Roundha una regola di arrotondamento specifica nel tuo contesto. È un errore che compila. - Le decisioni di architettura. Quale area migrare prima, se tenere WCF, se togliere la sessione. Sono giudizi che dipendono dall'azienda, non dal codice.
- La sicurezza. Autenticazione, autorizzazione e gestione dei segreti non si delegano senza una lettura attenta.
Come l'ho tenuta fuori dal conto
Nel modello non ho scontato nemmeno un euro per l'intelligenza artificiale: le 235 giornate della strada C sono quelle che prevedo con gli strumenti di oggi. Dove l'ho vista funzionare, il risparmio sta nell'inventario e nei test, non nella scrittura del codice, ed è un guadagno che si misura a progetto finito. Chi promette di migrare in automatico con l'intelligenza artificiale promette la stessa cosa che prometteva la conversione automatica di VB6 nel 2008: un programma che compila e che nessuno capisce. Per il confronto con quel caso c'è la guida alla migrazione da VB6 a .NET.
Quali errori si ripetono nelle migrazioni a .NET moderno?
Ne ho visti sette ripetersi con poche variazioni.
- Partire dalla riscrittura. Si decide di "rifare tutto bene" e dopo quattordici mesi si ha mezzo portale nuovo e un portale vecchio che nel frattempo è cambiato. Il confronto sta in riscrivere o rifattorizzare.
- Aggiornare .NET e l'architettura nello stesso momento. Se passi a .NET 10 e insieme dividi in microservizi, non sai più se un errore viene dal runtime o dalla nuova struttura. Prima si cambia il runtime, poi si decide se la struttura va cambiata.
- Migrare a una versione che sta per scadere. Oggi .NET 8 e .NET 9 finiscono il 10 novembre 2026. Si migra a .NET 10, o si salta al 12 quando sarà LTS.
- Scrivere i test dopo. Senza i test di caratterizzazione non puoi dire se il nuovo fa quello che faceva il vecchio. Dopo averlo spostato, è tardi.
- Lasciare i report e le stampe per ultimi senza stimarli. È il punto che sfonda il budget. Si stima per primo, anche se si esegue per ultimo.
- Dimenticare l'infrastruttura. Pipeline di rilascio, monitoraggio, registro degli errori, backup. Il nuovo portale in container richiede un modo di rilasciare e di vedere cosa succede, e se non c'è lo si costruisce a metà progetto, di corsa.
- Non misurare il guadagno. Se non conti le giornate di attrito prima e dopo, non puoi dire se il progetto ha pagato. Si misura il tempo di compilazione, la frequenza dei rilasci e le giornate di assistenza.
Un'altra, meno tecnica: decidere dal titolo di un articolo. Anche questo mostra un caso composto. Il tuo portale ha un'altra curva, e l'unico modo per saperlo è mettere i tuoi numeri nella tabella.
Quando non conviene migrare da .NET Framework?
Ci sono quattro situazioni in cui consiglio di non migrare, e le dico io per primo.
- Un programma che sta per essere sostituito. Se entro due anni arriva un gestionale nuovo, ogni euro in migrazione è perso. Si mette in regola (la strada A) e basta.
- Un solo sviluppatore che lo tocca poche volte l'anno. Con l'attrito di 18 giornate il pareggio è a 11,2 anni. Una struttura così non si ripaga.
- Un programma per Windows che resta per Windows. Un'applicazione WinForms che gira sui PC dell'ufficio non ha un costo di infrastruttura da abbattere. Si può portarla a .NET 10 per avere strumenti moderni, ma non è una scadenza. L'idea di portarla su più piattaforme è un altro progetto, con un altro conto.
- Un sistema che nessuno modifica. Se il codice non cambia da tre anni, l'attrito è zero e il solo costo è la macchina. Si sposta su un server supportato e si lascia dov'è.
In tutte e quattro la strada A, quella da 13.020 euro, è la risposta giusta. Un consiglio che non dice quando non spendere vale poco.
Come si decide in una settimana?
Il lavoro di decisione non richiede un progetto. Richiede cinque giorni.
- Giorno 1: l'inventario. Quattordici righe, cinque colonne. Con l'Upgrade Assistant in sola lettura.
- Giorno 2: le tre domande per progetto e l'elenco di ciò che non passa: WCF, report, componenti a 32 bit.
- Giorno 3: i quattro numeri. Quanti sviluppatori lavorano sul portale, quante giornate perdono in attrito, quanto costa l'infrastruttura, quanto costerebbe un fermo di una settimana.
- Giorno 4: il conto. La tabella a uno, tre, cinque, otto e dieci anni con i tuoi valori, e il punto di pareggio.
- Giorno 5: la decisione e il primo passo. Una pagina sola, con la strada scelta, la prima area da migrare e il test che la protegge.
Al termine devi poter rispondere a tre domande senza guardare gli appunti: quanto costa restare, quanto costa muoversi e dopo quanti anni conviene. Se non sai rispondere, la settimana non è finita.
Se vuoi che qualcuno guardi i numeri con te, ne parliamo nella consulenza software. Se cerchi il quadro più ampio, con i casi in cui la risposta è non toccare niente, c'è la pagina sulla modernizzazione del software legacy.
Che cosa farebbe oggi l'azienda di Bergamo?
Con tre sviluppatori, un portale che cambia ogni settimana e una macchina che esce dal supporto a gennaio, l'azienda fa tre cose, in questo ordine.
- Entro dicembre, i primi passi della A: sorgenti in controllo di versione, rilascio automatico, pacchetti aggiornati, passaggio a Windows Server 2022 e al database supportato. Servono in ogni caso e sono già dentro le 235 giornate della C.
- Da gennaio, i test di caratterizzazione e la conversione dei file di progetto al formato moderno, su tutto il portale vecchio. Sono i due passi che costano poco e tagliano il rischio di tutto il resto.
- Da febbraio, la strada C con il catalogo come prima area. Il proxy davanti, il carrello come seconda. Revisione dei numeri dopo la prima area: se le giornate effettive sono più alte di quelle previste, il pareggio si sposta e si ricalcola.
Sono 98.700 euro in diciotto mesi. A regime il portale costa 19.376 euro l'anno in meno, e dal sesto anno il conto torna. Non è un investimento che si ripaga in fretta. È la risposta giusta se il portale deve vivere altri dieci anni, e se non deve, i 13.020 euro bastano.
In cinque righe. Misura le giornate di attrito prima di parlare con qualsiasi fornitore. Metti in regola l'ambiente: sorgenti, rilascio, pacchetti, server supportato. Scrivi i test sul comportamento del vecchio con casi reali. Migra un'area per volta con un proxy davanti, partendo da una pagina di sola lettura. Se il portale deve vivere meno di cinque anni, non migrare. Per altre situazioni, il conto da fare è lo stesso e si trova per Delphi, per WebForms e per VB6, e il quadro d'insieme è nella pagina sulla modernizzazione del software legacy. Se stai valutando anche quanto spendere per l'infrastruttura dopo il passaggio, la guida a come risparmiare con Azure mostra dove si recuperano i costi.
Domande frequenti
Per un portale MVC 5 con tre sviluppatori, migrare un'area per volta costa circa 98.700 euro (235 giornate, diciotto mesi). Aggiornare tutto in blocco costa 115.710 euro con lo sforamento previsto, riscrivere 235.200 e restare in regola 13.020.
No. Segue il ciclo di vita di Windows, quindi riceve correzioni finché esiste un Windows supportato che lo include. Scadono invece Windows Server 2016 (gennaio 2027) e SQL Server 2016 (luglio 2026), e .NET 8 e 9 il 10 novembre 2026.
Nel caso descritto il pareggio fra migrare un'area per volta e restare in regola sta a 5,9 anni (6,6 rispetto al non fare niente). Scende a 3,9 anni con sei sviluppatori e a 3,7 se un fermo costa 20.000 euro l'anno; sale a 11,2 con uno sviluppatore.
Con un proxy YARP in ASP.NET Core davanti al portale vecchio e un'area per volta: le librerie condivise si compilano per net48 e net10.0, sessione e autenticazione si condividono con SystemWebAdapters e un solo database serve entrambi.
Quattro: la stampa dei numeri double, il confronto di stringhe con ICU, cultura e formato di date e numeri e la serializzazione (BinaryFormatter rimosso, JSON diverso). Si scrivono test di caratterizzazione sul vecchio e si rieseguono sul nuovo.
Quando il programma verrà sostituito entro due anni, quando lo tocca un solo sviluppatore (pareggio a 11,2 anni), quando è un'applicazione Windows che resta su Windows o quando il codice non cambia da anni. Lì bastano 13.020 euro di messa in regola.
