Sérialisation XML et types hérités

85

Suite à ma question précédente, j'ai travaillé sur la sérialisation de mon modèle objet vers 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 abstrait, qui est peuplée par les types dérivés concrets.

J'ai pensé que ce serait bien d'ajouter simplement les attributs XML à toutes les classes impliquées et que tout serait pêche. Malheureusement, ce n'est pas le cas!

J'ai donc fait quelques recherches sur Google et je comprends maintenant pourquoi cela ne fonctionne pas. En fait, le XmlSerializerfait en fait une réflexion intelligente afin de sérialiser des objets vers / depuis XML, et comme il est basé sur le type abstrait, il ne peut pas comprendre à quoi il parle . Bien.

Je suis tombé sur cette page sur CodeProject, qui semble que cela pourrait bien aider (encore à lire / consommer pleinement), mais je pensais que je voudrais également apporter ce problème à la table StackOverflow, pour voir si vous en avez hacks / astuces pour que cela soit opérationnel de la manière la plus rapide / la plus légère possible.

Une chose que je devrais également ajouter est que je ne veux PAS emprunter cette XmlIncludevoie. Il y a tout simplement trop de couplage avec celui-ci, et cette zone du système est en plein développement, ce serait donc un véritable casse-tête de maintenance!

Rob Cooper
la source
1
Il serait utile de voir quelques extraits de code pertinents extraits des classes que vous essayez de sérialiser.
Rex M
Mate: J'ai rouvert parce que je pense que d'autres personnes pourraient trouver cela utile, mais n'hésitez pas à fermer si vous n'êtes pas d'accord
JamesSugrue
Un peu confus par cela, puisqu'il n'y a rien eu sur ce fil depuis si longtemps?
Rob Cooper
Voici la réponse: stackoverflow.com/questions/6737666/…
Odys

Réponses:

54

Problème résolu!

OK, alors j'y suis enfin arrivé (certes avec beaucoup d'aide d' ici !).

Alors résumez:

Buts:

  • Je ne voulais pas emprunter la route XmlInclude à cause du mal de tête lié à la maintenance.
  • Une fois qu'une solution a été trouvée, je voulais qu'elle 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 me soucier de devoir faire des choses "spéciales" dans les classes concrètes.

Problèmes identifiés / points à noter:

  • XmlSerializer fait une réflexion assez cool, mais il est très limité en ce qui concerne les types abstraits (c'est-à-dire qu'il ne fonctionnera qu'avec des instances du type abstrait lui-même, pas des sous-classes).
  • Les décorateurs d'attributs Xml définissent la manière dont XmlSerializer traite les propriétés de ses recherches. Le type physique peut également être spécifié, mais cela crée un couplage étroit entre la classe et le sérialiseur (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 spécifiez le type générique comme type abstrait avec lequel vous travaillerez. Cela donne à la classe la possibilité de "traduire" entre le type abstrait et le type concret puisque nous pouvons coder en dur le casting (c'est-à-dire que nous pouvons obtenir plus d'informations que le XmlSerializer).

J'ai ensuite implémenté l' interface IXmlSerializable , c'est assez simple, mais lors de la sérialisation, nous devons nous assurer que nous écrivons le type de la classe concrète dans le XML, afin de pouvoir le renvoyer lors de la désérialisation. Il est également important de noter qu'il doit être entièrement qualifié car les assemblys dans lesquels se trouvent 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.

Étant donné que XmlSerializer ne peut pas effectuer de conversion, nous devons fournir le code pour le faire, de sorte que l'opérateur implicite est alors surchargé (je n'ai même jamais su que vous pouviez le faire!).

Le code pour 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
    }
}

Alors, à partir de là, comment dire au XmlSerializer de fonctionner avec notre sérialiseur plutôt qu'avec le sérialiseur par défaut? Nous devons passer notre type dans la propriété de type des attributs Xml, 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 une collection et une seule propriété exposée, et tout ce que nous avons à faire est d'ajouter le paramètre de type nommé à la déclaration Xml, facile! :RÉ

REMARQUE: Si vous utilisez ce code, j'apprécierais vraiment un cri. Cela aidera également à attirer plus de personnes dans la communauté :)

Maintenant, mais je ne sais pas quoi faire avec les réponses ici, car ils avaient tous leurs avantages et leurs inconvénients. Je vais améliorer ceux que je trouve utiles (sans offenser ceux qui ne le sont pas) et fermer cela une fois que j'ai le représentant :)

Problème intéressant et très amusant à résoudre! :)

Rob Cooper
la source
J'ai moi-même rencontré ce problème il y a quelque temps. Personnellement, j'ai fini par abandonner XmlSerializer et utiliser directement l'interface IXmlSerializable, puisque toutes mes classes devaient de toute façon l'implémenter. Sinon, les solutions sont assez similaires. Bonne rédaction cependant :)
Thorarin
Nous utilisons les propriétés XML_ où nous convertissons la liste en tableaux :)
Arcturus
2
Parce qu'un constructeur sans paramètre est nécessaire pour instancier dynamiquement la classe.
Silas Hansen
1
salut! Je cherchais une solution comme celle-ci depuis un certain temps maintenant. Je pense que c'est génial! Bien que je ne sache pas comment l'utiliser, pourriez-vous donner un exemple? Êtes-vous en train de sérialiser votre classe ou la liste contenant vos objets?
Daniel
1
Beau code. Notez que le constructeur sans paramètre peut être déclaré privateou protectedpour imposer qu'il ne soit pas disponible pour d'autres classes.
tcovo
9

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

Constructeur XmlSerialiser avec paramètre extraTypes

EDIT: J'ajouterais que cette approche a l'avantage sur les attributs XmlInclude, etc. que vous pouvez trouver un moyen de découvrir et de compiler une liste de vos types concrets possibles au moment de l'exécution et de les remplir.

Shaun Austin
la source
C'est ce que j'essaie de faire, mais ce n'est pas facile comme je le pensais: stackoverflow.com/questions/3897818/…
Luca
Il s'agit d'un article très ancien, mais pour quiconque cherche à implémenter cela comme nous l'avons fait, veuillez noter que le constructeur de XmlSerializer avec le paramètre extraTypes ne met pas en cache les assemblys qu'il génère à la volée. Cela nous coûte des semaines de débogage de cette fuite de mémoire. Donc, si vous devez utiliser les types supplémentaires avec le code de la réponse acceptée, mettez en cache le sérialiseur . Ce comportement est documenté ici: support.microsoft.com/en-us/kb/886385
Julien Lebot
3

Sérieusement, un cadre extensible de POCO ne sera jamais sérialisé en XML de manière fiable. Je dis cela parce que je peux garantir que quelqu'un viendra, prolongera votre cours et le bâclera.

Vous devriez envisager d'utiliser XAML pour sérialiser vos graphiques d'objets. Il est conçu pour cela, contrairement à la sérialisation XML.

Le sérialiseur et désérialiseur Xaml gère les génériques sans problème, ainsi que les collections de classes de base et d'interfaces (tant que les collections elles-mêmes implémentent IListou IDictionary). Il y a quelques mises en garde, telles que le marquage de vos propriétés de collection en lecture seule avec le DesignerSerializationAttribute, mais retravailler votre code pour gérer ces cas d'angle n'est pas si difficile.


la source
Le lien semble être mort
bkribbs
Tant pis. Je vais atomiser ce peu. Beaucoup d'autres ressources sur le sujet.
2

Juste une petite mise à jour à ce sujet, je n'ai pas oublié!

Je fais juste quelques recherches supplémentaires, on dirait que je suis sur un gagnant, juste besoin de faire trier le code.

Jusqu'à présent, j'ai ce qui suit:

  • Le XmlSeralizer est essentiellement une classe qui fait une réflexion astucieuse sur les classes qu'il sérialise. Il détermine les propriétés sérialisées en fonction du type .
  • La raison pour laquelle le problème se produit est qu'une incompatibilité de type se produit, il attend le BaseType mais reçoit en fait le DerivedType . Bien que vous puissiez penser qu'il le traiterait de manière polymorphe, ce n'est pas le cas car cela impliquerait une charge supplémentaire complète de réflexion et vérification de type, ce pour quoi il n’est pas conçu.

Ce comportement semble pouvoir être remplacé (code en attente) en créant une classe proxy qui servira d'intermédiaire pour le sérialiseur. Cela déterminera essentiellement le type de la classe dérivée, puis la sérialisera normalement. Cette classe proxy alimentera ensuite cette sauvegarde XML de la ligne vers le sérialiseur principal.

Surveillez cet endroit! ^ _ ^

Rob Cooper
la source
2

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 (car vos clients utilisent toujours leurs anciens fichiers / bases de données, ou ils se connectent à votre serveur utilisant l'ancienne version de votre produit). Mais vous ne pouvez plus utiliser ce sérialisateur, car vous avez utilisé

type.AssemblyQualifiedName

qui ressemble à

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

qui contient vos attributs d'assemblage et votre version ...

Maintenant, si vous essayez de changer la version de votre assembly, ou si vous décidez de la signer, cette désérialisation ne fonctionnera pas ...

Max Galkin
la source
1

J'ai fait des choses similaires. Ce que je fais normalement est de m'assurer que tous les attributs de sérialisation XML sont sur la classe concrète, et que les propriétés de cette classe appellent simplement les classes de base (si nécessaire) pour récupérer les informations qui seront dé / sérialisées lorsque le sérialiseur appelle ces propriétés. C'est un peu plus de travail de codage, mais cela fonctionne beaucoup mieux que d'essayer de forcer le sérialiseur à faire ce qu'il faut.

Le Schtroumpf
la source
1

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; } 
}
user2009677
la source
2
C'est génial si vous connaissez vos cours, c'est la solution la plus élégante. Si vous chargez de nouvelles classes héritées d'une source externe, vous ne pouvez malheureusement pas l'utiliser.
Vladimir