XML-Serialisierung und geerbte Typen

84

In Anlehnung an meine vorherige Frage habe ich daran gearbeitet, mein Objektmodell in XML zu serialisieren. Aber ich bin jetzt auf ein Problem gestoßen (quelle Überraschung!).

Das Problem, das ich habe, ist, dass ich eine Sammlung habe, die von einem abstrakten Basisklassentyp ist, der von den konkreten abgeleiteten Typen gefüllt wird.

Ich dachte, es wäre in Ordnung, nur die XML-Attribute allen beteiligten Klassen hinzuzufügen, und alles wäre pfirsichfarben. Leider ist das nicht der Fall!

Also habe ich ein bisschen bei Google gegraben und verstehe jetzt, warum es nicht funktioniert. Dadurch , dass das XmlSerializerist in der Tat eine kluge Reflexion um serialize tun Objekte zu / von XML, und seit seinem auf der Grundlage der abstrakten Art, es nicht herausfinden kann , was zum Teufel , um es spricht . Fein.

Ich bin auf diese Seite in CodeProject gestoßen, die anscheinend sehr hilfreich ist (noch nicht vollständig gelesen / konsumiert), aber ich dachte, ich möchte dieses Problem auch in die StackOverflow-Tabelle aufnehmen, um zu sehen, ob Sie eine ordentliche haben Hacks / Tricks, um dies so schnell wie möglich zum Laufen zu bringen.

Eine Sache, die ich auch hinzufügen sollte, ist, dass ich den Weg NICHT gehen möchte XmlInclude. Es gibt einfach zu viel Kopplung damit, und dieser Bereich des Systems befindet sich in einer intensiven Entwicklung, so dass es ein echtes Wartungsproblem wäre!

Rob Cooper
quelle
Es wäre hilfreich, einige relevante Codefragmente zu sehen, die aus den Klassen extrahiert wurden, die Sie serialisieren möchten.
Rex M
Mate: Ich habe wieder geöffnet, weil ich der Meinung bin, dass andere Leute dies nützlich finden könnten, aber Sie können es
gerne
Etwas verwirrt, da so lange nichts mehr in diesem Thread war?
Rob Cooper
Es gibt die Antwort: stackoverflow.com/questions/6737666/…
Odys

Antworten:

54

Problem gelöst!

OK, also bin ich endlich da (zugegebenermaßen mit viel Hilfe von hier !).

Fassen Sie also zusammen:

Tore:

  • Ich wollte die XmlInclude- Route wegen der Wartungskopfschmerzen nicht gehen .
  • Sobald eine Lösung gefunden wurde, wollte ich, dass sie schnell in andere Anwendungen implementiert werden kann.
  • Es können Sammlungen von abstrakten Typen sowie einzelne abstrakte Eigenschaften verwendet werden.
  • Ich wollte mich nicht wirklich darum kümmern, "besondere" Dinge in den konkreten Klassen tun zu müssen.

Identifizierte Probleme / zu beachtende Punkte:

  • XmlSerializer macht einige ziemlich coole Reflexionen, aber es ist sehr begrenzt, wenn es um abstrakte Typen geht (dh es funktioniert nur mit Instanzen des abstrakten Typs selbst, nicht mit Unterklassen).
  • Die Xml-Attributdekoratoren definieren, wie der XmlSerializer die gefundenen Eigenschaften behandelt. Der physikalische Typ kann ebenfalls angegeben werden, dies schafft jedoch eine enge Kopplung zwischen der Klasse und dem Serializer (nicht gut).
  • Wir können unseren eigenen XmlSerializer implementieren, indem wir eine Klasse erstellen, die IXmlSerializable implementiert .

Die Lösung

Ich habe eine generische Klasse erstellt, in der Sie den generischen Typ als den abstrakten Typ angeben, mit dem Sie arbeiten werden. Dies gibt der Klasse die Möglichkeit, zwischen dem abstrakten Typ und dem konkreten Typ zu "übersetzen", da wir das Casting hart codieren können (dh wir können mehr Informationen erhalten als der XmlSerializer).

Ich habe dann die IXmlSerializable- Schnittstelle implementiert , dies ist ziemlich einfach, aber beim Serialisieren müssen wir sicherstellen, dass wir den Typ der konkreten Klasse in das XML schreiben, damit wir ihn beim De-Serialisieren zurücksetzen können. Es ist auch wichtig zu beachten, dass es vollständig qualifiziert sein muss, da sich die Baugruppen, in denen sich die beiden Klassen befinden, wahrscheinlich unterscheiden. Es gibt natürlich eine kleine Typprüfung und Dinge, die hier passieren müssen.

Da der XmlSerializer nicht umgewandelt werden kann, müssen wir den Code dafür bereitstellen, damit der implizite Operator dann überladen wird (ich wusste nicht einmal, dass Sie dies tun können!).

Der Code für den AbstractXmlSerializer lautet:

using System;
using System.Collections.Generic;
using System.Text;
using System.Xml.Serialization;

namespace Utility.Xml
{
    public class AbstractXmlSerializer<AbstractType> : IXmlSerializable
    {
        // Override the Implicit Conversions Since the XmlSerializer
        // Casts to/from the required types implicitly.
        public static implicit operator AbstractType(AbstractXmlSerializer<AbstractType> o)
        {
            return o.Data;
        }

        public static implicit operator AbstractXmlSerializer<AbstractType>(AbstractType o)
        {
            return o == null ? null : new AbstractXmlSerializer<AbstractType>(o);
        }

        private AbstractType _data;
        /// <summary>
        /// [Concrete] Data to be stored/is stored as XML.
        /// </summary>
        public AbstractType Data
        {
            get { return _data; }
            set { _data = value; }
        }

        /// <summary>
        /// **DO NOT USE** This is only added to enable XML Serialization.
        /// </summary>
        /// <remarks>DO NOT USE THIS CONSTRUCTOR</remarks>
        public AbstractXmlSerializer()
        {
            // Default Ctor (Required for Xml Serialization - DO NOT USE)
        }

        /// <summary>
        /// Initialises the Serializer to work with the given data.
        /// </summary>
        /// <param name="data">Concrete Object of the AbstractType Specified.</param>
        public AbstractXmlSerializer(AbstractType data)
        {
            _data = data;
        }

        #region IXmlSerializable Members

        public System.Xml.Schema.XmlSchema GetSchema()
        {
            return null; // this is fine as schema is unknown.
        }

        public void ReadXml(System.Xml.XmlReader reader)
        {
            // Cast the Data back from the Abstract Type.
            string typeAttrib = reader.GetAttribute("type");

            // Ensure the Type was Specified
            if (typeAttrib == null)
                throw new ArgumentNullException("Unable to Read Xml Data for Abstract Type '" + typeof(AbstractType).Name +
                    "' because no 'type' attribute was specified in the XML.");

            Type type = Type.GetType(typeAttrib);

            // Check the Type is Found.
            if (type == null)
                throw new InvalidCastException("Unable to Read Xml Data for Abstract Type '" + typeof(AbstractType).Name +
                    "' because the type specified in the XML was not found.");

            // Check the Type is a Subclass of the AbstractType.
            if (!type.IsSubclassOf(typeof(AbstractType)))
                throw new InvalidCastException("Unable to Read Xml Data for Abstract Type '" + typeof(AbstractType).Name +
                    "' because the Type specified in the XML differs ('" + type.Name + "').");

            // Read the Data, Deserializing based on the (now known) concrete type.
            reader.ReadStartElement();
            this.Data = (AbstractType)new
                XmlSerializer(type).Deserialize(reader);
            reader.ReadEndElement();
        }

        public void WriteXml(System.Xml.XmlWriter writer)
        {
            // Write the Type Name to the XML Element as an Attrib and Serialize
            Type type = _data.GetType();

            // BugFix: Assembly must be FQN since Types can/are external to current.
            writer.WriteAttributeString("type", type.AssemblyQualifiedName);
            new XmlSerializer(type).Serialize(writer, _data);
        }

        #endregion
    }
}

Wie können wir den XmlSerializer von dort aus anweisen, mit unserem Serializer und nicht mit dem Standard zu arbeiten? Wir müssen unseren Typ innerhalb der Xml-Attribute type-Eigenschaft übergeben, zum Beispiel:

[XmlRoot("ClassWithAbstractCollection")]
public class ClassWithAbstractCollection
{
    private List<AbstractType> _list;
    [XmlArray("ListItems")]
    [XmlArrayItem("ListItem", Type = typeof(AbstractXmlSerializer<AbstractType>))]
    public List<AbstractType> List
    {
        get { return _list; }
        set { _list = value; }
    }

    private AbstractType _prop;
    [XmlElement("MyProperty", Type=typeof(AbstractXmlSerializer<AbstractType>))]
    public AbstractType MyProperty
    {
        get { return _prop; }
        set { _prop = value; }
    }

    public ClassWithAbstractCollection()
    {
        _list = new List<AbstractType>();
    }
}

Hier können Sie sehen, dass eine Sammlung und eine einzelne Eigenschaft verfügbar gemacht werden. Alles, was wir tun müssen, ist , der XML-Deklaration den Parameter type namens hinzuzufügen. : D.

HINWEIS: Wenn Sie diesen Code verwenden, würde ich mich sehr über ein Shout-Out freuen. Es wird auch helfen, mehr Leute in die Community zu bringen :)

Nun, aber unsicher, was wir mit den Antworten hier anfangen sollen, da sie alle ihre Vor- und Nachteile hatten. Ich werde diejenigen aufrüsten, die ich für nützlich halte (keine Beleidigung für diejenigen, die es nicht waren) und dies abschließen, sobald ich den Repräsentanten habe :)

Interessantes Problem und viel Spaß zu lösen! :) :)

Rob Cooper
quelle
Ich bin vor einiger Zeit selbst auf dieses Problem gestoßen. Persönlich habe ich XmlSerializer verlassen und die IXmlSerializable-Schnittstelle direkt verwendet, da alle meine Klassen es trotzdem implementieren mussten. Ansonsten sind die Lösungen ziemlich ähnlich. Gutes Schreiben :)
Thorarin
Wir verwenden XML_-Eigenschaften, bei denen wir die Liste in Arrays konvertieren :)
Arcturus
2
Weil ein parameterloser Konstruktor benötigt wird, um die Klasse dynamisch zu instanziieren.
Silas Hansen
1
Hallo! Ich habe schon seit einiger Zeit nach einer solchen Lösung gesucht. Ich finde es genial! Obwohl ich nicht in der Lage bin herauszufinden, wie man es benutzt, würde es Ihnen etwas ausmachen, ein Beispiel zu geben? Serialisieren Sie Ihre Klasse oder die Liste mit Ihren Objekten?
Daniel
1
Schöner Code. Beachten Sie, dass der parameterlose Konstruktor deklariert werden kann privateoder protectederzwingen kann, dass er anderen Klassen nicht zur Verfügung steht.
Tcovo
9

Eine Sache, die Sie sich ansehen sollten, ist die Tatsache, dass Sie im XmlSerialiser-Konstruktor ein Array von Typen übergeben können, die der Serialiser möglicherweise nur schwer auflösen kann. Ich musste das einige Male verwenden, wenn eine Sammlung oder ein komplexer Satz von Datenstrukturen serialisiert werden musste und diese Typen in verschiedenen Assemblys usw. lebten.

XmlSerialiser-Konstruktor mit dem Parameter extraTypes

BEARBEITEN: Ich möchte hinzufügen, dass dieser Ansatz gegenüber XmlInclude-Attributen usw. den Vorteil hat, dass Sie einen Weg finden können, eine Liste Ihrer möglichen konkreten Typen zur Laufzeit zu ermitteln und zusammenzustellen und sie einzufügen.

Shaun Austin
quelle
Dies ist, was ich versuche zu tun, aber es ist nicht einfach, wie ich dachte: stackoverflow.com/questions/3897818/…
Luca
Dies ist ein sehr alter Beitrag, aber für alle, die dies wie wir implementieren möchten, beachten Sie bitte, dass der Konstruktor von XmlSerializer mit dem Parameter extraTypes die von ihm generierten Assemblys nicht zwischenspeichert . Dies kostet uns Wochen des Debuggens dieses Speicherverlusts. Wenn Sie also die zusätzlichen Typen mit dem Code der akzeptierten Antwort verwenden möchten, speichern Sie den Serializer im Cache . Dieses Verhalten ist hier dokumentiert: support.microsoft.com/en-us/kb/886385
Julien Lebot
3

Im Ernst, ein erweiterbares Framework von POCOs wird niemals zuverlässig in XML serialisiert. Ich sage das, weil ich garantieren kann, dass jemand mitkommt, Ihre Klasse erweitert und sie verpfuscht.

Sie sollten die Verwendung von XAML zum Serialisieren Ihrer Objektdiagramme in Betracht ziehen. Es wurde entwickelt, um dies zu tun, während die XML-Serialisierung dies nicht tut.

Der Xaml-Serializer und -Deserializer verarbeitet Generika problemlos, auch Sammlungen von Basisklassen und Schnittstellen (sofern die Sammlungen selbst IListoder implementieren IDictionary). Es gibt einige Einschränkungen, z. B. das Markieren Ihrer schreibgeschützten Sammlungseigenschaften mit dem DesignerSerializationAttribute, aber die Überarbeitung Ihres Codes zur Behandlung dieser Eckfälle ist nicht so schwierig.


quelle
Link scheint tot zu sein
bkribbs
Naja. Ich werde das bisschen nuklear machen. Viele andere Ressourcen zu diesem Thema.
2

Nur ein kurzes Update dazu habe ich nicht vergessen!

Ich mache nur noch ein paar Nachforschungen und sehe aus, als wäre ich auf dem Weg zu einem Gewinner. Ich muss nur den Code sortieren.

Bisher habe ich folgendes:

  • Der XmlSeralizer ist im Grunde eine Klasse, die die Klassen, die sie serialisiert, geschickt reflektiert. Es bestimmt die Eigenschaften, die basierend auf dem Typ serialisiert werden .
  • Die Ursache für das Problem tritt auf , weil ein Typenkonflikt auftritt, wird die erwartete Basetype aber in der Tat erhält die DerivedType .. Während Sie vielleicht denken, dass es es polymorph behandeln würde, tut es nicht , da es eine ganze zusätzliche Belastung bedeuten würde Reflexion und Typprüfung, für die es nicht vorgesehen ist.

Dieses Verhalten scheint überschrieben werden zu können (Code ausstehend), indem eine Proxy-Klasse erstellt wird, die als Vermittler für den Serializer fungiert. Dies bestimmt im Grunde den Typ der abgeleiteten Klasse und serialisiert diese dann wie gewohnt. Diese Proxy-Klasse speist dann dieses XML in die Zeile des Hauptserialisierers ein.

Schau dir diesen Raum an! ^ _ ^

Rob Cooper
quelle
2

Es ist sicherlich eine Lösung für Ihr Problem, aber es gibt ein anderes Problem, das Ihre Absicht, ein "portables" XML-Format zu verwenden, etwas untergräbt. Schlimme Dinge passieren, wenn Sie sich entscheiden, Klassen in der nächsten Version Ihres Programms zu ändern, und Sie beide Serialisierungsformate unterstützen müssen - das neue und das alte (weil Ihre Clients immer noch ihre alten Dateien / Datenbanken verwenden oder eine Verbindung zu ihnen herstellen Ihr Server verwendet die alte Version Ihres Produkts). Sie können diesen Serializer jedoch nicht mehr verwenden, da Sie ihn verwendet haben

type.AssemblyQualifiedName

das sieht aus wie

TopNamespace.SubNameSpace.ContainingClass+NestedClass, MyAssembly, Version=1.3.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089

das heißt, Ihre Assembly-Attribute und Version ...

Wenn Sie nun versuchen, Ihre Assembly-Version zu ändern, oder wenn Sie sie signieren möchten, funktioniert diese Deserialisierung nicht ...

Max Galkin
quelle
1

Ich habe ähnliche Dinge getan. Normalerweise stelle ich sicher, dass sich alle XML-Serialisierungsattribute in der konkreten Klasse befinden, und lasse nur die Eigenschaften dieser Klasse die Basisklassen aufrufen (falls erforderlich), um Informationen abzurufen, die beim Aufrufen des Serializers de / serialisiert werden diese Eigenschaften. Es ist ein bisschen mehr Codierungsarbeit, aber es funktioniert viel besser als der Versuch, den Serializer zu zwingen, genau das Richtige zu tun.

Der Schlumpf
quelle
1

Noch besser mit Notation:

[XmlRoot]
public class MyClass {
    public abstract class MyAbstract {} 
    public class MyInherited : MyAbstract {} 
    [XmlArray(), XmlArrayItem(typeof(MyInherited))] 
    public MyAbstract[] Items {get; set; } 
}
user2009677
quelle
2
Dies ist großartig, wenn Sie Ihre Klassen kennen, es ist die eleganteste Lösung. Wenn Sie neue geerbte Klassen von einer externen Quelle laden, können Sie diese leider nicht verwenden.
Vladimir