Serialización XML y tipos heredados

85

Siguiendo con mi pregunta anterior , he estado trabajando para que mi modelo de objetos se serialice en XML. Pero ahora me he encontrado con un problema (¡quelle sorpresa!).

El problema que tengo es que tengo una colección, que es de un tipo de clase base abstracta, que está poblada por los tipos derivados concretos.

Pensé que estaría bien simplemente agregar los atributos XML a todas las clases involucradas y todo sería perfecto. Lamentablemente, ¡ese no es el caso!

Así que he investigado un poco en Google y ahora entiendo por qué no funciona. En ese el XmlSerializeres, de hecho, haciendo una reflexión inteligente con el fin de serializar objetos a / desde XML, y desde su base en el tipo abstracto, no puede averiguar qué diablos está hablando . Multa.

Me encontré con esta página en CodeProject, que parece que puede ayudar mucho (aún para leer / consumir por completo), pero pensé que también me gustaría llevar este problema a la tabla StackOverflow, para ver si tiene algo ordenado hacks / trucos para que esto funcione de la manera más rápida / ligera posible.

Una cosa que también debo agregar es que NO quiero seguir la XmlIncluderuta. Simplemente hay demasiado acoplamiento con él, y esta área del sistema está en un gran desarrollo, por lo que sería un verdadero dolor de cabeza de mantenimiento.

Rob Cooper
fuente
1
Sería útil ver algunos fragmentos de código relevantes extraídos de las clases que está intentando serializar.
Rex M
Mate: Reabrí porque creo que otras personas podrían encontrar esto útil, pero siéntete libre de cerrar si no estás de acuerdo
JamesSugrue
¿Un poco confundido por esto, ya que no ha habido nada en este hilo durante tanto tiempo?
Rob Cooper
Ahí está la respuesta: stackoverflow.com/questions/6737666/…
Odys

Respuestas:

54

¡Problema resuelto!

Bien, finalmente llegué allí (¡es cierto que con mucha ayuda de aquí !).

Así que resuma:

Metas:

  • No quería seguir la ruta XmlInclude debido al dolor de cabeza de mantenimiento.
  • Una vez que se encontró una solución, quería que se implementara rápidamente en otras aplicaciones.
  • Se pueden utilizar colecciones de tipos abstractos, así como propiedades abstractas individuales.
  • Realmente no quería molestarme en tener que hacer cosas "especiales" en las clases concretas.

Problemas identificados / puntos a tener en cuenta:

  • XmlSerializer hace una reflexión bastante interesante, pero es muy limitado cuando se trata de tipos abstractos (es decir, solo funcionará con instancias del tipo abstracto en sí, no con subclases).
  • Los decoradores de atributos Xml definen cómo XmlSerializer trata las propiedades que encuentra. El tipo físico también se puede especificar, pero esto crea un acoplamiento estrecho entre la clase y el serializador (no es bueno).
  • Podemos implementar nuestro propio XmlSerializer creando una clase que implemente IXmlSerializable .

La solución

Creé una clase genérica, en la que especificas el tipo genérico como el tipo abstracto con el que trabajarás. Esto le da a la clase la capacidad de "traducir" entre el tipo abstracto y el tipo concreto, ya que podemos codificar la conversión (es decir, podemos obtener más información de la que puede obtener el XmlSerializer).

Luego implementé la interfaz IXmlSerializable , esto es bastante sencillo, pero al serializar debemos asegurarnos de escribir el tipo de la clase concreta en el XML, para que podamos devolverlo al deserializar. También es importante tener en cuenta que debe estar completamente calificado ya que es probable que los ensamblados en los que se encuentran las dos clases difieran. Por supuesto, hay una pequeña verificación de tipos y cosas que deben suceder aquí.

Dado que XmlSerializer no puede emitir, necesitamos proporcionar el código para hacer eso, por lo que el operador implícito se sobrecarga (¡ni siquiera sabía que podía hacer esto!).

El código para AbstractXmlSerializer es este:

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

Entonces, a partir de ahí, ¿cómo le decimos al XmlSerializer que funcione con nuestro serializador en lugar del predeterminado? Debemos pasar nuestro tipo dentro de la propiedad de tipo de atributos Xml, por ejemplo:

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

Aquí puede ver, tenemos una colección y una sola propiedad expuesta, y todo lo que tenemos que hacer es agregar el parámetro con nombre de tipo a la declaración Xml, ¡fácil! :RE

NOTA: Si usa este código, le agradecería mucho un reconocimiento. También ayudará a atraer a más personas a la comunidad :)

Ahora, pero no estoy seguro de qué hacer con las respuestas aquí, ya que todos tenían sus pros y sus contras. Mejoraré los que considero útiles (sin ofender a los que no lo sean) y cerraré esto una vez que tenga la reputación :)

¡Problema interesante y divertido de resolver! :)

Rob Cooper
fuente
Me encontré con este problema hace algún tiempo. Personalmente, terminé abandonando XmlSerializer y usando la interfaz IXmlSerializable directamente, ya que todas mis clases necesitaban implementarlo de todos modos. De lo contrario, las soluciones son bastante similares. Sin embargo, una buena
reseña
Usamos propiedades XML_ donde convertimos la lista a Arrays :)
Arcturus
2
Porque se necesita un constructor sin parámetros para instanciar dinámicamente la clase.
Silas Hansen
1
¡Hola! He estado buscando una solución como esta desde hace bastante tiempo. ¡Creo que es brillante! Aunque no puedo entender cómo usarlo, ¿le importaría dar un ejemplo? ¿Está serializando su clase o la lista que contiene sus objetos?
Daniel
1
Buen código. Tenga en cuenta que el constructor sin parámetros podría declararse privateo protectedhacer cumplir que no esté disponible para otras clases.
tcovo
9

Una cosa a tener en cuenta es el hecho de que en el constructor XmlSerialiser puede pasar una serie de tipos que el serializador podría tener dificultades para resolver. Tuve que usar eso varias veces cuando una colección o un conjunto complejo de estructuras de datos necesitaban ser serializados y esos tipos vivían en diferentes ensamblajes, etc.

Constructor XmlSerialiser con extraTypes param

EDITAR: Yo agregaría que este enfoque tiene el beneficio sobre los atributos XmlInclude, etc. de que puede encontrar una forma de descubrir y compilar una lista de sus posibles tipos concretos en tiempo de ejecución y rellenarlos.

Shaun Austin
fuente
Esto es lo que estoy tratando de hacer, pero no es fácil como estaba pensando: stackoverflow.com/questions/3897818/…
Luca
Esta es una publicación muy antigua, pero para cualquiera que busque implementar esto como lo hicimos nosotros, tenga en cuenta que el constructor de XmlSerializer con extraTypes param no almacena en caché los ensamblados que genera sobre la marcha. Esto nos cuesta semanas depurar esa pérdida de memoria. Entonces, si va a usar los tipos adicionales con el código de respuesta aceptado, almacene en caché el serializador . Este comportamiento se documenta aquí: support.microsoft.com/en-us/kb/886385
Julien Lebot
3

En serio, un marco extensible de POCO nunca se serializará en XML de manera confiable. Digo esto porque puedo garantizar que alguien vendrá, extenderá su clase y lo arruinará.

Debería considerar el uso de XAML para serializar sus gráficos de objetos. Está diseñado para hacer esto, mientras que la serialización XML no lo es.

El serializador y deserializador Xaml maneja genéricos sin problemas, colecciones de clases base e interfaces también (siempre que las propias colecciones implementen IListo IDictionary). Hay algunas advertencias, como marcar las propiedades de la colección de solo lectura con el DesignerSerializationAttribute, pero reelaborar su código para manejar estos casos de esquina no es tan difícil.


fuente
Link parece estar muerto
bkribbs
Oh bien. Atacaré esa parte. Muchos otros recursos sobre el tema.
2

Solo una actualización rápida sobre esto, ¡no lo he olvidado!

Solo estoy investigando un poco más, parece que estoy en un ganador, solo necesito ordenar el código.

Hasta ahora, tengo lo siguiente:

  • El XmlSeralizer es básicamente una clase que hace una reflexión ingeniosa en las clases se serializar. Determina las propiedades que se serializan según el tipo .
  • La razón se produce el problema se debe a un tipo de desajuste se produce, se esperaba que el BaseType pero en realidad recibe el DerivedType .. Mientras que usted puede pensar que sería tratarlo polimórfica, no es así ya que implicaría toda una carga extra de reflexión y verificación de tipos, para lo cual no está diseñado.

Este comportamiento parece poder anularse (código pendiente) creando una clase de proxy para actuar como intermediario para el serializador. Esto básicamente determinará el tipo de clase derivada y luego lo serializará como de costumbre. Esta clase de proxy luego alimentará ese XML de respaldo de la línea al serializador principal.

¡Mira este espacio! ^ _ ^

Rob Cooper
fuente
2

Ciertamente es una solución a su problema, pero hay otro problema, que de alguna manera socava su intención de utilizar el formato XML "portátil". Sucede algo malo cuando decide cambiar las clases en la próxima versión de su programa y necesita admitir ambos formatos de serialización, el nuevo y el anterior (porque sus clientes todavía usan sus archivos / bases de datos antiguos, o se conectan a su servidor usando la versión anterior de su producto). Pero ya no puede usar este serializador, porque usó

type.AssemblyQualifiedName

que parece

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

que contiene sus atributos de ensamblaje y versión ...

Ahora bien, si intenta cambiar su versión de ensamblado, o decide firmarlo, esta deserialización no va a funcionar ...

Max Galkin
fuente
1

He hecho cosas similares a esto. Lo que normalmente hago es asegurarme de que todos los atributos de serialización XML estén en la clase concreta, y solo hacer que las propiedades de esa clase llamen a las clases base (donde sea necesario) para recuperar información que se eliminará / serializará cuando el serializador llame a esas propiedades. Es un poco más de trabajo de codificación, pero funciona mucho mejor que intentar forzar al serializador a hacer lo correcto.

El Pitufo
fuente
1

Aún mejor, usando la notación:

[XmlRoot]
public class MyClass {
    public abstract class MyAbstract {} 
    public class MyInherited : MyAbstract {} 
    [XmlArray(), XmlArrayItem(typeof(MyInherited))] 
    public MyAbstract[] Items {get; set; } 
}
usuario2009677
fuente
2
Esto es genial si conoces tus clases, es la solución más elegante. Si carga nuevas clases heredadas de una fuente externa, lamentablemente no podrá usarla.
Vladimir