serializar objeto c# xml
Serialización XML de la propiedad de la interfaz (9)
Me gustaría XML serializar un objeto que tiene (entre otros) una propiedad de tipo IModelObject (que es una interfaz).
public class Example
{
public IModelObject Model { get; set; }
}
Cuando trato de serializar un objeto de esta clase, recibo el siguiente error:
"No se puede serializar el miembro Example.Model de tipo Example porque es una interfaz".
Entiendo que el problema es que una interfaz no se puede serializar. Sin embargo, el tipo de objeto de modelo concreto es desconocido hasta el tiempo de ejecución.
Reemplazar la interfaz IModelObject con un tipo abstracto o concreto y usar herencia con XMLInclude es posible, pero parece una solución fea.
¿Alguna sugerencia?
Reemplazar la interfaz IModelObject con un tipo abstracto o concreto y usar herencia con XMLInclude es posible, pero parece una solución fea.
Si es posible usar una base abstracta, recomendaría esa ruta. Todavía será más limpio que usar la serialización enrollada a mano. El único problema que veo con la base abstracta es que todavía vas a necesitar el tipo concreto? Al menos así es como lo he usado en el pasado, algo así como:
public abstract class IHaveSomething
{
public abstract string Something { get; set; }
}
public class MySomething : IHaveSomething
{
string _sometext;
public override string Something
{ get { return _sometext; } set { _sometext = value; } }
}
[XmlRoot("abc")]
public class seriaized
{
[XmlElement("item", typeof(MySomething))]
public IHaveSomething data;
}
Desgraciadamente para mí, tuve un caso en el que la clase que se serializaría tenía propiedades que también tenían interfaces como propiedades, por lo que necesitaba procesar de forma recursiva cada propiedad. Además, algunas de las propiedades de la interfaz estaban marcadas como [XmlIgnore], así que quería omitirlas. Tomé ideas que encontré en este hilo y le agregué algunas cosas para que sea recursivo. Solo el código de deserialización se muestra aquí:
void main()
{
var serializer = GetDataContractSerializer<MyObjectWithCascadingInterfaces>();
using (FileStream stream = new FileStream(xmlPath, FileMode.Open))
{
XmlDictionaryReader reader = XmlDictionaryReader.CreateTextReader(stream, new XmlDictionaryReaderQuotas());
var obj = (MyObjectWithCascadingInterfaces)serializer.ReadObject(reader);
// your code here
}
}
DataContractSerializer GetDataContractSerializer<T>() where T : new()
{
Type[] types = GetTypesForInterfaces<T>();
// Filter out duplicates
Type[] result = types.ToList().Distinct().ToList().ToArray();
var obj = new T();
return new DataContractSerializer(obj.GetType(), types);
}
Type[] GetTypesForInterfaces<T>() where T : new()
{
return GetTypesForInterfaces(typeof(T));
}
Type[] GetTypesForInterfaces(Type T)
{
Type[] result = new Type[0];
var obj = Activator.CreateInstance(T);
// get the type for all interface properties that are not marked as "XmlIgnore"
Type[] types = T.GetProperties()
.Where(p => p.PropertyType.IsInterface &&
!p.GetCustomAttributes(typeof(System.Xml.Serialization.XmlIgnoreAttribute), false).Any())
.Select(p => p.GetValue(obj, null).GetType())
.ToArray();
result = result.ToList().Concat(types.ToList()).ToArray();
// do the same for each of the types identified
foreach (Type t in types)
{
Type[] embeddedTypes = GetTypesForInterfaces(t);
result = result.ToList().Concat(embeddedTypes.ToList()).ToArray();
}
return result;
}
Esto es simplemente una limitación inherente de serialización declarativa donde la información de tipo no está incorporada en el resultado.
Al intentar convertir <Flibble Foo="10" />
nuevo en
public class Flibble { public object Foo { get; set; } }
¿Cómo sabe el serializador si debe ser una int, una cadena, un doble (o algo más) ...
Para que esto funcione, tiene varias opciones, pero si realmente no sabe hasta el tiempo de ejecución, la manera más fácil de hacerlo es usar XmlAttributeOverrides .
Lamentablemente, esto solo funcionará con las clases base, no con las interfaces. Lo mejor que puede hacer es ignorar la propiedad que no es suficiente para sus necesidades.
Si realmente debe permanecer con las interfaces, tiene tres opciones reales:
Ocultarlo y tratarlo en otra propiedad
Placa de caldera fea, desagradable y mucha repetición, pero la mayoría de los consumidores de la clase no tendrán que lidiar con el problema:
[XmlIgnore()]
public object Foo { get; set; }
[XmlElement("Foo")]
[EditorVisibile(EditorVisibility.Advanced)]
public string FooSerialized
{
get { /* code here to convert any type in Foo to string */ }
set { /* code to parse out serialized value and make Foo an instance of the proper type*/ }
}
Es probable que se convierta en una pesadilla de mantenimiento ...
Implementar IXmlSerializable
Similar a la primera opción en que tomas el control total de las cosas, pero
- Pros
- No tienes propiedades desagradables ''falsas'' dando vueltas.
- puede interactuar directamente con la estructura xml agregando flexibilidad / control de versiones
- Contras
- puede terminar teniendo que volver a implementar la rueda para todas las demás propiedades de la clase
Los problemas de duplicación de esfuerzos son similares a los primeros.
Modifique su propiedad para usar un tipo de envoltura
public sealed class XmlAnything<T> : IXmlSerializable
{
public XmlAnything() {}
public XmlAnything(T t) { this.Value = t;}
public T Value {get; set;}
public void WriteXml (XmlWriter writer)
{
if (Value == null)
{
writer.WriteAttributeString("type", "null");
return;
}
Type type = this.Value.GetType();
XmlSerializer serializer = new XmlSerializer(type);
writer.WriteAttributeString("type", type.AssemblyQualifiedName);
serializer.Serialize(writer, this.Value);
}
public void ReadXml(XmlReader reader)
{
if(!reader.HasAttributes)
throw new FormatException("expected a type attribute!");
string type = reader.GetAttribute("type");
reader.Read(); // consume the value
if (type == "null")
return;// leave T at default value
XmlSerializer serializer = new XmlSerializer(Type.GetType(type));
this.Value = (T)serializer.Deserialize(reader);
reader.ReadEndElement();
}
public XmlSchema GetSchema() { return(null); }
}
Usar esto implicaría algo así como (en el proyecto P):
public namespace P
{
public interface IFoo {}
public class RealFoo : IFoo { public int X; }
public class OtherFoo : IFoo { public double X; }
public class Flibble
{
public XmlAnything<IFoo> Foo;
}
public static void Main(string[] args)
{
var x = new Flibble();
x.Foo = new XmlAnything<IFoo>(new RealFoo());
var s = new XmlSerializer(typeof(Flibble));
var sw = new StringWriter();
s.Serialize(sw, x);
Console.WriteLine(sw);
}
}
que te da:
<?xml version="1.0" encoding="utf-16"?>
<MainClass
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Foo type="P.RealFoo, P, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<RealFoo>
<X>0</X>
</RealFoo>
</Foo>
</MainClass>
Esto es obviamente más engorroso para los usuarios de la clase, aunque evita mucho la placa de la caldera.
Un medio feliz puede estar fusionando la idea XmlAnything en la propiedad de ''respaldo'' de la primera técnica. De esta forma, la mayor parte del trabajo gruñón se realiza para usted, pero los consumidores de la clase no sufren ningún impacto más allá de la confusión con la introspección.
He encontrado una solución más simple (no necesita DataContractSerializer), gracias a este blog aquí: serialización XML de tipos derivados cuando el tipo base está en otro espacio de nombres o DLL
Pero pueden surgir 2 problemas en esta implementación:
(1) ¿Qué sucede si DerivedBase no está en el espacio de nombres de la clase Base, o incluso peor en un proyecto que depende del espacio de nombres Base, por lo que Base no puede incluir XMLInclude DerivedBase?
(2) ¿Qué pasa si solo tenemos la clase Base como dll, entonces otra vez Base no puede incluir XMLInclude DerivedBase?
Hasta ahora, ...
Entonces la solución a los 2 problemas es mediante el uso de XmlSerializer Constructor (Type, array []) :
XmlSerializer ser = new XmlSerializer(typeof(A), new Type[]{ typeof(DerivedBase)});
Aquí se proporciona un ejemplo detallado en MSDN: XmlSerializer Constructor (Type, extraTypesArray [])
Me parece que para DataContracts o Soap XMLs, necesita verificar el XmlRoot como se menciona aquí en esta pregunta SO .
Una respuesta similar está aquí en SO, pero no está marcada como una sola, ya que no parece que OP ya la haya considerado.
La solución a esto es usar la reflexión con DataContractSerializer. Ni siquiera tiene que marcar su clase con [DataContract] o [DataMember]. Se serializará cualquier objeto, independientemente de si tiene propiedades de tipo de interfaz (incluidos diccionarios) en xml. Aquí hay un método de extensión simple que serializará cualquier objeto en XML, incluso si tiene interfaces (tenga en cuenta que podría modificar esto para que también se ejecute de forma recursiva).
public static XElement ToXML(this object o)
{
Type t = o.GetType();
Type[] extraTypes = t.GetProperties()
.Where(p => p.PropertyType.IsInterface)
.Select(p => p.GetValue(o, null).GetType())
.ToArray();
DataContractSerializer serializer = new DataContractSerializer(t, extraTypes);
StringWriter sw = new StringWriter();
XmlTextWriter xw = new XmlTextWriter(sw);
serializer.WriteObject(xw, o);
return XElement.Parse(sw.ToString());
}
lo que hace la expresión LINQ es que enumera cada propiedad, devuelve cada propiedad que es una interfaz, obtiene el valor de esa propiedad (el objeto subyacente), obtiene el tipo de ese objeto concreto que pone en una matriz, y lo agrega al serializador lista de tipos conocidos
Ahora el serializador sabe qué tipo de serialización tiene para que pueda hacer su trabajo.
Lamentablemente, no hay una respuesta simple, ya que el serializador no sabe qué serializar para una interfaz. Encontré una explicación más completa sobre cómo solucionar esto en MSDN
Puede usar ExtendedXmlSerializer . Esta serialización admite la serialización de la propiedad de la interfaz sin ningún truco.
var serializer = new ConfigurationContainer().UseOptimizedNamespaces().Create();
var obj = new Example
{
Model = new Model { Name = "name" }
};
var xml = serializer.Serialize(obj);
Tu xml se verá así:
<?xml version="1.0" encoding="utf-8"?>
<Example xmlns:exs="https://extendedxmlserializer.github.io/v2" xmlns="clr-namespace:ExtendedXmlSerializer.Samples.Simple;assembly=ExtendedXmlSerializer.Samples">
<Model exs:type="Model">
<Name>name</Name>
</Model>
</Example>
ExtendedXmlSerializer admite .net 4.5 y .net Core.
Si conoce los implementadores de su interfaz por adelantado, puede usar un truco bastante simple para hacer que su tipo de interfaz se serialice sin escribir ningún código de análisis:
public interface IInterface {}
public class KnownImplementor01 : IInterface {}
public class KnownImplementor02 : IInterface {}
public class KnownImplementor03 : IInterface {}
public class ToSerialize {
[XmlIgnore]
public IInterface InterfaceProperty { get; set; }
[XmlArray("interface")]
[XmlArrayItem("ofTypeKnownImplementor01", typeof(KnownImplementor01)]
[XmlArrayItem("ofTypeKnownImplementor02", typeof(KnownImplementor02)]
[XmlArrayItem("ofTypeKnownImplementor03", typeof(KnownImplementor03)]
public object[] InterfacePropertySerialization {
get { return new[] { InterfaceProperty } }
set { InterfaceProperty = (IInterface)value.Single(); }
}
}
El xml resultante debe parecer algo similar a
<interface><ofTypeKnownImplementor01><!-- etc... -->
en mi proyecto, tengo un
List <IFormatStyle> FormatStyleTemplates;
que contiene diferentes tipos.
A continuación, uso la solución ''XmlAnything'' de arriba para serializar esta lista de diferentes tipos. El xml generado es hermoso.
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
[XmlArray("FormatStyleTemplates")]
[XmlArrayItem("FormatStyle")]
public XmlAnything<IFormatStyle>[] FormatStyleTemplatesXML
{
get
{
return FormatStyleTemplates.Select(t => new XmlAnything<IFormatStyle>(t)).ToArray();
}
set
{
// read the values back into some new object or whatever
m_FormatStyleTemplates = new FormatStyleProvider(null, true);
value.ForEach(t => m_FormatStyleTemplates.Add(t.Value));
}
}