Riferimento sulla funzionalità di BinaryFormatter
BinaryFormatter è stato introdotto per la prima volta con la versione iniziale di .NET Framework nel 2002. Per comprendere come sostituire l'utilizzo di BinaryFormatter, è utile sapere come funziona BinaryFormatter.
BinaryFormatter può serializzare qualsiasi istanza di qualsiasi tipo annotata con [Serializable]
oppure implementa l'interfaccia ISerializable.
Nomi dei membri
Nello scenario più comune, il tipo viene annotato con [Serializable]
e il serializzatore usa la reflection per serializzare tutti i campi (pubblici e non pubblici), ad eccezione di quelli annotati con [NonSerialized]
. Per impostazione predefinita, i nomi dei membri serializzati corrispondono ai nomi dei campi del tipo. Storicamente, questo ha portato a incompatibilità quando anche i campi privati vengono rinominati sui tipi di [Serializable]
. Durante le migrazioni da BinaryFormatter, diventa necessario comprendere come sono stati gestiti e sottoposti a override i nomi dei campi serializzati.
Proprietà automatiche C#
Per le proprietà implementate automaticamente da C# ({ get; set; }
), BinaryFormatter serializzerà i campi sottostanti generati dal compilatore C#, non le proprietà. I nomi di tali campi di backup serializzati contengono caratteri C# non validi e non possono essere controllati. Un decompiler C# (ad esempio https://sharplab.io/ o ILSpy) può illustrare come vengono presentate al runtime le proprietà automatiche C#.
[Serializable]
internal class PropertySample
{
public string Name { get; set; }
}
La classe precedente viene convertita dal compilatore C# in:
[Serializable]
internal class PropertySample
{
private string <Name>k__BackingField;
public string Name
{
get
{
return <Name>k__BackingField;
}
set
{
<Name>k__BackingField = value;
}
}
}
In questo caso, <Name>k__BackingField
è il nome del membro che BinaryFormatter
usa nel payload serializzato. Non è possibile usare nameof
o qualsiasi altro operatore C# per ottenere questo nome.
L'interfaccia ISerializable include il metodo GetObjectData che consente agli utenti di controllare i nomi usando uno dei metodi AddValue.
// Note lack of any special attribute.
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue("Name", this.Name);
}
Se tale personalizzazione è stata applicata, è necessario specificare anche le informazioni durante la deserializzazione. Questo è possibile usando il costruttore di serialization, in cui tutti i valori vengono letti da SerializationInfo con uno dei metodi Get
forniti.
private PropertySample(SerializationInfo info, StreamingContext context)
{
this.Name = info.GetString("Name");
}
Nota
L'operatore nameof
non è stato usato volutamente in questo caso, perché il payload può essere salvato in modo permanente e la proprietà può essere rinominata in un secondo momento. Pertanto, anche se viene rinominato (ad esempio FirstName
perché si decide di introdurre anche una proprietà LastName
), per rimanere compatibile con le versioni precedenti, la serialization deve comunque usare il nome precedente che potrebbe essere stato salvato in modo permanente in un punto qualsiasi.
Serialization binder
È consigliabile usare SerializationBinder per controllare il caricamento delle classi e imporre la classe da caricare. Ciò riduce al minimo le vulnerabilità di sicurezza (quindi vengono caricati solo i tipi consentiti, anche se l'utente malintenzionato modifica il payload per deserializzare e caricare qualcos'altro).
L'uso di questo tipo richiede l'ereditarietà da esso e l'override del metodo BindToType.
Idealmente, l'elenco dei tipi serializzabili è chiuso perché significa che è possibile sapere quali tipi possono essere creati come istanze per ridurre le vulnerabilità di sicurezza.