84 votes

Sérialisation XML et types hérités

Suite à ma question précédente, j'ai travaillé sur la sérialisation de mon modèle d'objet en XML. Mais je rencontre maintenant 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 remplie par les types dérivés concrets.

Je pensais que ce serait bon d'ajouter simplement les attributs XML à toutes les classes concernées et que tout se passerait bien. Malheureusement, ce n'est pas le cas!

Donc j'ai fouillé sur Google et je comprends maintenant pourquoi cela ne fonctionne pas. En fait, le XmlSerializer réalise en fait une réflexion astucieuse pour sérialiser des objets vers/depuis XML, et puisqu'il est basé sur le type abstrait, il ne peut pas comprendre à qui il parle. D'accord.

J'ai trouvé cette page sur CodeProject, qui semble pouvoir beaucoup aider (je dois encore la lire/la comprendre pleinement), mais je voulais également soulever ce problème à la communauté de StackOverflow pour voir si vous avez des astuces/trucs pour le faire fonctionner de la manière la plus rapide/légère possible.

Une chose que je dois également ajouter est que je ne veux PAS suivre la voie de XmlInclude. Il y a simplement trop de couplage avec cela, et cette partie du système est en plein développement, donc ce serait un véritable casse-tête en termes de maintenance!

54voto

Rob Cooper Points 15945

Problème Résolu!

D'accord, j'y suis enfin arrivé (admettons-le avec beaucoup d'aide de ici!).

Donc, pour résumer :

Objectifs :

  • Je ne voulais pas suivre la voie de XmlInclude en raison du casse-tête de maintenance.
  • Une fois une solution 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 donner la peine de faire des choses "spéciales" dans les classes concrètes.

Problèmes Identifiés/Points à Noter :

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

J'ai ensuite implémenté l'interface IXmlSerializable, cela est assez simple, mais lors de la sérialisation, nous devons nous assurer d'écrire le type de la classe concrète dans le XML, afin que nous puissions le caster lorsque nous désérialisons. Il est également important de noter qu'il doit être entièrement qualifié car les assemblies dans lesquelles se trouvent les deux classes sont susceptibles de différer. Bien sûr, il y a un peu de vérification de type et des choses qui doivent se produire ici.

Puisque XmlSerializer ne peut pas caster, nous devons fournir le code pour le faire, donc l'opérateur implicite est ensuite surchargé (je ne savais même pas que vous pouviez faire ça!).

Le code pour l'AbstractXmlSerializer est le suivant :

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

namespace Utility.Xml
{
    public class AbstractXmlSerializer : IXmlSerializable
    {
        // Remplace les conversions implicites, car le XmlSerializer
        // casting vers/depuis les types requis implicitement.
        public static implicit operator AbstractType(AbstractXmlSerializer o)
        {
            return o.Data;
        }

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

        private AbstractType _data;
        /// 
        /// Données [concrètes] à stocker/stockées en tant que XML.
        /// 
        public AbstractType Data
        {
            get { return _data; }
            set { _data = value; }
        }

        /// 
        /// **NE PAS UTILISER** Ceci est uniquement ajouté pour permettre la serialisation XML.
        /// 
        /// NE PAS UTILISER CE CONSTRUCTEUR
        public AbstractXmlSerializer()
        {
            // Constructeur par défaut (Requis pour la serialisation Xml - NE PAS UTILISER)
        }

        /// 
        /// Initialise le sérialiseur pour fonctionner avec les données fournies.
        /// 
        /// Objet concret du type abstrait spécifié.
        public AbstractXmlSerializer(AbstractType data)
        {
            _data = data;
        }

        #region Membres IXmlSerializable

        public System.Xml.Schema.XmlSchema GetSchema()
        {
            return null; // c'est bon car le schéma est inconnu.
        }

        public void ReadXml(System.Xml.XmlReader reader)
        {
            // Caster les données en arrière du type abstrait.
            string typeAttrib = reader.GetAttribute("type");

            // Assurer que le type a été spécifié
            if (typeAttrib == null)
                throw new ArgumentNullException("Impossible de lire les données Xml pour le type abstrait '" + typeof(AbstractType).Name +
                    "' car aucun attribut 'type' n'a été spécifié dans le XML.");

            Type type = Type.GetType(typeAttrib);

            // Vérifier si le type est trouvé.
            if (type == null)
                throw new InvalidCastException("Impossible de lire les données Xml pour le type abstrait '" + typeof(AbstractType).Name +
                    "' car le type spécifié dans le XML n'a pas été trouvé.");

            // Vérifier que le type est une sous-classe du type abstrait.
            if (!type.IsSubclassOf(typeof(AbstractType)))
                throw new InvalidCastException("Impossible de lire les données Xml pour le type abstrait '" + typeof(AbstractType).Name +
                    "' car le type spécifié dans le XML diffère ('" + type.Name + "').");

            // Lire les données, désérialiser en fonction du type concret (maintenant connu).
            reader.ReadStartElement();
            this.Data = (AbstractType)new
                XmlSerializer(type).Deserialize(reader);
            reader.ReadEndElement();
        }

        public void WriteXml(System.Xml.XmlWriter writer)
        {
            // Écrire le nom du type dans l'élément XML en tant qu'attribut et serialiser
            Type type = _data.GetType();

            // Correctif de bug : L'assembly doit être un FQN puisque les types peuvent/étaient externes à l'actuel.
            writer.WriteAttributeString("type", type.AssemblyQualifiedName);
            new XmlSerializer(type).Serialize(writer, _data);
        }

        #endregion
    }
}

Alors, à partir de là, comment dire au XmlSerializer de travailler avec notre sérialiseur plutôt qu'avec le défaut ? Nous devons passer notre type dans la propriété nommée type des attributs Xml, par exemple :

[XmlRoot("ClasseAvecCollectionAbstraite")]
public class ClasseAvecCollectionAbstraite
{
    private List _list;
    [XmlArray("ElementsDeListe")]
    [XmlArrayItem("ElementDeListe", Type = typeof(AbstractXmlSerializer))]
    public List Liste
    {
        get { return _list; }
        set { _list = value; }
    }

    private AbstractType _prop;
    [XmlElement("MaPropriété", Type=typeof(AbstractXmlSerializer))]
    public AbstractType MaPropriété
    {
        get { return _prop; }
        set { _prop = value; }
    }

    public ClasseAvecCollectionAbstraite()
    {
        _list = new List();
    }
}

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 nommé type à la déclaration Xml, facile ! :D

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

Maintenant, mais incertain de ce qu'il faut faire avec les réponses ici car elles avaient toutes leurs avantages et inconvénients. Je vais upvoter ceux que je trouve utiles (sans offenser ceux qui ne le sont pas) et fermer cela une fois que j'aurai la réputation :)

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

9voto

Shaun Austin Points 2512

Une chose à prendre en compte est que dans le constructeur XmlSerialiser, vous pouvez passer un tableau de types que le sérialiseur pourrait avoir du mal à résoudre. J'ai dû utiliser cela plusieurs fois lorsque qu'il fallait sérialiser une collection ou un ensemble de structures de données complexes et que ces types vivaient dans différentes assemblées, etc.

Constructeur XmlSerialiser avec le paramètre extraTypes

EDIT : J'ajouterais que cette approche présente l'avantage par rapport aux attributs XmlInclude, etc. que vous pouvez trouver un moyen de découvrir et compiler une liste de vos types concrets possibles à l'exécution et les insérer.

3voto

Will Points 76760

Sérieusement, un cadre extensible de POCOs ne se sérialisera jamais de manière fiable en XML. Je dis cela parce que je peux garantir que quelqu'un viendra, étendra votre classe et la gâchera.

Vous devriez envisager d'utiliser XAML pour sérialiser vos graphiques d'objet. Il est conçu pour cela, tandis que la sérialisation XML ne l'est pas.

Le sérialiseur et le désérialiseur Xaml gèrent sans problème les types génériques, les collections de classes de base et les interfaces également (tant que les collections elles-mêmes implémentent IList ou IDictionary). Il y a quelques limitations, comme marquer vos propriétés de collection en lecture seule avec l'attribut DesignerSerializationAttribute, mais retravailler votre code pour gérer ces cas spécifiques n'est pas si difficile.

2voto

Rob Cooper Points 15945

Juste une petite mise à jour sur cela, je n'ai pas oublié!

Je fais juste un peu plus de recherche, il semble que je sois sur la bonne voie, il me suffit juste de trier le code.

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

  • L'XmlSeralizer est essentiellement une classe qui effectue une réflexion astucieuse sur les classes qu'elle sérialise. Elle 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 qu'il y a une incompatibilité de types, elle attend le BaseType mais reçoit en réalité le DerivedType .. Même si vous pourriez penser qu'il le traiterait polymorphiquement, ce n'est pas le cas car cela impliquerait une charge supplémentaire de réflexion et de vérification des types, pour lequel il n'est pas conçu.

Ce comportement semble pouvoir être contourné (code en attente) en créant une classe proxy pour agir comme intermédiaire pour le sérialiseur. Cette

2voto

Max Galkin Points 10116

C'est certainement une solution à votre problème, mais il y a un autre problème, qui mine quelque peu votre intention d'utiliser un format XML "portable". Le problème survient lorsque vous décidez de changer les classes 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 anciens fichiers/bases de données, ou qu'ils se connectent à votre serveur en utilisant une ancienne version de votre produit). Mais vous ne pouvez plus utiliser ce sérialiseur, 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'assembly et de version...

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X