Serializzazione XML e tipi ereditati


85

In seguito alla mia domanda precedente, ho lavorato per ottenere la serializzazione del mio modello a oggetti in XML. Ma ora ho riscontrato un problema (quelle sorpresa!).

Il problema che ho è che ho una raccolta, che è di un tipo di classe di base astratta, che viene popolata dai tipi derivati ​​concreti.

Ho pensato che sarebbe andato bene aggiungere semplicemente gli attributi XML a tutte le classi coinvolte e tutto sarebbe stato peachy. Purtroppo, non è così!

Quindi ho scavato un po 'su Google e ora capisco perché non funziona. In questo l' XmlSerializerè in realtà facendo qualche riflessione intelligente per serializzare gli oggetti da / XML, e sin dalla sua base al tipo astratto, ma non riesco a capire cosa diavolo sta parlando . Bene.

Mi sono imbattuto in questa pagina su CodeProject, che sembra che possa aiutare molto (ancora da leggere / consumare completamente), ma ho pensato di portare questo problema anche alla tabella StackOverflow, per vedere se ne hai qualcosa di pulito trucchi / trucchi per farlo funzionare nel modo più veloce / leggero possibile.

Una cosa che dovrei anche aggiungere è che NON voglio scendere lungo il XmlIncludepercorso. C'è semplicemente troppo accoppiamento con esso, e quest'area del sistema è in forte sviluppo, quindi sarebbe un vero grattacapo di manutenzione!


1
Sarebbe utile vedere alcuni frammenti di codice pertinenti estratti dalle classi che stai tentando di serializzare.
Rex M

Mate: ho riaperto perché sento che altre persone potrebbero trovarlo utile, ma sentiti libero di chiudere se non sei d'accordo
JamesSugrue

Un po 'confuso da questo, dal momento che non c'è stato nulla su questo thread per così tanto tempo?
Rob Cooper,

Risposte:


54

Problema risolto!

OK, quindi finalmente ci sono arrivato (devo ammettere che con molto aiuto da qui !).

Quindi riassumi:

Obiettivi:

  • Non volevo scendere in XmlInclude percorso causa del mal di testa di manutenzione.
  • Una volta trovata una soluzione, volevo che fosse rapida da implementare in altre applicazioni.
  • È possibile utilizzare raccolte di tipi astratti, nonché singole proprietà astratte.
  • Non volevo davvero preoccuparmi di dover fare cose "speciali" nelle classi concrete.

Problemi identificati / punti da notare:

  • XmlSerializer fa alcune riflessioni piuttosto interessanti, ma è molto limitato quando si tratta di tipi astratti (cioè funzionerà solo con istanze del tipo astratto stesso, non con sottoclassi).
  • I decoratori dell'attributo Xml definiscono il modo in cui XmlSerializer tratta le proprietà trovate. È anche possibile specificare il tipo fisico, ma questo crea un accoppiamento stretto tra la classe e il serializzatore (non buono).
  • Possiamo implementare il nostro XmlSerializer creando una classe che implementa IXmlSerializable .

La soluzione

Ho creato una classe generica, in cui specifichi il tipo generico come tipo astratto con cui lavorerai. Questo dà alla classe la capacità di "tradurre" tra il tipo astratto e il tipo concreto poiché possiamo codificare il casting (cioè possiamo ottenere più informazioni di quante ne possa fare XmlSerializer).

Ho quindi implementato l' interfaccia IXmlSerializable , questo è abbastanza semplice, ma durante la serializzazione dobbiamo assicurarci di scrivere il tipo della classe concreta nell'XML, in modo da poterlo eseguire nuovamente durante la de-serializzazione. È anche importante notare che deve essere pienamente qualificato poiché è probabile che le assemblee in cui si trovano le due classi differiscano. Ovviamente c'è un piccolo controllo del tipo e cose che devono accadere qui.

Poiché XmlSerializer non può eseguire il cast, è necessario fornire il codice per farlo, quindi l'operatore implicito viene quindi sovraccaricato (non sapevo nemmeno che potessi farlo!).

Il codice per AbstractXmlSerializer è questo:

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

Quindi, da lì, come facciamo a dire a XmlSerializer di lavorare con il nostro serializzatore piuttosto che con quello predefinito? Dobbiamo passare il nostro tipo all'interno della proprietà del tipo di attributi Xml, ad esempio:

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

Qui puoi vedere che abbiamo una raccolta e una singola proprietà esposta e tutto ciò che dobbiamo fare è aggiungere il tipo parametro denominato di alla dichiarazione Xml, facile! : D

NOTA: se usi questo codice, apprezzerei davvero un grido. Aiuterà anche a guidare più persone verso la comunità :)

Ora, ma non sono sicuro di cosa fare con le risposte qui dato che avevano tutti i loro pro e contro. Modificherò quelli che ritengo utili (senza offesa per quelli che non lo erano) e lo chiuderò una volta che avrò il rappresentante :)

Problema interessante e buon divertimento da risolvere! :)


Mi sono imbattuto in questo problema qualche tempo fa. Personalmente, ho finito per abbandonare XmlSerializer e utilizzare direttamente l'interfaccia IXmlSerializable, poiché tutte le mie classi dovevano comunque implementarlo. Altrimenti, le soluzioni sono abbastanza simili. Buona recensione però :)
Thorarin

Usiamo proprietà XML_ dove convertiamo l'elenco in Arrays :)
Arcturus

2
Perché è necessario un costruttore senza parametri per istanziare dinamicamente la classe.
Silas Hansen

1
Ciao! Sto cercando una soluzione come questa da un po 'di tempo ormai. Penso sia geniale! Anche se non sono in grado di capire come usarlo, mi dispiacerebbe fare un esempio? Stai serializzando la tua classe o l'elenco contenente i tuoi oggetti?
Daniel

1
Bel codice. Si noti che il controllore senza parametri potrebbe essere dichiarato privateo protectedper imporre che non sia disponibile per altre classi.
tcovo

9

Una cosa da considerare è il fatto che nel costruttore XmlSerialiser è possibile passare un array di tipi che il serializzatore potrebbe avere difficoltà a risolvere. Ho dovuto usarlo un paio di volte in cui una raccolta o un insieme complesso di strutture di dati doveva essere serializzato e quei tipi vivevano in assembly diversi ecc.

Costruttore XmlSerialiser con extraTypes param

EDIT: aggiungerei che questo approccio ha il vantaggio sugli attributi XmlInclude ecc. Che puoi trovare un modo per scoprire e compilare un elenco dei tuoi possibili tipi concreti in fase di esecuzione e inserirli.


Questo è quello che sto cercando di fare, ma non è facile come pensavo: stackoverflow.com/questions/3897818/…
Luca

Questo è un post molto vecchio, ma per chiunque voglia implementarlo come abbiamo fatto noi, tieni presente che il costruttore di XmlSerializer con il parametro extraTypes non memorizza nella cache gli assembly che genera al volo. Questo ci costa settimane di debug di quella perdita di memoria. Quindi, se devi utilizzare i tipi extra con il codice della risposta accettata, memorizza nella cache il serializzatore . Questo comportamento è documentato qui: support.microsoft.com/en-us/kb/886385
Julien Lebot,

3

Seriamente, un framework estensibile di POCO non verrà mai serializzato in XML in modo affidabile. Dico questo perché posso garantire che qualcuno verrà, estenderà la tua classe e rovinerà tutto.

Dovresti cercare di usare XAML per serializzare i tuoi grafici a oggetti. È progettato per fare ciò, mentre la serializzazione XML non lo è.

Il serializzatore e deserializzatore Xaml gestisce i generici senza problemi, le raccolte di classi di base e anche le interfacce (purché le raccolte stesse implementino IListo IDictionary). Ci sono alcuni avvertimenti, come contrassegnare le proprietà della raccolta di sola lettura con DesignerSerializationAttribute, ma rielaborare il codice per gestire questi casi d'angolo non è così difficile.


Link sembra essere morto
bkribbs

Oh bene. Io bombarderò quel pezzo. Molte altre risorse sull'argomento.

2

Solo un rapido aggiornamento su questo, non ho dimenticato!

Sto solo facendo qualche ricerca in più, sembra che io sia su un vincitore, ho solo bisogno di ordinare il codice.

Finora ho quanto segue:

  • Il XmlSeralizer è fondamentalmente una classe che fa qualche riflessione nifty sulle classi è serializzazione. Determina le proprietà serializzate in base al tipo .
  • Il motivo per cui si verifica il problema è perché si sta verificando una mancata corrispondenza del tipo, si aspetta il BaseType ma in realtà riceve il DerivedType .. Anche se potresti pensare che lo tratterebbe polimorficamente, non lo fa poiché comporterebbe un intero carico extra di riflessione e controllo del tipo, cosa che non è progettata per fare.

Questo comportamento sembra essere in grado di essere sovrascritto (codice in sospeso) creando una classe proxy che funga da intermediario per il serializzatore. Questo fondamentalmente determinerà il tipo della classe derivata e quindi lo serializzerà normalmente. Questa classe proxy quindi alimenterà quell'XML di backup della linea al serializzatore principale.

Guarda questo spazio! ^ _ ^


2

È certamente una soluzione al tuo problema, ma c'è un altro problema, che in qualche modo mina la tua intenzione di utilizzare il formato XML "portabile". La cosa negativa accade quando decidi di cambiare classe nella prossima versione del tuo programma e devi supportare entrambi i formati di serializzazione - quello nuovo e quello vecchio (perché i tuoi clienti usano ancora i loro vecchi file / database, o si connettono a il tuo server utilizzando la vecchia versione del tuo prodotto). Ma non puoi più usare questo serializzatore, perché hai usato

type.AssemblyQualifiedName

che assomiglia

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

che contiene gli attributi e la versione dell'assembly ...

Ora se provi a cambiare la versione dell'assembly o decidi di firmarla, questa deserializzazione non funzionerà ...


1

Ho fatto cose simili a questa. Quello che faccio normalmente è assicurarmi che tutti gli attributi di serializzazione XML siano sulla classe concreta e che le proprietà di quella classe chiamino le classi base (dove richiesto) per recuperare le informazioni che verranno de / serializzate quando il serializzatore chiama quelle proprietà. È un po 'più di lavoro di codifica, ma funziona molto meglio che tentare di forzare il serializzatore a fare la cosa giusta.


1

Ancora meglio, usando la notazione:

[XmlRoot]
public class MyClass {
    public abstract class MyAbstract {} 
    public class MyInherited : MyAbstract {} 
    [XmlArray(), XmlArrayItem(typeof(MyInherited))] 
    public MyAbstract[] Items {get; set; } 
}

2
È fantastico se conosci le tue classi, è la soluzione più elegante. Se carichi nuove classi ereditate da una fonte esterna, sfortunatamente non puoi usarle.
Vladimir
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.