Sérialisation XML et types hérités

suite à ma question précédente j'ai travaillé à obtenir mon modèle d'objet à sérialiser en XML. Mais j'ai maintenant rencontré un problème (quelle surprise!).

le problème que j'ai est que j'ai une collection, qui est d'un type de classe de base abstraite, qui est peuplée par les types dérivés concrets.

j'ai pensé qu'il serait bien d'ajouter simplement les attributs XML pour toutes les classes et tout serait peachy. Malheureusement, ce n'est pas le cas!

donc j'ai fait quelques recherches sur Google et je comprends maintenant pourquoi ça ne marche pas. Dans ce le XmlSerializer fait en fait une réflexion intelligente afin de sérialiser les objets vers/depuis XML, et comme il est basé sur le type abstrait, il ne peut pas comprendre à quoi il parle . Fin.

je suis venu à travers cette page sur CodeProject, qui semble comme il peut très bien aider beaucoup (encore à lire/consommer pleinement), mais j'ai pensé que je voudrais apporter ce problème à la table StackOverflow trop, pour voir si vous avez des hacks/Trucs soignés afin de mettre en place et fonctionner de la manière la plus rapide/la plus légère possible.

une chose que je devrais également ajouter est que je ne pas veulent descendre la route XmlInclude . Il y a tout simplement trop de couplage avec elle, et cette partie du système est sous le développement lourd, il serait donc un véritable casse-tête de la maintenance!

81
demandé sur Community 2008-08-21 18:30:53

7 réponses

Problème Résolu!

OK, donc j'y suis finalement arrivé (il est vrai avec un lot d'aide de ici !).

en résumé:

Objectifs:

  • Je n'ai pas voulu suivre la route XmlInclude à cause des maux de tête de maintien.
  • une fois qu'une solution a été trouvée, je voulais qu'il soit rapide à mettre en œuvre dans d'autres applications.
  • des Collections de types abstraits peuvent être utilisées, ainsi que des propriétés abstraites individuelles.
  • Je ne voulais pas vraiment m'embêter à faire des choses" spéciales " dans les classes de béton.

questions soulevées / points à noter:

  • XmlSerializer fait assez cool de réflexion, mais il est très limité lorsqu'il s'agit de types abstraits (c'est-à-dire qu'il ne fonctionne qu'avec des instances du type abstrait lui-même, et non avec des sous-classes).
  • l'attribut Xml decorators définit comment XmlSerializer traite les propriétés de ses objets trouvés. Le type physique peut également être spécifié, mais cela crée un couplage serré entre la classe et le serializer (pas bon).
  • nous pouvons implémenter notre propre XmlSerializer en créant une classe qui implémente IXmlSerializable .

La Solution

j'ai créé une classe générique, dans laquelle vous indiquez le type générique comme le type abstrait, vous allez travailler avec. Cela donne à la classe la capacité de "traduire" entre le type abstrait et le type concret puisque nous pouvons coder dur le moulage (i.e. nous pouvons obtenir plus d'informations que le XmlSerializer peut).

j'ai ensuite mis en place le Ixmlserialisable interface, c'est assez simple, mais lors de la sérialisation, nous devons nous assurer d'écrire le type de la classe de béton au XML, de sorte que nous pouvons le jeter en arrière lors de la désérialisation. Il est également important de noter qu'il doit être pleinement qualifié car les ensembles dans lesquels les deux classes sont susceptibles de différer. Il y a bien sûr une petite vérification de type et des choses qui doivent se produire ici.

puisque le XmlSerializer ne peut pas cast, nous avons besoin de fournir le code pour le faire, alors l'opérateur implicite est alors surchargé (je ne savais même pas que vous pourriez le faire!).

le code de L'AbstractXmlSerializer est le suivant:

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
    }
}

donc, à partir de là, comment dire au XmlSerializer de travailler avec notre serializer plutôt que par défaut? Nous devons passer notre type dans la propriété Xml attributes type, par exemple:

[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>();
    }
}

ici vous pouvez voir, nous avons un collection et une seule propriété étant exposée, et tout ce que nous avons à faire est d'ajouter le paramètre type nommé à la déclaration Xml, facile! : D

NOTE: Si vous utilisez ce code, j'apprécierais vraiment un shout-out. Il sera également aider à conduire plus de gens à la communauté :)

maintenant, mais pas sûr de savoir quoi faire avec les réponses ici car ils ont tous eu leur pour et con. Je soulèverai ceux que je sens étaient utile (aucune offense à ceux qui ne l'étaient pas) et fermer cette une fois que j'ai le rep:)

problème intéressant et bon plaisir à résoudre! :)

53
répondu Rob Cooper 2009-06-12 07:42:23

une chose à regarder est le fait que dans le constructeur XmlSerialiser vous pouvez passer un tableau de types que le sérialiseur pourrait avoir de la difficulté à résoudre. J'ai dû l'utiliser Pas mal de fois où une collection ou un ensemble complexe d'infrastructures de données devait être sérialisé et ces types vivaient dans des assemblages différents, etc.

Xmlserialiserconstructor with extraTypes param

EDIT: je voudrais ajouter que ce approach a l'avantage sur xmlinclude attributs etc que vous pouvez élaborer un moyen de découvrir et de compiler une liste de vos types concrets possibles à l'exécution et de les remplir.

9
répondu Shaun Austin 2008-08-21 14:40:35

sérieusement, un cadre extensible de POCOs ne sera jamais sérialiser en XML de manière fiable. Je dis ça parce que je peux garantir que quelqu'un viendra, prolongera ta classe, et bâillonnera tout.

vous devriez regarder en utilisant XAML pour sérialiser vos graphiques d'objet. Il est conçu pour le faire, alors que la sérialisation XML ne l'est pas.

le serializer XAML et deserializer gère génériques sans problème, collections de classes de base et interfaces comme Eh bien (tant que les collections elles-mêmes mettent en œuvre IList ou IDictionary ). Il y a quelques mises en garde, comme marquer vos propriétés de collection en lecture seule avec le DesignerSerializationAttribute , mais retravailler votre code pour gérer ces cas de coin n'est pas si difficile.

3
répondu Will 2015-08-21 12:46:19

juste une mise à jour rapide, je n'ai pas oublié!

je fais juste quelques recherches, on dirait que je suis sur un gagnant, juste besoin d'avoir le code trié.

jusqu'à présent, j'ai le suivant:

  • le XmlSeralizer est fondamentalement une classe qui fait une réflexion fine sur les classes qu'elle sérialise. Il détermine les propriétés qui sont sérialisées en fonction du Type .
  • la raison pour laquelle le problème se produit est parce qu'une inadéquation de type se produit, il s'attend à la BaseType mais en fait reçoit le DerivedType .. Alors que vous pouvez penser qu'il serait traiter polymorphiquement, il ne le fait pas car il impliquerait une charge supplémentaire de réflexion et de vérification de type, ce qui n'est pas conçu pour le faire.

ce comportement semble pouvoir être outrepasser (code pending) en créant une classe proxy pour agir comme intermédiaire pour le serializer. Cela va essentiellement déterminer le type de la classe dérivée et ensuite sérialiser cela comme normal. Cette classe proxy acheminera alors ce XML à la ligne principale serializer..

regardez cet espace! ^_^

2
répondu Rob Cooper 2008-08-22 09:00:51

c'est certainement une solution à votre problème, mais il y a un autre problème, qui mine quelque peu votre intention d'utiliser le format XML" portable". Une mauvaise chose se produit lorsque vous décidez de changer de classe dans la prochaine version de votre programme et que vous devez prendre en charge les deux formats de sérialisation-le nouveau et l'ancien (parce que vos clients utilisent toujours leurs vieux fichiers/bases de données, ou ils se connectent à votre serveur en utilisant l'ancienne version de votre produit). Mais vous ne pouvez pas utiliser ce serializator plus maintenant, parce que vous avez utilisé

type.AssemblyQualifiedName

qui ressemble à

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

qui contient les attributs et la version de votre assemblage...

maintenant si vous essayez de changer votre version d'assemblage, ou si vous décidez de la signer, cette desérialisation ne va pas fonctionner...

2
répondu Max Galkin 2008-12-25 09:49:27

j'ai fait des choses similaires. Ce que je fais normalement, c'est de m'assurer que tous les attributs de sérialisation XML sont sur la classe concrete, et juste avoir les propriétés sur cet appel de classe jusqu'aux classes de base (si nécessaire) pour récupérer l'information qui sera dés/sérialisé quand le sérialiseur appelle sur ces propriétés. C'est un peu plus de travail de codage, mais il fonctionne beaucoup mieux que d'essayer de forcer le sérialiseur de faire la bonne chose.

1
répondu TheSmurf 2008-08-21 14:34:43

encore mieux, en utilisant la notation:

[XmlRoot]
public class MyClass {
    public abstract class MyAbstract {} 
    public class MyInherited : MyAbstract {} 
    [XmlArray(), XmlArrayItem(typeof(MyInherited))] 
    public MyAbstract[] Items {get; set; } 
}
1
répondu user2009677 2014-10-15 01:59:53