Abwärtskompatibilität in .NET mit BinaryFormatter

8

Wir verwenden BinaryFormatter in einem C # -Spiel, um Benutzerspielfortschritt, Spielniveaus usw. zu speichern. Wir stoßen auf das Problem der Rückwärtskompatibilität.

Die Ziele:

  • Leveldesigner erstellt Kampagnen (Ebenen und Regeln), wir ändern den Code, die Kampagne sollte trotzdem funktionieren. Dies kann jeden Tag während der Entwicklung vor der Veröffentlichung geschehen.
  • Der Benutzer speichert das Spiel, wir veröffentlichen einen Spiel-Patch, der Benutzer sollte trotzdem in der Lage sein, das Spiel
  • zu laden
  • Der unsichtbare Datenkonvertierungsprozess sollte funktionieren, egal wie weit die beiden Versionen entfernt sind. Zum Beispiel kann ein Benutzer unsere ersten 5 kleineren Aktualisierungen überspringen und die 6 direkt erhalten. Trotzdem sollten seine gespeicherten Spiele immer noch gut geladen werden.

Die Lösung muss für Benutzer und Ebenenentwickler vollständig unsichtbar sein und Codierer, die etwas ändern möchten, minimal belasten (z. B. ein Feld umbenennen, weil sie an einen besseren Namen denken).

Einige Objektgraphen, die wir serialisieren, sind in einer Klasse verwurzelt, manche in anderen. Vorwärtskompatibilität ist nicht erforderlich.

Mögliche Änderungen (und was passiert, wenn wir die alte Version serialisieren und in die neue Version deserialisieren):

  • Feld hinzufügen (wird standardmäßig initialisiert)
  • Ändern Sie den Feldtyp (Fehler)
  • Umbenennen (entspricht dem Entfernen und Hinzufügen eines neuen)
  • Ändere Eigenschaft zu Feld und zurück (entspricht einer Umbenennung)
  • Ändern Sie die Eigenschaft autoimplemented so, dass sie das Hintergrundfeld (entspricht einer Umbenennung) verwendet
  • add superclass (entspricht dem Hinzufügen seiner Felder zur aktuellen Klasse)
  • interpretiere ein Feld anders (z. B. in Grad, jetzt im Bogenmaß)
  • Für Typen, die ISerializable implementieren, können wir unsere Implementierung der ISerializable-Methoden ändern (z. B. die Komprimierung in der ISerializable-Implementierung für einen wirklich großen Typ starten)
  • Benennen Sie eine Klasse um, benennen Sie einen enum-Wert um

Ich habe gelesen:

Meine aktuelle Lösung :

  • Wir machen so viele Änderungen wie möglich, indem wir Dinge wie den OnDeserializing Callback verwenden.
  • Wir planen, die Änderungen einmal alle zwei Wochen zu ändern, sodass weniger Kompatibilitätscode zur Verfügung steht.
  • Immer, bevor wir eine bahnbrechende Änderung vornehmen, kopieren wir all die von uns verwendeten [Serializable] -Klassen in einen Namespace / Ordner namens OldClassVersions.VersionX (wobei X die nächste Ordnungszahl nach der letzten ist) ). Wir machen das auch, wenn wir nicht bald eine Veröffentlichung machen werden.
  • Beim Schreiben in eine Datei ist das, was wir serialisieren, eine Instanz dieser Klasse: class SaveFileData {int version; Objektdaten; }
  • Beim Lesen aus der Datei deserialisieren wir die SaveFileData und übergeben sie an eine iterative "update" -Routine, die in etwa wie folgt vorgeht:

.

%Vor%
  • Aus praktischen Gründen kann die Funktion Update () in ihrer Implementierung eine CopyOverlappingPart () - Funktion verwenden, die Reflektion verwendet, um so viele Daten wie möglich von der alten Version in die neue Version zu kopieren. Auf diese Weise kann die Update () - Funktion nur Dinge handhaben, die sich tatsächlich geändert haben.

Einige Probleme damit:

  • deserialisiert die Deserialisierung zur Klasse Foo und nicht zur Klasse OldClassVersions.Version5.Foo - weil die Klasse Foo serialisiert ist.
  • fast unmöglich zu testen oder zu debuggen
  • erfordert es, alte Kopien vieler Klassen zu behalten, was fehleranfällig, zerbrechlich und nervig ist
  • Ich weiß nicht, was ich tun soll, wenn wir eine Klasse umbenennen wollen

Dies sollte ein wirklich häufiges Problem sein. Wie lösen die Leute es normalerweise?

    
Stefan Monov 27.08.2010, 11:29
quelle

2 Antworten

3

Schwierig. Ich würde binär dump und XML-Serialisierung verwenden (einfacher zu verwalten, tolerant gegenüber Änderungen, die nicht zu extrem sind - wie das Hinzufügen / Entfernen von Feldern). In extremeren Fällen ist es einfacher, eine Transformation (xslt vielleicht) von einer Version in eine andere zu schreiben und die Klassen sauber zu halten. Wenn eine Deckkraft und ein geringer Platzbedarf auf der Festplatte erforderlich sind, können Sie versuchen, die Daten vor dem Schreiben auf die Festplatte zu komprimieren.

    
Adrian Zanescu 27.08.2010, 11:47
quelle
2

Wir haben das gleiche Problem in unserer Anwendung mit dem Speichern von Benutzerprofildaten (Gitterspaltenanordnung, Filtereinstellungen ...).

In unserem Fall war das Problem die AssemblyVersion.

Für dieses Problem erstelle ich ein SerializationBinder , das die tatsächliche Assemblyversion von liest die Assemblys (alle Assemblys erhalten bei der neuen Implementierung eine neue Versionsnummer) mit Assembly.GetExecutingAssembly().GetName().Version .

In der überschriebenen Methode BindToType wird die Typinformation mit der neuen Assemblyversion erstellt.

Die Deserialisierung wird "von Hand" durchgeführt, dh

  • Deserialisieren über normalen BinaryFormatter
  • ruft alle Felder ab, die deserialisiert werden müssen (kommentiert mit eigenem Attribut)
  • Füllen Sie das Objekt mit Daten aus dem deserialisierten Objekt

Funktioniert mit all unseren Daten und seit drei oder vier Releases.

    
Khh 27.08.2010 11:47
quelle