In diesem Artikel werde ich die Erfahrungen mit der Serialisierung von Binärtypen zwischen Assemblys ohne Bezug zueinander teilen. Wie sich herausstellte, gibt es echte und „legitime“ Fälle, in denen Sie Daten deserialisieren müssen, ohne einen Link zu der Assembly zu haben, in der sie deklariert sind. In dem Artikel werde ich über das Szenario sprechen, in dem es erforderlich war, ich werde die Lösungsmethode beschreiben und ich werde auch über Zwischenfehler sprechen, die während der Suche gemacht wurden
Einführung Erklärung des Problems
Wir arbeiten mit einem großen Unternehmen auf dem Gebiet der Geologie zusammen. In der Vergangenheit hat das Unternehmen eine sehr unterschiedliche Software für die Arbeit mit Daten geschrieben, die von verschiedenen Gerätetypen stammen + Datenanalyse + Prognose. Leider ist all diese Software weit davon entfernt, immer „freundlich“ miteinander zu sein, und meistens überhaupt nicht freundlich. Um die Informationen irgendwie zu konsolidieren, wird jetzt ein Webportal erstellt, in dem verschiedene Programme ihre Daten in Form von XML hochladen. Und das Portal versucht, eine Plus-Minus-Gesamtansicht zu erstellen. Eine wichtige Nuance: Da die Entwickler des Portals in den Themenbereichen jeder Anwendung nicht stark sind, stellte jedes Team ein Parser / Datenkonverter-Modul von seiner XML-Datei für die Portaldatenstrukturen bereit.
Ich arbeite in einem Team, das eine der Anwendungen entwickelt, und wir haben ziemlich einfach einen Exportmechanismus für unseren Teil der Daten geschrieben. Hier entschied der Geschäftsanalyst jedoch, dass das zentrale Portal einen der Berichte benötigt, die unser Programm erstellt. Hier trat das erste Problem auf: Der Bericht wird jedes Mal neu erstellt und die Ergebnisse werden nirgendwo gespeichert.
"Also rette es!" Der Leser wird wahrscheinlich denken. Das habe ich auch gedacht, war aber ernsthaft enttäuscht von der Anforderung, dass der Bericht bereits für die heruntergeladenen Daten erstellt werden muss. Nichts zu tun - Sie müssen Logik übertragen.
Stufe 0. Refactoring. Nichts bedeutete Ärger
Es wurde beschlossen, die Logik zum Erstellen des Berichts (tatsächlich ist dies eine 4-Spalten-Beschriftung, aber die Logik ist ein Wagen und ein großer Wagen) in eine separate Klasse zu unterteilen und die Datei mit dieser Klasse durch Bezugnahme in die Parser-Assembly aufzunehmen. Damit wir:
- Vermeiden Sie direktes Kopieren
- Schutz vor Versionsdifferenzen
Die Aufteilung der Logik in eine separate Klasse ist keine schwierige Aufgabe. Aber dann war nicht alles so rosig: Der Algorithmus basierte auf Geschäftsobjekten, deren Übertragung nicht in unser Konzept passte. Ich musste die Methoden neu schreiben, damit sie nur einfache Typen akzeptieren und sie bearbeiten. Es war nicht immer einfach und erforderte stellenweise Lösungen, deren Schönheit in Frage blieb, aber insgesamt wurde eine zuverlässige Lösung ohne offensichtliche Krücken erhalten.
Es gab ein Detail, das, wie Sie wissen, oft als gemütliche Zuflucht für den Teufel dient: Wir haben einen seltsamen Ansatz von früheren Entwicklergenerationen geerbt, nach dem einige der zum Erstellen eines Berichts erforderlichen Daten als binär serialisierte .Net-Objekte in der Datenbank gespeichert werden ( Fragen „Warum?“, „Kaaak?“ usw. bleiben leider aufgrund des Mangels an Adressaten unbeantwortet. Und bei der Eingabe der Berechnungen müssen wir sie natürlich deserialisieren.
Diese Typen, die man nicht loswerden konnte, haben wir auch "per Referenz" aufgenommen, zumal sie eher nicht kompliziert waren.
Stufe 1. Deserialisierung. Merken Sie sich den vollständigen Typnamen
Nachdem ich die obigen Manipulationen durchgeführt und einen Testlauf durchgeführt hatte, erhielt ich unerwartet einen Laufzeitfehler
[A] Namespace.TypeA kann nicht in [B] Namespace.TypeA umgewandelt werden. Typ A stammt aus 'Assembley.Application, Version = 1.0.0.0, Culture = neutral, PublicKeyToken = null' im Kontext 'Default' am Speicherort '...'. Typ B stammt aus 'Assmbley.Portal, Version = 1.0.0.0, Culture = neutral, PublicKeyToken = null' im Kontext 'Default' at location ''.
Die allerersten Google-Links haben mir mitgeteilt, dass BinaryFormatter nicht nur Daten schreibt, sondern auch Informationen in den Ausgabestream eingibt, was logisch ist. Und wenn man bedenkt, dass der vollständige Name des Typs die Assembly enthält, in der er deklariert ist, unterscheidet sich das Bild von dem, für das ich versucht habe, einen Typ zu deserialisieren, völlig aus der Sicht von .Net
Nachdem ich mich am Kopf gekratzt hatte, traf ich zufällig eine offensichtliche, aber leider bösartige Entscheidung, einen bestimmten Typ A-Typ während der
dynamischen Deserialisierung zu ersetzen. Alles hat funktioniert. Die Ergebnisse des Berichts konvergierten von oben nach unten, Tests auf dem Build-Server wurden bestanden. Mit dem Gefühl der Leistung senden wir die Aufgabe an die Tester.
Stufe 2. Die Haupt. Serialisierung zwischen Baugruppen
Die Abrechnung erfolgte schnell in Form von von Testern registrierten Fehlern, die besagten, dass der Parser auf der Portalseite mit der Ausnahme, dass die Assembly Assembley.Application (Assembly aus unserer Anwendung) nicht geladen werden konnte, herunterfiel. Erster Gedanke - Ich habe keine Referenzen bereinigt. Aber - nein, alles ist in Ordnung, niemand bezieht sich. Ich versuche es erneut im Sandkasten auszuführen - alles funktioniert. Ich fange an, einen Erstellungsfehler zu vermuten, aber hier fällt mir eine Idee ein, die mir nicht gefällt: Ich ändere den Ausgabepfad für den Parser in einen separaten Ordner und nicht in das freigegebene bin-Verzeichnis der Anwendung. Und voila - ich bekomme die beschriebene Ausnahme. Die Stectrace-Analyse bestätigt vage Vermutungen - die Deserialisierung nimmt ab.
Das Bewusstsein war schnell und schmerzhaft: Das Ersetzen eines bestimmten Typs durch Dynamic änderte nichts. BinaryFormatter erstellte immer noch einen Typ aus einer externen Assembly, nur wenn die Assembly mit dem Typ in der Nähe war, die Laufzeit ihn natürlich lud und wenn die Assembly weg war. Wir bekommen einen Fehler.
Es gab einen Grund, traurig zu sein. Aber googeln gab Hoffnung in Form
der SerializationBinder-Klasse . Wie sich herausstellte, können Sie damit den Typ bestimmen, in dem unsere Daten deserialisiert werden. Erstellen Sie dazu einen Erben und definieren Sie darin die folgende Methode.
public abstract Type BindToType(String assemblyName, String typeName);
in dem Sie für bestimmte Bedingungen einen beliebigen Typ zurückgeben können.
Die BinaryFormatter-Klasse verfügt über eine
Binder- Eigenschaft, in die Sie Ihre Implementierung einfügen können.
Es scheint, dass es kein Problem gibt. Aber auch hier bleiben Details (siehe oben).
Zunächst müssen Sie Anforderungen für
alle Typen (und auch für Standard) verarbeiten.
Eine interessante Implementierungsoption wurde hier im Internet gefunden , aber sie versuchen, den Standardordner von BinaryFormatter in Form einer Konstruktion zu verwenden
var defaultBinder = new BinaryFormatter().Binder
Tatsächlich ist die Binder-Eigenschaft jedoch standardmäßig null. Eine Analyse des Quellcodes ergab, dass innerhalb des BinaryFormatter, ob Binder überprüft wird, wenn ja, seine Methoden aufgerufen werden, wenn nicht, interne Logik verwendet wird, die letztendlich auf läuft
var assembly = Assembly.Load(assemblyName); return FormatterServices.GetTypeFromAssembly(assembly, typeName);
Ohne weiteres wiederholte ich die gleiche Logik in mir.
Folgendes ist in der ersten Implementierung passiert
public class MyBinder : SerializationBinder { public override Type BindToType(string assemblyName, string typeName) { if (assemblyName.Contains("<ObligatoryPartOfNamespace>") ) { var bindToType = Type.GetType(typeName); return bindToType; } else { var bindToType = LoadTypeFromAssembly(assemblyName, typeName); return bindToType; } } private Type LoadTypeFromAssembly(string assemblyName, string typeName) { if (string.IsNullOrEmpty(assemblyName) || string.IsNullOrEmpty(typeName)) return null; var assembly = Assembly.Load(assemblyName); return FormatterServices.GetTypeFromAssembly(assembly, typeName); } }
Das heißt, Es wird geprüft, ob der Namespace zum Projekt gehört - wir geben den Typ aus der aktuellen Domäne zurück, wenn der Systemtyp - wir laden aus der entsprechenden Assembly
Es sieht logisch aus. Wir beginnen zu testen: Unser Typ kommt - wir ersetzen, er wird erstellt. Hurra! String kommt - wir gehen entlang des Zweigs mit dem Laden aus der Baugruppe. Es funktioniert! Öffnen Sie virtuellen Champagner ...
Aber hier ... Ein Wörterbuch enthält Elemente von Benutzertypen: Da es sich um einen Systemtyp handelt, versuchen wir natürlich, ihn aus der Assembly zu laden, aber da die Elemente, die er enthält, unsere Typen sind, mit vollständiger Qualifikation (Assembly, Version, Schlüssel) ), dann fallen wir wieder. (Es sollte ein trauriges Lächeln geben).
Natürlich müssen Sie den Eingabenamen des Typs ändern und Links zur gewünschten Assembly ersetzen. Ich habe wirklich gehofft, dass es für den Typnamen ein Analogon der
AssemblyName- Klasse gibt, aber ich habe nichts Ähnliches gefunden. Das Schreiben eines universellen Parsers mit Ersatz ist keine leichte Aufgabe. Nach einer Reihe von Experimenten kam ich zu der folgenden Lösung: Im statischen Konstruktor subtrahiere ich die zu ersetzenden Typen und suche dann in der Zeile nach dem Namen des erstellten Typs nach ihren Namen. Wenn ich ihn finde, ersetze ich den Namen der Baugruppe
Wie Sie sehen können, habe ich davon ausgegangen, dass PublicKeyToken das letzte in der Typbeschreibung ist. Vielleicht ist dies nicht 100% zuverlässig, aber in meinen Tests habe ich keine Fälle gefunden, in denen dies nicht der Fall ist.
Also eine Zeile der Form
"System.Collections.Generic.Dictionary`2 [[SomeNamespace.CustomType, Assembley.Application, Version = 1.0.0.0, Culture = neutral, PublicKeyToken = null], [System.Byte [], mscorlib, Version = 4.0.0.0, Kultur = neutral, PublicKeyToken = b77a5c561934e089]] »
verwandelt sich in
"System.Collections.Generic.Dictionary`2 [[SomeNamespace.CustomType, Assembley.Portal, Version = 1.0.0.0, Culture = neutral, PublicKeyToken = null], [System.Byte [], mscorlib, Version = 4.0.0.0, Kultur = neutral, PublicKeyToken = b77a5c561934e089]] »
Jetzt funktionierte endlich alles "wie eine Uhr". Es gab kleinere technische Feinheiten: Wenn Sie sich erinnern, wurden die von uns enthaltenen Dateien in den Link der Hauptanwendung aufgenommen. In der Hauptanwendung werden all diese Tänze jedoch nicht benötigt. Daher ein bedingter Kompilierungsmechanismus des Formulars
BinaryFormatter binForm = new BinaryFormatter(); #if EXTERNAL_LIB binForm.Binder = new MyBinder(); #endif
Dementsprechend definieren wir in der Portalassembly das Makro EXTERNAL_LIB, aber in der Hauptanwendung - Nr
"Nicht-lyrischer Exkurs"
Tatsächlich habe ich beim Codieren, um die Lösung schnell zu überprüfen, eine Fehleinschätzung vorgenommen, die mich wahrscheinlich eine bestimmte Anzahl von Nervenzellen gekostet hat: Für den Anfang habe ich nur die Typensubstitution für Dicitionary fest codiert. Infolgedessen stellte sich nach der Deserialisierung heraus, dass es sich um ein leeres Wörterbuch handelte, das ebenfalls "abstürzte", wenn versucht wurde, einige Operationen damit auszuführen. Ich begann bereits zu glauben, dass man BinaryFormatter nicht täuschen könnte , und begann verzweifelte Experimente mit dem Versuch, den Erben des Wörterbuchs zu schreiben. Glücklicherweise hörte ich fast rechtzeitig auf und schrieb wieder einen universellen Substitutionsmechanismus. Als ich ihn implementierte, stellte ich fest, dass es nicht ausreicht, seinen Typ neu zu definieren, um seinen Typ neu zu definieren: Sie müssen sich immer noch um die Typen für KeyValuePair <TKey, TValue>, Comparer kümmern, die ebenfalls angefordert werden Bindemittel
Dies sind die Abenteuer der binären Serialisierung. Für das Feedback wäre ich dankbar.