Serialización XML y tipos heredados

Después de mi pregunta anterior , he estado trabajando para que mi modelo de objetos se serialice en XML. Pero ahora me encontré con un problema (¡qué 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 agregar los atributos XML a todas las clases involucradas y todo sería color de rosa. Lamentablemente, ese no es el caso!

Así que investigué un poco en Google y ahora entiendo por qué no está funcionando. En este sentido, XmlSerializer está haciendo una reflexión inteligente para serializar objetos a / desde XML, y dado que está basado en el tipo abstracto, no puede entender con qué diablos está hablando . Multa.

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

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

¡Problema resuelto!

OK, finalmente llegué allí (¡con toda la ayuda de aquí !).

Entonces resum:

Metas:

  • No quería ir por la ruta XmlInclude debido al dolor de cabeza de mantenimiento.
  • Una vez que se encontró una solución, quería que fuera rápida de implementar en otras aplicaciones.
  • Se pueden usar colecciones de tipos abstractos, así como propiedades abstractas individuales.
  • Realmente no quería molestarme con tener que hacer cosas “especiales” en las clases concretas.

Problemas / puntos identificados a tener en cuenta:

  • XmlSerializer hace una reflexión muy buena, pero es muy limitada cuando se trata de tipos abstractos (es decir, solo funcionará con instancias del tipo abstracto en sí mismo, no de subclases).
  • Los decoradores de atributo Xml definen cómo el 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 especifica el tipo genérico como el tipo abstracto con el que trabajará. Esto le da a la clase la capacidad de “traducir” entre el tipo abstracto y el tipo concreto, ya que podemos codificar el casting (es decir, podemos obtener más información de la que XmlSerializer puede).

Luego implementé la interfaz IXmlSerializable , esto es bastante sencillo, pero cuando serialicemos debemos asegurarnos de escribir el tipo de la clase concreta en el XML, para poder devolverlo al deserializar. También es importante tener en cuenta que debe estar totalmente calificado ya que los conjuntos en los que se encuentran las dos clases probablemente difieran. Por supuesto, hay un pequeño control de tipo y cosas que deben suceder aquí.

Como XmlSerializer no puede transmitir, necesitamos proporcionar el código para hacerlo, por lo que el operador implícito está sobrecargado (¡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 : IXmlSerializable { // Override the Implicit Conversions Since the XmlSerializer // Casts to/from the required types implicitly. 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; ///  /// [Concrete] Data to be stored/is stored as XML. ///  public AbstractType Data { get { return _data; } set { _data = value; } } ///  /// **DO NOT USE** This is only added to enable XML Serialization. ///  /// DO NOT USE THIS CONSTRUCTOR public AbstractXmlSerializer() { // Default Ctor (Required for Xml Serialization - DO NOT USE) } ///  /// Initialises the Serializer to work with the given data. ///  /// Concrete Object of the AbstractType Specified. 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, desde allí, ¿cómo le decimos al XmlSerializer que trabaje 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 _list; [XmlArray("ListItems")] [XmlArrayItem("ListItem", Type = typeof(AbstractXmlSerializer))] public List List { get { return _list; } set { _list = value; } } private AbstractType _prop; [XmlElement("MyProperty", Type=typeof(AbstractXmlSerializer))] public AbstractType MyProperty { get { return _prop; } set { _prop = value; } } public ClassWithAbstractCollection() { _list = new List(); } } 

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

NOTA: Si usa este código, realmente agradecería un mensaje de agradecimiento. También ayudará a llevar 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 contras. Actualizaré aquellos que considero útiles (sin ofender a aquellos que no lo fueron) y cerraré esto una vez que tenga el representante 🙂

¡Problema interesante y divertido de resolver! 🙂

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

XmlSerialiser Constructor con extraTypes param

EDITAR: Yo añadiría que este enfoque tiene el beneficio sobre los atributos de XmlInclude, etc. que puede encontrar una manera de descubrir y comstackr una lista de sus posibles tipos de concreto en tiempo de ejecución y rellenarlos.

En serio, un marco extensible de POCO nunca se serializará en XML de manera confiable. Digo esto porque puedo garantizar que alguien vendrá, ampliará tu 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 el deserializador Xaml manejan generics sin problemas, colecciones de clases base e interfaces también (siempre que las colecciones mismas implementen IList o IDictionary ). Hay algunas advertencias, como marcar sus propiedades de colección de solo lectura con DesignerSerializationAttribute , pero no es tan difícil modificar el código para manejar estos casos de esquina.

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

Solo haciendo un poco más de investigación, parece que estoy listo para un ganador, solo necesito ordenar el código.

Hasta ahora, tengo lo siguiente:

  • El XmlSeralizer es básicamente una clase que hace una buena reflexión sobre las clases que está serializando. Determina las propiedades que se serializan en función del tipo .
  • La razón por la que ocurre el problema es porque se está produciendo un desajuste de tipo, está esperando el BaseType pero de hecho recibe el DerivedType … Si bien puede pensar que lo trataría de forma polimórfica, no lo hace, ya que implicaría una carga adicional de reflexión y verificación de tipo, que no está diseñado para hacer.

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

¡Mira este espacio! ^ _ ^

Sin duda, es una solución a su problema, pero hay otro problema, que en cierto modo socava su intención de utilizar el formato XML “portátil”. Lo malo sucede cuando decides cambiar clases en la próxima versión de tu progtwig y necesitas admitir ambos formatos de serialización, el nuevo y el antiguo (porque tus clientes todavía usan tus viejos archivos o bases de datos, o se conectan a su servidor usando la versión anterior de su producto). Pero ya no puedes usar este serializador, porque usaste

 type.AssemblyQualifiedName 

que se ve como

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

eso contiene los atributos y la versión de su ensamble …

Ahora, si intentas cambiar tu versión de ensamblaje o decides firmarla, esta deserialización no funcionará …

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 tengo las propiedades de esa clase en las clases base (cuando sea necesario) para recuperar información que será de / serializada cuando el serializador invoca esas propiedades. Es un poco más de trabajo de encoding, pero funciona mucho mejor que intentar obligar al serializador a hacer lo correcto.

Mejor aún, 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; } }