Fortgeschrittene Themen der .NET Serialisierung

Veröffentlicht: 19. Mrz 2003 | Aktualisiert: 22. Jun 2004

Von Ingo Hassler

Dieser Artikel zeigt, wie Sie die Feinheiten der .NET Serialisierung in den Griff bekommen. Highlights sind die Versionierung beim Deserialisieren, "Custom Serialization" und das Serialisieren "nicht serialisierbarer" Klassen.

Auf dieser Seite

 Einführung
 StreamingContext
 Custom Serialization
 IDeserializationCallback
 Die Identität eines Typs
 SerializationBinder
 Surrogate Serialization
 Versionierung
 SerializationExceptions
 Fazit

Einführung

Einfaches Serialisieren von Objekten ist unter .NET wirklich sehr "simpel" geworden. Eine Klasse kann ohne das Schreiben von Code, durch Setzen des Attributs [Serializable] serialisierbar gemacht werden. Sehen wir uns ein kurzes Beispiel zur "Simple Serialization" an (Downloaden Sie den Quellcode)

// Assembly A 
// ========== 
using System; 
using System.IO; 
using System.Runtime.Serialization; 
using System.Runtime.Serialization.Formatters; 
using System.Runtime.Serialization.Formatters.Soap; 
[Serializable] 
class Katze // Version 1 
{ 
  public string Name; 
  public double Gewicht; 
  public double Laenge; 
  public Katze (string N, double G, double L) 
  { 
 Name = N; Gewicht = G; Laenge = L; 
  } 
} 
public class App 
{ 
  public static void Main () 
  { 
 string path = @"javascript:void(null);"; 
 Katze Garfield = new Katze("Garfield", 16, 0.42); 
 // Serialize Katze 
 FileStream fs = new FileStream (path,  
   FileMode.Create, FileAccess.Write); 
 SoapFormatter sf = new SoapFormatter (); 
 sf.Serialize (fs, Garfield); 
 fs.Close (); 
 // Destroy Katze 
 Garfield = null; 
 // Deserialize Katze 
 fs = new FileStream (path, FileMode.Open, FileAccess.Read); 
 Garfield = sf.Deserialize (fs) as Katze; 
  } 
}

In Main() wird ein Objekt unserer serialisierbaren Klasse Katze von einem SoapFormatter-Objekt in einen seriellen Datenstrom transformiert und durch ein FileStream-Objekt in der Datei C:\serialize_Katze_V1.xml abgespeichert. Der Aufruf der Serialize() Methode des Formatters erfasst auch alle in der Klasse aggregierten Objektreferenzen und eine eventuell vorhandene Basisklasse.

Wunderbar, alles geht fast wie von selbst, Sie müssen lediglich eine Referenz auf das Assembly System.Runtime.Formatters.Soap von Hand zu Ihrem Projekt hinzufügen.

Im Anschluss an das Serialisieren löschen wir das Katze-Objekt und deserialisieren es mit der Deserialize() Methode des Formatters sofort wieder aus der eben erzeugten Datei.

Die generierte XML/SOAP Datei hat folgenden Inhalt:

<SOAP-ENV:Envelope xmlns:xsi="<A href="http://www.w3.org/2001/XMLSchema-instance">http://www.w3.org/2001/XMLSchema-instance</A>"  
 xmlns:xsd="<A href="http://www.w3.org/2001/XMLSchema">http://www.w3.org/2001/XMLSchema</A>"  
 xmlns:SOAP-ENC="<A href="https://schemas.xmlsoap.org/soap/encoding/">https://schemas.xmlsoap.org/soap/encoding/</A>"  
 xmlns:SOAP-ENV="<A href="https://schemas.xmlsoap.org/soap/envelope/">https://schemas.xmlsoap.org/soap/envelope/</A>"  
 xmlns:clr="<A href="https://schemas.microsoft.com/soap/encoding/clr/1.0">https://schemas.microsoft.com/soap/encoding/clr/1.0</A>"  
 SOAP-ENV:encodingStyle="<A href="https://schemas.xmlsoap.org/soap/encoding/">https://schemas.xmlsoap.org/soap/encoding/</A>"> 
<SOAP-ENV:Body> 
<a1:Katze id="ref-1"  
 xmlns:a1="<A href="https://schemas.microsoft.com/clr/assem/A">https://schemas.microsoft.com/clr/assem/A</A> 
 %2C%20Version%3D1.0.1122.17761%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Dnull"> 
<Name id="ref-3">Garfield</Name> 
<Gewicht>16</Gewicht> 
<Laenge>0.42</Laenge> 
</a1:Katze> 
</SOAP-ENV:Body> 
</SOAP-ENV:Envelope>

Alternativ zum SoapFormatter hätten wir auch den performanteren BinaryFormatter verwenden können, der zwar eine kompaktere, aber dafür nicht lesbare Ausgabe generiert.

Alle Formatter-Klassen implementieren das IFormatter Interface:

public interface IFormatter 
{ 
  // Properties 
  ISurrogateSelector SurrogateSelector {get; set;} 
  SerializationBinder Binder {get; set;} 
  StreamingContext Context {get; set;} 
  // Methods 
  object Deserialize (Stream serializationStream); 
  void Serialize (Stream serializationStream, object graph); 
}

Die Methoden Serialize() und Deserialize() haben wir nun kennen gelernt. Sehen wir als nächstes das Property StreamingContext an.

 

StreamingContext

Die Objektserialisierung wird im .NET Framework an vielen Stellen eingesetzt:

  • Sicherung des Session-Zustands bei einer ASP.NET Webapplikation.

  • Transport von Daten übers Clipboard in Windows Forms Applikationen.

  • Übertragen von Objekten z.B. von einem Rechner zum anderen bei .NET Remoting.

  • SOAP

  • ...

Daten, wie z.B. Pointer, die in Fields eines zu serialisierenden Objekts gespeichert sind, können natürlich nicht sinnvoll über Prozessgrenzen oder gar Rechnergrenzen transportiert werden.
Über das Feld State des StreamingContext Properties kann dem Formatter deshalb mitgeteilt werden, in welchem Zusammenhang er bei der Serialisierung bzw. bei der Deserialisierung gerade arbeitet. Der mögliche Serialisierungskontext enthält die Aufzählung StreamingContextStates:

enum  StreamingContextStates  { All, Clone,  
 CrossAppDomain, CrossMachine, CrossProcess, File, Other, Persistence, Remoting }

Arbeitet der Formatter z.B. im StreamingContextState Clone, ist ein und der selbe Prozess sowohl Quelle als auch Ziel des Serialisierungsstreams. Folglich macht es durchaus Sinn, Pointer oder Fenster-Handle zu serialisieren.

Im StreamingContext CrossMachine hingegen sollten wir beim Serialisieren, Pointer oder Handles einfach weglassen, um den Serialisierungsstream abzuspecken und beim Deserialisieren dafür sorgen, dass Pointer und Handles wieder korrekt befüllt werden.

Ok das bedeutet also, dass wir in irgend einer Form Einfluss auf den Serialisierungsvorgang nehmen müssen, damit wir den StreamingContextState auswerten können. Wie wir Einfluss nehmen können zeigt der folgende Abschnitt Custom Serialization.

 

Custom Serialization

Die StreamingContext-Information können wir bei der "Custom Serialization" auswerten.
Im Gegensatz zur "Simple Serialization" erlaubt uns die "Custom Serialization" die volle Kontrolle über die Serialisierung eines Objekts zu übernehmen, hat aber den Nachteil, dass wir uns auch um die Serialisierung Basisklasse selbst kümmern müssen. "Custom Serialization" beruht auf der Implemtierung des ISerializable Interfaces und eines speziellen Konstruktors.

public interface ISerializable  
{ 
 void GetObjectData (SerializationInfo info,  
   StreamingContext context); 
}

Sehen wir uns ein Beispiel an:

// Assembly CustomSerialize 
// ======================== 
[Serializable] 
class Katze : ISerializable 
{ 
  public string Name; 
  public double Gewicht; 
  public double Laenge; 
  // ISerializable  
  public void GetObjectData (SerializationInfo info, 
  StreamingContext context) 
  { 
 // evaluate StreamingContext Flags here 
 // if (context  == StreamingContextStates.CrossProcess) 
 // { . . . }  
 info.AddValue ("Name", Name); 
 info.AddValue ("Gewicht", Gewicht); 
 info.AddValue ("Laenge", Laenge); 
  } 
  // Deserialize Constructor 
  protected Katze (SerializationInfo si, StreamingContext context) 
  { 
 // evaluate StreamingContext Flags here 
   //  e.g. 
 // if (context.State  == StreamingContextStates.CrossProcess) 
 // { . . . }  
 Name = si.GetString ("Name"); 
 Gewicht = si.GetDouble ("Gewicht"); 
 Laenge  = si.GetDouble ("Laenge"); 
  } 
}

Das Attribut [Serializable] wird für serialisierbare Klassen in jedem Fall benötigt.

Wie funktioniert nun die "Custom Serialization"? Der Formatter ruft beim Serialisieren eines Objekts, welches das ISerializable Interface implementiert, dessen GetObjectData() Methode auf und übergibt als Parameter eine SerializationInfo und eine StreamingContext Referenz. Das übergebene SerializationInfo Objekt muss in GetObjectData () mit den zu serialisierenden Daten befüllt werden. Dies geschieht für jedes zu serialisierende Feld mit der Methode AddValue(). AddValue() bekommt als Parameter ein String-Value Paar übergeben und speichert den Value in einer Hashtabelle ab, wobei der String zur Erzeugung des Hash-Keys dient und eindeutig sein muss. AddValue() ist für alle Common Type System (CTS) Basis-Datentypen sowie einige wichtige Framework Class Library (FCL) Klassen überladen und kann auch für aggregierte Objekte aufgerufen werden, sofern diese das ISerializable Interface implementieren. Der Formatter erkennt dies und ruft dann die GetObjectData() Methode des Objekts auf.

Gut, was heißt das letztendlich für uns Programmierer? Wir müssen in GetObjectData() alle zu serialisierenden Felder unseres Objekts mit einem eindeutigem Namen durch die AddValue() Methode in das SerializationInfo Objekt einhängen.

Beim Deserialisieren ruft der Formatter den erwähnten Konstruktor auf (der am besten als protected deklariert wird). Auch dieser Konstruktor enthält als Parameter eine SerializationInfo- und eine StreamingContext Referenz. Im Konstruktor werden die Daten mit GetXXX() Methoden aus dem SerializationInfo Objekt herausgeholt, wobei XXX durch den jeweiligen Typnamen zu ersetzen ist.

 

IDeserializationCallback

Eine andere Möglichkeit, Einfluss auf den Deserialisierungsprozess zu nehmen, ist das Interface IDeserializationCallback.

public Interface IDeserializationCallback 
{ 
  void OnDeserialization(object sender); 
}

Implementiert ein vom Formatter deserialisiertes Objekt das IDeserializationCallback Interface, so ruft der Formatter nach der Deserialisierung des Objekts abschließend die Methode OnDeserialization() auf. In dieser Methode können z.B. Felder, die mit dem [NonSerialized] Attribut von der Serialisierung ausgeschlossen wurden, wieder befüllt werden.

// Assembly DeserializationCallback 
// ================================ 
[Serializable] 
class Katze : IDeserializationCallback // Version 2 
{ 
  public string Name; 
  public double Gewicht; 
  public double Laenge; 
  [NonSerialized] 
  public int BodyMassIndex;  // Excluded from Serialization 
  public Katze (string N, double G, double L) 
  { 
 Name = N; Gewicht = G; Laenge = L; 
 CalcBMI (); 
  } 
  protected void CalcBMI () 
  { 
 BodyMassIndex = (int)(Gewicht / (Laenge * Laenge)); 
  } 
  // IDeserializationCallback 
  public void OnDeserialization(object sender) 
  { 
 CalcBMI (); 
  } 
}

Die Version 2 unserer Katzenklasse enthält ein zusätzliches Field namens BodyMassIndex, das jederzeit aus Gewicht und Laenge der Katze neu berechnet werden kann. Um unser XML/SOAP File nicht unnötig aufzublähen, wird es von der Serialisierung ausgeschlossen. Damit das Katzenobjekt nach dem Deserialisieren wieder vollständig befüllt ist, sorgen wir in OnDeserialization() für eine Neuberechnung des ausgeschlossenen Fields.

 

Die Identität eines Typs

Untersucht man die XML/SOAP Ausgabe-Datei unseres ersten Beispiels genauer, so fällt auf, dass der Formatter nicht nur die Felder unseres Katze-Objekts abgespeichert hat, sondern auch Name, Version, Culture und PublicKeyToken des Assemblies in dem die Katzenklasse definiert ist. Hier sind noch mal die betreffenden Zeilen:

xmlns:a1="<A href="https://schemas.microsoft.com/clr/assem/A%2C%20Version%3">https://schemas.microsoft.com/clr/assem/A%2C%20Version%3</A> 
 D1.0.1122.17761%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Dnull">

Tatsächlich verwendet der Formatter für die Identifizierung eines Typs den vollständigen "TypeName" und den vollständigen "AssemblyName". Die beiden Strings sind wie folgt aufgebaut:

TypeName:   Namespace.Class 
AssemblyName:  SimpleName,<, Culture=CultureInfo> 
   <, Version = Major.Minor.Build.Revision> 
   <, PublikKeyToken> 
   <, StrongName>'\0' < > = optional 
Legende:  
Namespace   Textueller Name des hierarchischen Namespaces  
Class Textueller Name des Typs 
SimpleName  Textueller Name des Assemblies 
Culture  RCF1766 kodierte Kulturinformation 
Version  Zusammengesetzt aus den Nummern 
   Major.Minor.Build.Revision 
StrongName  Hex low-order 64Bit Zahl des PublicKey 
   Hash-Wertes (codiert mit dem SHA-1 Alg.)  
PublikKeyToken Wenn bekannt, kann der StrongName durch den 
   Hex kodierten PublicKey ersetzt werden

Das bedeutet aber, dass die Identität einer Klasse alleine durch das Verschieben von einem Assembly in ein anderes verändert wird. Ein Formatter ist nicht in der Lage, die Verwandtschaft der beiden Typen zu erkennen und wird ein "altes" Objekt nicht mehr in den "neuen" Typ deserialisieren können. Versucht man es dennoch, wirft der Formatter eine SerializationException.

 

SerializationBinder

Der Ausweg aus diesem Dilemma heißt SerializationBinder. In einem Satz könnte man die Funktion dieses Objekts so umschreiben: Der SerializationBinder gibt dem Formatter beim Deserialisieren eines Typs Information darüber, ob eventuell ein "Ersatz-Typ" verwendet werden soll.

Im folgenden Beispiel implementieren wir ein neues Assembly B mit einer exakten Kopie der Katzenklasse aus Assembly A. Mit Hilfe eines SerializationBinder Objekts sind wir in der Lage, das XML/SOAP File mit dem Katzen-Objekt aus Assembly A in ein Katzen-Objekt des Assemblies B zu deserialisieren.

Assembly B 
========== 
using System; 
using System.IO; 
using System.Runtime.Serialization; 
using System.Runtime.Serialization.Formatters; 
using System.Runtime.Serialization.Formatters.Soap; 
using System.Reflection; 
[Serializable] 
class Katze // Version 1 
{ 
  public string   Name; 
  public double Gewicht; 
  public double Laenge; 
  public Katze (string N, double G, double L) 
  { 
 Name = N; Gewicht = G; Laenge = L; 
  } 
} 
public class App 
{ 
  public static void Main () 
  { 
 string path = @"javascript:void(null);"; 
 Katze Garfield; 
 // Deserialize 
 FileStream fs  = new FileStream (path, FileMode.Open, 
 FileAccess.Read); 
 SoapFormatter sf  = new SoapFormatter (); 
 MySeBinder Binder = new MySeBinder (); 
 sf.Binder = Binder; 
 Garfield  = sf.Deserialize (fs) as Katze; 
  } 
} 
sealed class MySeBinder : SerializationBinder 
{ 
  public override Type BindToType (string assemblyName, 
  string typeName) 
  { 
 // Versisoning 
 // return desired Type for each  
 // requested deserialization Type 
 if (assemblyName == "A, Version=1.0.1122.17761" + 
   ", Culture=neutral" + 
   ", PublicKeyToken=null" 
  && typeName == ".Katze") 
 { 
   assemblyName = Assembly.GetExecutingAssembly().FullName; 
   typeName = "Katze"; 
 } 
 // return Type to be deserialized   
 string s = string.Format ("{0}, {1}", typeName, 
   assemblyName); 
 Type T =  Type.GetType(s); 
 return T; 
  } 
}

Wie funktioniert nun der SerializationBinder? Zunächst einmal unterscheidet sich der neue Deserialisierungscode in Main() von demjenigen im ersten Beispiel durch ein MySeBinder Objekt, welches dem Binder-Property des Formatters zugewiesen wird. Die Klasse MySeBinder haben wir von SerializationBinder abgeleitet und darin die Methode BindToType() überschrieben. BindToType() wird vom Formatter jedes Mal vor dem Deserialisieren eines Objekts aufgerufen. Der Formatter erwartet von BindToType() die Rückgabe des Typs, der für die Deserialisierung des Objekts tatsächlich verwendet werden soll. In unserem Fall ist es logischerweise die Katzenklasse des Assemblies B. Die Identifzierung des Typs läuft wieder über die Strings AssemblyName und TypeName, die als Parameter an BindToType() übrgeben werden.

Natürlich kann der Formatter eine Katze nicht in einen Hund verwandeln und daher muss der Aufbau und Inhalt des von BindToType() zurückgegebenen Typs exakt mit dem ursprünglichen Typ übereinstimmen. Tut er es nicht, wirft der Formatter eine System.Runtime.Serialization.SerializationException.

 

Surrogate Serialization

Ein anderes Problem taucht auf, wenn Sie ein Objekt eines Typs serialisieren wollen, der nicht serialisierbar ist. Stammt der Typ aus einem Assembly eines Fremdherstellers, können Sie nicht einfach den Quelltext editieren, das [Serializable] Attribut vor den Typ setzen und das Assembly neu compilieren.
Es gibt aber eine Lösung und die heißt "Surrogate Serialization".

Ein SurrogateSelector-Objekt wird in den Formatter eingehängt und teilt diesem beim Serialisiern bzw. Deserialisieren eines Typs mit, ob ein "Ersatz-Typ" verwendet werden soll. Den "Ersatz-Typ" nennt man den Surrogat-Typ. Bis hierher erinnert das sehr stark an den SerializationBinder. Der Unterschied ist jedoch, dass der Surrogat-Typ nur für den Serialisierungszweck existiert und häufig nur das Interface ISerializationSurrogate implementiert.

public Interface ISerializationSurrogate 
{ 
  void GetObjectData (object obj,  
 SerializationInfo info, 
 StreamingContext context); 
 object SetObjectData (object obj, 
 SerializationInfo info, 
 StreamingContext context, 
 ISurrogateSelector selector); 
}

Wie Sie unschwer erraten haben, wird GetObjectData() beim Serialisieren und SetObjectData() beim Deserialisieren vom Formatter aufgerufen. Sehen wir uns wieder ein Beispiel an:

// Assembly C 
// ========== 
using System; 
// Class Katze is not serializable ! 
public class Katze // Version 1 
{ 
  . . . 
} 
// Assembly D 
// ========== 
using System; 
using System.IO; 
using System.Runtime.Serialization; 
using System.Runtime.Serialization.Formatters; 
using System.Runtime.Serialization.Formatters.Soap; 
class App 
{ 
  static void Main(string[] args) 
  { 
 string path = @"javascript:void(null);"; 
 Katze Garfield = new Katze("Garfield", 16, 0.42); 
 // *** Serialize 
 FileStream fs = new FileStream (path, FileMode.Create, 
   FileAccess.Write); 
 SoapFormatter sf = new SoapFormatter (); 
 // Build SurrogateSelector and call AddSurrogate 
 // for each Surrogate Type 
 SurrogateSelector sursel = new SurrogateSelector (); 
 sursel.AddSurrogate (typeof (Katze), 
 new StreamingContext (StreamingContextStates.All), 
 new ErsatzKatze()); 
 sf.SurrogateSelector = sursel; 
 sf.Serialize (fs, Garfield); 
 fs.Close (); 
 // *** Destroy Objekt 
 Garfield = null; 
 // *** Deserialize 
 fs = new FileStream (path, FileMode.Open,  
 FileAccess.Read); 
 Garfield = sf.Deserialize (fs) as Katze;    
  } 
} 
sealed class ErsatzKatze : ISerializationSurrogate 
{ 
  // Method called to serialize a "Katze" 
  public void GetObjectData (Object obj,  
  SerializationInfo si, 
  StreamingContext sc) 
  { 
 Katze K = obj as Katze; 
 si.AddValue ("Name", K.Name); 
 si.AddValue ("Gewicht", K.Gewicht); 
 si.AddValue ("Laenge", K.Laenge); 
  } 
  // Method called to deserialize a "Katze" 
  public object SetObjectData (Object obj, 
 SerializationInfo si, 
 StreamingContext sc, 
 ISurrogateSelector sl) 
  { 
 Katze K  = obj as Katze; 
 K.Name   = si.GetString ("Name"); 
 K.Gewicht= si.GetDouble ("Gewicht"); 
 K.Laenge = si.GetDouble ("Laenge"); 
 return K; 
  } 
}

Die Surrogat-Klasse ErsatzKatze ist mit dem Keyword sealed versehen, damit nicht versehentlich von ihr abgeleitet werden kann.

Beginnen wir in Main(): Dort legen wir ein SurrogateSelector-Objekt an, in das wir mit AddSurrogate() unsere Surrogat Typen eintragen. In unserem Fall ist das nur der Typ ErsatzKatze, der als Surrogat für die Klasse Katze dient.

void AddSurrogate (Type, StreamingContext,  
 ISerializationSurrogate);

Type ist der auszutauschende Typ, StreamingContext der Serialisierungskontext und ISerializationSurrogate der Surrogat-Typ.

Soll nun ein Objekt vom nicht serialisierbaren Typ Katze serialisiert werden, so sieht der Formatter zuerst im SurrogateSelector nach, ob ein Surrogat-Typ verwendet werden soll. Da das hier der Fall ist, ruft der Formatter somit GetObjectData () unserer Surrogat-Klasse auf.
GetObjectData () sorgt schließlich dafür, dass die Felder aus dem Katzenobjekt in den Stream serialisiert werden. Eventuell vorhandene private Felder können per Reflection abgefragt werden.
Beim Deserialisieren läuft es prinzipiell genauso ab, nur wird dann SetObjectData() vom Formatter aufgerufen.

 

Versionierung

Wir haben noch nicht den problematischsten Anwendungsfall der Serialisierung unter die Lupe genommen, die Versionierung.

Eine Anwendung versucht, eine XML/SOAP Datei einer älteren Version zu laden. Es soll z.B. ein Katzenobjekt der Version 1.0 in ein Katzenobjekt der aktuellen Version 3.0 deserialisiert werden. Diese Problematik betrifft ausschließlich die Deserialisierung, denn beim Serialisieren wird natürlich immer die aktuelle Version der Katzenklasse verwendet.

Folgende Punkte sind zu beachten:

  • Der Typ Katze Version 1.0 ist der Anwendung nicht mehr bekannt. Das betreffende Assembly wurde natürlich nicht referenziert.

  • Die Custom Serialization alleine kann das Problem nicht lösen, da der Typ Katze Version 1.0 nicht mehr vorhanden ist.

  • Der SerializationBinder alleine kann das Problem nicht lösen. Er kann zwar den Typ Katze Version 3.0 als Ersatz-Typ für den Typ Katze Version 1.0 definieren, der Formatter wird aber eine SerializationException werfen, weil die Typen sich inhaltlich unterscheiden.

  • Die Surrogate Serialization alleine kann das Problem nicht lösen, da Sie nur einen Surrogate-Typen für einen in der Anwendung bekannten Typ definieren kann. Der Ursprungstyp ist ja aber eben nicht referenziert und damit auch nicht bekannt.

Ok, wir müssen also die oben genannten Werkzeuge kombinieren, um eine Lösung herbeizuführen. Zwei Kombinationen sind denkbar:

  • SerializationBinder + Custom Serialization

  • SerializationBinder + Surrogate Serialization

Sehen wir uns ein Beispiel an, welches die Kombination SerializationBinder + Custom Serialization verwendet.

// Assembly DeserializeVersioning 
// ============================== 
using System; 
using System.IO; 
using System.Runtime.Serialization; 
using System.Runtime.Serialization.Formatters; 
using System.Runtime.Serialization.Formatters.Soap; 
using System.Reflection; 
[Serializable] 
class Katze : ISerializable // Version 3 
{ 
  public string Name; 
  public double Gewicht; 
  public double Laenge; 
  public int BodyMassIndex; 
  public bool   fHungrig; 
  public Katze (string N, double G, double L) 
  { 
 Name = N; Gewicht = G; Laenge = L; 
 CalcBMI (); 
 fHungrig = true; 
  } 
  // ISerializable  
  public void GetObjectData (SerializationInfo info, 
 StreamingContext context) 
  { 
 info.AddValue ("Name", Name); 
 info.AddValue ("Gewicht", Gewicht); 
 info.AddValue ("Laenge",  Laenge); 
 info.AddValue ("BMI",  BodyMassIndex); 
 info.AddValue ("Hungrig", fHungrig); 
  } 
  // Deserialize Constructor 
  public Katze (SerializationInfo si, StreamingContext sct) 
  { 
 // Version 1.0 
 Name = si.GetString ("Name"); 
 Gewicht = si.GetDouble ("Gewicht"); 
 Laenge  = si.GetDouble ("Laenge"); 
 // *** Versioning through Exception Handling  
 // Version 2.0 
 try 
 { 
   BodyMassIndex = si.GetInt32 ("BMI"); 
 } 
 catch (SerializationException ex) 
 { 
   CalcBMI (); 
 } 
 // Version 2.7 
 try 
 { 
   fHungrig = si.GetBoolean ("Hungrig"); 
 } 
 catch (SerializationException ex) 
 { 
   fHungrig = true; 
 } 
  } 
  protected void CalcBMI () 
  { 
 BodyMassIndex = (int)(Gewicht / Laenge * Laenge); 
  } 
} 
public class App 
{ 
  public static void Main () 
  { 
 string path = @"javascript:void(null);"; 
 Katze Garfield;  
 // *** Deserialize 
 SoapFormatter sf  = new SoapFormatter (); 
 MySeBinder Binder = new MySeBinder (); 
 sf.Binder = Binder; 
 FileStream fs = new FileStream (path, FileMode.Open,  
   FileAccess.Read); 
 Garfield = sf.Deserialize (fs) as Katze;  
  } 
} 
sealed class MySeBinder : SerializationBinder 
{ 
  public override Type BindToType (string assemblyName, 
 string typeName) 
  { 
 // Versisoning 
 // return desired Type for each  
 // requested deserialization Type 
 if (assemblyName == "A, Version=1.0.1122.17761" + 
   ", Culture=neutral" + 
   ", PublicKeyToken=null" 
   && typeName == ".Katze") 
 { 
   assemblyName = Assembly.GetExecutingAssembly().FullName; 
   typeName = "Katze"; 
 } 
 // return Type to be deserialized   
 string s = string.Format ("{0}, {1}", typeName, 
   assemblyName); 
 Type T =  Type.GetType (s); 
 return T; 
  } 
}

Unser Assembly definiert die Katzenklasse in der Version 3.
In Main() soll also ein Katzenobjekt der Version 1 deserialisiert werden. Wir legen ein MySeBinder Objekt an, und hängen es als SerializationBinder in den SoapFormatter ein. Beim Aufruf von Deserialize() wird BindToType() unserer MySeBinder-Klasse aufgerufen und gibt die aktuelle Katzenklasse Version 3 als zu deserialisierenden Typ zurück. Der Formatter ruft daraufhin den Deserialisierungs-Konstruktor dieser Klasse auf.

Die Fields Name, Gewicht, Laenge befanden sich schon in der Version 1 der Katzenklasse und können sofort deserialisiert werden. Das Field BodyMassIndex hingegen ist in der Version 2 neu hinzugekommen. Kann BodyMassIndex nicht aus dem FileSteam deserialisiert werden, so wirft der Formatter eine SerializationException, die wir auffangen und dann den BodyMassIndex neu berechnen.

Mit dem Field fHungrig sieht es ähnlich aus. Auch dieses Field ist in der Version 1 noch nicht enthalten und daher wird wieder eine Exception geworfen. Die Exception wird von uns dazu benutzt fHungrig mit einem geeignetem Default-Wert zu befüllen.

Die zweite Alternative SerializationBinder + Surrogate Serialization leistet das Gleiche, ist jedoch noch ein bisschen umständlicher und länger, da noch eine Surrogate-Klasse implementiert werden muss. Aus diesem Grund können wir guten Gewissens auf ein Beispiel verzichten.

 

SerializationExceptions

Abschließend ein paar Gründe warum ein Formatter eine SerializationException werfen kann. Eine SerializationException wird geworfen wenn ...

  • ein Objekt serialisiert werden soll, dessen Klasse das [Serializable] Attribut nicht besitzt.

  • die Basisklasse eines zu serialisierenden Objekts nicht serialierbar ist.

  • eine aggregierte Objektreferenz bei "Simple Serialization" auf ein nicht serialiseirbares Objekt zeigt.

  • Der Typ beim Deserialisieren nicht gefunden wird, z.B. weil das entsprechende Assembly nicht referenziert wurde.

  • Der Ersatztyp bei Verwendung eines SerializationBinders nicht 100% inhaltsgleich mit dem Ursprungstyp ist.

 

Fazit

Microsoft hat den .NET Framework mit sehr mächtigen und flexiblen Serialisierungseigenschaften ausgerüstet und uns Entwickler damit einen dicken Brocken Arbeit abgenommen. Für leichte Fälle genügt häufig die einfach zu bedienende Simple Serialization. Stößt diese an ihre Grenzen sorgen Custom- oder Surrogate Serialisierung für das nötige extra Quäntchen Kontrolle über den Serialisierungsvorgang. Abgerundet wird die Serialisierung durch den SerializationBinder, der bei Versionierung zusätzlich ins Spiel gebracht werden muss.

Literatur
[1] Jeffrey Richter, "Run-time Serialization":
Part 1, msdn.microsoft.com/msdnmag/issues/02/04/net/default.aspx, Apr. 2002
Part 2, msdn.microsoft.com/msdnmag/issues/02/07/net/default.aspx, Juli 2002
Part 3, msdn.microsoft.com/msdnmag/issues/02/09/net/default.aspx, Sep. 2002