Serialización XML de la propiedad de la interfaz

83

Me gustaría serializar XML un objeto que tiene (entre otras) una propiedad de tipo IModelObject (que es una interfaz).

public class Example
{
    public IModelObject Model { get; set; }
}

Cuando intento serializar un objeto de esta clase, recibo el siguiente error:
"No se puede serializar el miembro Example. Modelo de tipo Example porque es una interfaz".

Entiendo que el problema es que no se puede serializar una interfaz. Sin embargo, el tipo de objeto Model concreto se desconoce hasta el tiempo de ejecución.

Es posible reemplazar la interfaz IModelObject con un tipo abstracto o concreto y usar la herencia con XMLInclude, pero parece una mala solución.

¿Alguna sugerencia?

Elad
fuente

Respuestas:

116

Esto es simplemente una limitación inherente de la serialización declarativa donde la información de tipo no está incrustada en la salida.

Al intentar convertir de <Flibble Foo="10" />nuevo en

public class Flibble { public object Foo { get; set; } }

¿Cómo sabe el serializador si debe ser un 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, es probable que la forma más fácil de hacerlo sea utilizando XmlAttributeOverrides .

Lamentablemente, esto solo funcionará con clases base, no interfaces. Lo mejor que puede hacer es ignorar la propiedad que no es suficiente para sus necesidades.

Si realmente debes quedarte con las interfaces, tienes tres opciones reales:

Escóndelo y trátalo 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 esto se convierta en una pesadilla de mantenimiento ...

Implementar IXmlSerializable

Similar a la primera opción en que usted toma el control total de las cosas pero

  • Pros
    • No tienes desagradables propiedades "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 al primero.

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

Obviamente, esto es más engorroso para los usuarios de la clase, aunque evita mucha placa de caldera.

Un medio feliz puede ser fusionar la idea de XmlAnything en la propiedad de 'respaldo' de la primera técnica. De esta manera, la mayor parte del trabajo pesado está hecho por usted, pero los consumidores de la clase no sufren ningún impacto más allá de la confusión con la introspección.

ShuggyCoUk
fuente
Traté de implementar sus propiedades de envoltura de brujas de enfoque, pero desafortunadamente tengo un problema :( ¿Podría echar un vistazo a esta publicación, por favor: stackoverflow.com/questions/7584922/…
SOReader
¿Existe alguna propiedad de introducción artística de FooSerialized?
Gqqnbig
42

La solución a esto es utilizar la reflexión con DataContractSerializer. Ni siquiera tienes que marcar tu clase con [DataContract] o [DataMember]. 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 puede modificar esto para que se ejecute de forma recursiva también).

    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 enumerar 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, lo coloca en una matriz y lo agrega al serializador lista de tipos conocidos.

Ahora el serializador sabe qué hay de los tipos que serializa para que pueda hacer su trabajo.

Despertar
fuente
Muy elegante y fácil solución al problema. ¡Gracias!
Ghlouw
2
Esto no parece funcionar para una IList y una interfaz genéricas. por ejemplo, IList <IMyInterface>. El valor concreate para IMyInterface debe agregarse a KnownTypes, sin embargo, en su lugar, se agregará IList <IMyInterface>.
galford13x
6
@ galford13x Traté de hacer este ejemplo lo más simple posible sin dejar de demostrar el punto. Agregar siempre un solo caso, como recursividad o tipos de interfaz, hace que sea menos claro de leer y quita el punto principal. Siéntase libre de agregar cualquier verificación adicional para extraer los tipos conocidos necesarios. Para ser honesto, no creo que haya nada que no puedas conseguir usando la reflexión. Esto, por ejemplo, obtendrá el tipo de parámetro genérico, stackoverflow.com/questions/557340/…
Despertar
Entiendo, solo mencioné esto porque la pregunta pedía la serialización de la interfaz. Pensé que les haría saber a otros que el error se esperaría sin modificaciones para evitar que se golpeen la cabeza. Sin embargo, aprecié su código, ya que agregué el atributo [KnownType ()] y su código me llevó al resultado.
galford13x
1
¿Hay alguna forma de omitir el nombre al serializar? Intenté usar xmlwriterSettings usando un xmlwriter en su lugar, uso la sobrecarga donde puedo pasar los tipos adicionales, pero no está funcionando ...
Leyendas
9

Si conoce los implementadores de su interfaz desde el principio, hay un truco bastante simple que puede usar para 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 tener un aspecto similar al de

 <interface><ofTypeKnownImplementor01><!-- etc... -->
hannasm
fuente
1
Muy útil, gracias. En la mayoría de situaciones, conozco las clases que implementan la interfaz. Esta respuesta debería estar más arriba en mi opinión.
Jonás
Ésta es la solución más sencilla. ¡Gracias!
mKay
8

Puede utilizar ExtendedXmlSerializer . Este serializador 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);

Su 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.

Wojtpl2
fuente
3

Es posible reemplazar la interfaz IModelObject con un tipo abstracto o concreto y usar la herencia con XMLInclude, pero parece una solución desagradable.

Si es posible utilizar una base abstracta, recomendaría esa ruta. Aún 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 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;
}
csharptest.net
fuente
2

Desafortunadamente, 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

MattH
fuente
1

Desafortunadamente para mí, tuve un caso en el que la clase a serializar tenía propiedades que también tenían interfaces como propiedades, por lo que necesitaba procesar recursivamente 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 hacerlo recursivo. Aquí solo se muestra el código de deserialización:

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;
}
acordner
fuente
1

He encontrado una solución más simple (no necesita el DataContractSerializer), gracias a este blog aquí: XML serializando 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é pasa 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, entonces Base no puede XMLInclude DerivedBase?

(2) ¿Qué pasa si solo tenemos la clase Base como una dll, entonces de nuevo Base no puede XMLInclude DerivedBase

Hasta ahora, ...

Entonces, la solución a los 2 problemas es usar 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 XML, debe verificar XmlRoot como se menciona aquí en esta pregunta SO .

Una respuesta similar está aquí en SO, pero no está marcada como una, ya que no parece que el OP ya la haya considerado.

B Carlos H
fuente
0

en mi proyecto, tengo una
List <IFormatStyle> FormatStyleTemplates;
que contiene diferentes tipos.

Luego 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));
        }
    }
Detlef Kroll
fuente