86 Stimmen

XML-Serialisierung und vererbte Typen

Im Anschluss an meine vorherige Frage Ich habe auf immer mein Objektmodell in XML zu serialisieren gearbeitet. Aber ich habe jetzt in ein Problem laufen (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 bevölkert wird.

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

Ich habe also ein wenig bei Google recherchiert und verstehe jetzt warum Es funktioniert nicht. In diesem die XmlSerializer tut in der Tat einige clevere Reflexion, um Objekte in/aus XML zu serialisieren, und da es auf dem abstrakten Typ basiert, kann es nicht herausfinden, was zur Hölle es spricht mit . Gut.

Ich stieß auf diese Seite auf CodeProject, die aussieht, wie es gut eine Menge helfen kann (noch zu lesen/verzehren vollständig), aber ich dachte, ich möchte dieses Problem auf den StackOverflow-Tisch zu bringen, um zu sehen, wenn Sie alle ordentlichen Hacks/Tricks haben, um dies in die schnellste/leichteste Weise möglich zu erhalten.

Eine Sache, die ich noch hinzufügen sollte, ist, dass ich NICHT wollen die XmlInclude Route. Es ist einfach zu viel damit gekoppelt, und dieser Bereich des Systems wird intensiv weiterentwickelt, so dass die Wartung ein echtes Problem darstellen würde!

57voto

Rob Cooper Punkte 28132

Das Problem ist gelöst!

OK, ich habe es also endlich geschafft (zugegebenermaßen mit einem Los der Hilfe von aquí !).

Fassen Sie also zusammen:

Ziele:

  • Ich wollte nicht in die XmlInclude Route wegen der Wartungsprobleme.
  • Sobald eine Lösung gefunden war, wollte ich, dass sie schnell in andere Anwendungen implementiert werden kann.
  • Es können Sammlungen abstrakter Typen verwendet werden, aber auch einzelne abstrakte Eigenschaften.
  • Ich wollte mir nicht die Mühe machen, in den konkreten Klassen "besondere" Dinge zu tun.

Identifizierte Probleme/zu beachtende Punkte:

  • XmlSerializer einige ziemlich coole Reflexionen, aber es ist sehr begrenzt, wenn es um abstrakte Typen geht (d.h. 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 auch angegeben werden, aber das erzeugt eine enge Kopplung zwischen der Klasse und dem Serialisierer (nicht gut).
  • Wir können unseren eigenen XmlSerializer implementieren, indem wir eine Klasse erstellen, die Folgendes implementiert IXmlSerializable .

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 kodieren können (d.h. wir können mehr Informationen erhalten, als der XmlSerializer kann).

Ich habe dann die IXmlSerializable Schnittstelle ist dies ziemlich einfach, aber bei der Serialisierung müssen wir sicherstellen, dass wir den Typ der konkreten Klasse in die XML-Datei schreiben, damit wir ihn bei der De-Serialisierung zurückschreiben können. Es ist auch wichtig zu beachten, dass es sich um voll qualifiziert da die Versammlungen, in denen sich die beiden Klassen befinden, wahrscheinlich unterschiedlich sind. Es gibt natürlich eine kleine Typüberprüfung und andere Dinge, die hier passieren müssen.

Da der XmlSerializer nicht casten kann, müssen wir den Code dafür bereitstellen, so dass der implizite Operator dann überladen wird (ich wusste nicht einmal, dass man das tun kann!).

Der Code für den AbstractXmlSerializer lautet wie folgt:

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 also dem XmlSerializer mitteilen, dass er mit unserem Serializer und nicht mit dem Standard arbeiten soll? Wir müssen unseren Typ in der Eigenschaft type der Xml-Attribute ü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 wir eine Sammlung und eine einzelne Eigenschaft offenlegen, und alles, was wir tun müssen, ist das Hinzufügen der Typ benannten Parameter an die Xml-Deklaration an, ganz einfach! :D

HINWEIS: Wenn Sie diesen Code verwenden, würde ich mich sehr über einen Hinweis freuen. Es wird auch dazu beitragen, dass mehr Menschen die Community besuchen :)

Nun, ich weiß nicht, was ich mit den Antworten anfangen soll, da sie alle ihre Vor- und Nachteile haben. Ich werde die, die ich für nützlich halte, hochladen (nichts gegen die, die es nicht waren) und die Seite schließen, sobald ich die Antworten habe :)

Interessantes Problem und viel Spaß bei der Lösung! :)

9voto

Shaun Austin Punkte 3697

Eine Sache, die man sich ansehen sollte, ist die Tatsache, dass man im XmlSerialiser-Konstruktor ein Array von Typen übergeben kann, mit denen der Serialisierer möglicherweise Schwierigkeiten hat, sie aufzulösen. Ich musste das schon einige Male verwenden, wenn eine Sammlung oder ein komplexer Satz von Datenstrukturen serialisiert werden musste und diese Typen in verschiedenen Assemblies usw. vorkamen.

XmlSerialiser-Konstruktor mit extraTypes-Parameter

EDIT: Ich würde hinzufügen, dass dieser Ansatz den Vorteil über XmlInclude-Attribute usw. hat, dass Sie einen Weg zur Erkennung und Kompilierung einer Liste Ihrer möglichen konkreten Typen zur Laufzeit ausarbeiten können und stopfen sie in.

3voto

Im Ernst, ein erweiterbarer Rahmen von POCOs wird niemals zuverlässig nach XML serialisiert werden. Ich sage das, weil ich garantieren kann, dass jemand daherkommt, Ihre Klasse erweitert und es verpfuscht.

Sie sollten die Verwendung von XAML für die Serialisierung Ihrer Objektgraphen in Betracht ziehen. Es ist dafür ausgelegt, während die XML-Serialisierung nicht dafür geeignet ist.

Der Xaml-Serialisierer und -Deserialisierer verarbeitet Generika problemlos, ebenso Sammlungen von Basisklassen und Schnittstellen (solange die Sammlungen selbst die IList o IDictionary ). Es gibt einige Vorbehalte, wie z.B. das Markieren von schreibgeschützten Sammlungseigenschaften mit der Option DesignerSerializationAttribute aber es ist nicht schwer, den Code so zu überarbeiten, dass er auch in diesen Eckfällen funktioniert.

2voto

Rob Cooper Punkte 28132

Nur ein kurzes Update zu diesem Thema, ich habe es nicht vergessen!

Ich stelle gerade weitere Nachforschungen an, und es sieht so aus, als ob ich auf der Gewinnerseite stehe, ich muss nur noch den Code herausfinden.

Bislang habe ich Folgendes:

  • El XmlSeralizer ist im Grunde eine Klasse, die einige raffinierte Überlegungen zu den Klassen anstellt, die sie serialisiert. Sie bestimmt die Eigenschaften, die serialisiert werden, basierend auf den Typ .
  • Der Grund für das Problem ist, dass eine Fehlanpassung des Typs auftritt, d.h. es wird erwartet, dass die BaseType erhält aber in Wirklichkeit die AbgeleiteterTyp .. Während Sie vielleicht denken, dass es polymorph behandeln würde, tut es das nicht, da es eine ganze Menge zusätzlicher Reflexion und Typüberprüfung erfordern würde, wofür es nicht ausgelegt ist.

Dieses Verhalten scheint durch die Erstellung einer Proxyklasse, die als Vermittler für den Serializer fungiert, außer Kraft gesetzt werden zu können (Code steht noch aus). Diese bestimmt den Typ der abgeleiteten Klasse und serialisiert diese dann wie üblich. Diese Proxy-Klasse wird dann füttern, dass XML zurück bis die Zeile zum Haupt serializer

Beobachten Sie diese Stelle! ^_^

2voto

Max Galkin Punkte 16522

Es ist sicherlich eine Lösung für Ihr Problem, aber es gibt ein weiteres Problem, das Ihre Absicht, ein "portables" XML-Format zu verwenden, etwas untergräbt. Schlimmes passiert, 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 Kunden noch ihre alten Dateien/Datenbanken verwenden oder sie sich mit einer alten Version Ihres Produkts mit Ihrem Server verbinden). Aber Sie können diesen Serialisierer nicht mehr verwenden, weil Sie

type.AssemblyQualifiedName

die wie folgt aussieht

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

die Ihre Assembly-Attribute und Version enthält...

Wenn Sie nun versuchen, Ihre Assembly-Version zu ändern, oder Sie beschließen, sie zu signieren, wird diese Deserialisierung nicht funktionieren...

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X