CodeGym /Corsi /C# SELF /Compatibilità quando si modifica la struttura delle class...

Compatibilità quando si modifica la struttura delle classi

C# SELF
Livello 45, Lezione 4
Disponibile

1. Introduzione

Immagina: hai scritto una classe Person, serializzato un oggetto su file, e dopo un paio di mesi decidi che gli serve, per esempio, un nuovo indirizzo di residenza o hai cambiato il tipo di alcune proprietà. Sembrerebbe una cosa normale — ma quando provi a caricare (deserializzare) i dati salvati in un vecchio formato, potresti avere sorprese: qualcosa non si carica, viene lanciata un'eccezione, alcuni valori risultano vuoti o addirittura errati.

Questo comportamento è un caso tipico di rottura della compatibilità retroattiva. Nello sviluppo reale succede più spesso di quanto gli studenti dimentichino il punto e virgola (cioè molto spesso).

Presentiamo il problema con un esempio

Consideriamo il nostro mini-progetto didattico. Supponiamo che in questa fase avessimo questa classe:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

Serializziamo un'istanza di questa classe in JSON:

Person p = new Person { Name = "Alice", Age = 35 };
string json = JsonSerializer.Serialize(p);
File.WriteAllText("person.json", json);

Nel file otteniamo:

{"Name":"Alice","Age":35}

Ora, dopo una settimana, decidiamo di rendere la nostra app più moderna aggiungendo il campo indirizzo:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string Address { get; set; } // nuovo campo
}

E poi proviamo a caricare il vecchio file:

string json = File.ReadAllText("person.json");
Person p = JsonSerializer.Deserialize<Person>(json);

Cosa succederà? L'indirizzo nel nostro oggetto non ci sarà: la proprietà Address risulterà null. Non è stato sollevato nessun errore. Per ora tutto funziona... Ma se inizi a cambiare tipi, rimuovere campi o fare qualcosa di veramente "interessante", possono iniziare i problemi!

2. Che tipi di cambiamenti esistono — e come influenzano

I cambiamenti nella struttura delle classi influenzano la serializzazione in modi diversi. Analizziamo alcuni scenari tipici.

Aggiunta di nuove proprietà

Questo è il caso meno pericoloso. I dati vecchi (dove queste proprietà non esistevano) vengono deserializzati tranquillamente: le nuove proprietà saranno impostate ai valori di default (null per i reference, 0 per int, ecc.).

Attenzione: Se la tua nuova proprietà non è nullable e non ha un valore di default "ragionevole", potrebbe esserci un problema (soprattutto con le proprietà required di C# 11+).

Rimozione di proprietà

Se rimuovi una proprietà e nei dati serializzati essa è ancora presente — il serializer molto probabilmente ignorerà il "superfluo" e il caricamento avverrà comunque.

Ma questo dipende dal serializer usato. Per esempio, JsonSerializer e Newtonsoft.Json sono abbastanza permissivi: non lanceranno eccezioni, mentre alcuni serializer vecchi o custom possono comportarsi diversamente.

Rinominare proprietà

Qui inizia il divertimento. Se semplicemente rinomini la proprietà FirstName in Name, il serializer non sarà in grado di fare il mapping tra i campi dei vecchi dati e il nuovo oggetto. In altre parole, il campo risulterà vuoto (null/0), mentre il vecchio valore nel file verrà ignorato.

Cambio del tipo della proprietà

Per esempio, prima avevi public int Age, poi decidi di farlo diventare public string Age (magari qualcuno scriverà "immortale" — può succedere). Tentare di deserializzare i dati vecchi può portare a un errore ("Cannot convert number to string") o la proprietà potrebbe semplicemente ottenere il valore di default. Dipende dal serializer specifico e dalle sue impostazioni di strict typing.

Modifica delle gerarchie (ereditarietà, nidificazione)

Se cambi le classi base, sposti proprietà in posti diversi o, per esempio, fai una classe wrapper per un'altra — i dati serializzati vecchi possono diventare completamente incompatibili. Questo riguarda in particolare XML e gerarchie complesse di oggetti.

3. Problemi di compatibilità

Come accorgersi dei problemi di compatibilità?

Spesso l'errore di compatibilità non si manifesta subito e in modo evidente: la tua applicazione inizia semplicemente a comportarsi "stranamente", parte dei dati va persa, o nei log compare un'eccezione poco informativa. Di solito i problemi emergono quando:

  • Un utente carica un file vecchio in una nuova versione del programma.
  • Il server riceve JSON/XML da un client di "vecchia versione".
  • Lavori con un API esterno il cui contratto è stato aggiornato improvvisamente.

I sintomi vanno dagli errori di deserializzazione ai campi "inaspettatamente" vuoti.

Impatto del serializer sulla compatibilità

Non tutti i serializer si comportano allo stesso modo. I più "tolleranti" ai cambi di struttura sono i serializer JSON — sia lo standard System.Text.Json che Newtonsoft.Json. In genere essi ignorano le proprietà sconosciute dal file, e non serializzano di nuovo i campi dell'oggetto che sono sconosciuti.

Con XML le cose sono un po' più rigide: se cambia l'elemento radice o la gerarchia, possono comparire errori.

Nei formati binari possono addirittura lanciarsi eccezioni se l'ordine o i tipi sono stati cambiati!

4. Come minimizzare i rischi? Approcci e pratiche

Ecco alcuni approcci che aiutano a ridurre al minimo i problemi (e a volte a evitarli del tutto).

Usa versioni per classi e dati

Aggiungi un campo speciale Version negli oggetti serializzabili o nei file stessi. Questo permette di capire con quale versione della struttura è stato creato il file e decidere cosa fare al caricamento (per esempio eseguire un upgrade dei dati).

public class PersonV2
{
    public int Version { get; set; } = 2;
    public string Name { get; set; }
    public int Age { get; set; }
    public string Address { get; set; }
}

Applica attributi di mapping dei nomi (per la serializzazione)

Con JSON e XML puoi indicare esplicitamente come deve chiamarsi una proprietà nella forma serializzata. Se rinomini una proprietà — conserva il vecchio nome:

public class Person
{
    [JsonPropertyName("FirstName")] // per System.Text.Json
    [JsonProperty("FirstName")]     // per Newtonsoft.Json
    public string Name { get; set; }
    public int Age { get; set; }
}

Usa tipi nullable e valori di default

Se compaiono nuovi campi che non sono sempre presenti nei vecchi dati — rendili nullable o assegna un valore di default, così la deserializzazione non crea problemi:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string? Address { get; set; } = "Unknown";
}

Gestione dell'evento "campo sconosciuto"

Con Newtonsoft.Json puoi registrarti per gestire i campi "incomprensibili" tramite un handler dedicato, per esempio per loggare una potenziale situazione pericolosa.

var settings = new JsonSerializerSettings
{
    MissingMemberHandling = MissingMemberHandling.Error
};
try
{
    var person = JsonConvert.DeserializeObject<Person>(json, settings);
}
catch (JsonSerializationException ex)
{
    Console.WriteLine("Non è stato possibile deserializzare: " + ex.Message);
}

Migrazione dei dati

Se i cambiamenti sono sostanziali, è più sensato prevedere una fase di migrazione: per esempio caricare i dati nella "vecchia" struttura e poi convertirli in quella nuova:

// Supponiamo che PersonV1 fosse senza address
public class PersonV1 { public string Name; public int Age; }

// Nuova — con address
public class PersonV2 { public string Name; public int Age; public string Address; }

// Migrazione:
string oldJson = File.ReadAllText("person.json");
PersonV1 oldPerson = JsonSerializer.Deserialize<PersonV1>(oldJson);

PersonV2 migrated = new PersonV2
{
    Name = oldPerson.Name,
    Age = oldPerson.Age,
    Address = "Unknown"
};

5. Casi complessi e errori inattesi

Invarianza del campo e proprietà required

Con C# 11 e successivi sono arrivate le proprietà required. Ora se un campo è marcato required, la deserializzazione può fallire se quel campo non è presente nei dati:

public class Person
{
    public string Name { get; set; }
    [JsonPropertyName("Age")]
    public required int Age { get; set; }
    public string Address { get; set; }
}

Se nei dati vecchi manca la proprietà Age — verrà sollevata un'eccezione per mismatch della struttura.

Cambio di tipo: int → string

// Era:
public class Record { public int Count; }
// È diventato:
public class Record { public string Count; }

Se nei dati c'è "Count":42, la deserializzazione in string potrebbe funzionare (smart-conversion), ma nella direzione opposta — potrebbe scatenarsi un'eccezione.

Rimozione della classe base

Se l'oggetto serializzato aveva ereditarietà e hai cambiato la gerarchia — deserializzare i file vecchi può portare a errori, a volte "silenziosi", a volte evidenti.

6. Errori tipici lavorando sulla compatibilità

Errore n.1: cambiare a caso proprietà esistenti.
Rinominare o cambiare il tipo delle proprietà senza considerare i dati già serializzati porta alla perdita di informazioni durante la deserializzazione.

Errore n.2: dimenticare i tipi nullable per i nuovi campi.
I nuovi campi dovrebbero essere nullable o avere valori di default ragionevoli.

Errore n.3: non testare la compatibilità retroattiva.
Hai cambiato la classe — verifica sempre che i file/dati vecchi si carichino correttamente.

Errore n.4: mescolare attributi di librerie diverse.
Non usare JsonPropertyName e JsonProperty contemporaneamente sulla stessa proprietà.

2
Compito
C# SELF, livello 45, lezione 4
Bloccato
Gestione dell'assenza di campi durante la deserializzazione
Gestione dell'assenza di campi durante la deserializzazione
1
Sondaggio/quiz
Configurazione della serializzazione, livello 45, lezione 4
Non disponibile
Configurazione della serializzazione
Configurazione della serializzazione degli oggetti
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION