web-dev-qa-db-fra.com

Sérialisation XML de la propriété d'interface

Je voudrais sérialiser XML un objet qui a (entre autres) une propriété de type IModelObject (qui est une interface).

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

Lorsque j'essaie de sérialiser un objet de cette classe, je reçois l'erreur suivante:
"Impossible de sérialiser le membre Example.Model de type Example car il s'agit d'une interface."

Je comprends que le problème est qu'une interface ne peut pas être sérialisée. Cependant, le type d'objet concret Modèle est inconnu jusqu'à l'exécution.

Remplacer l'interface IModelObject par un type abstrait ou concret et utiliser l'héritage avec XMLInclude est possible, mais semble être une solution de contournement laide.

Aucune suggestion?

78
Elad

Il s'agit simplement d'une limitation inhérente à la sérialisation déclarative où les informations de type ne sont pas intégrées dans la sortie.

En essayant de convertir <Flibble Foo="10" /> retour dans

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

Comment le sérialiseur sait-il s'il doit s'agir d'un entier, d'une chaîne, d'un double (ou autre chose) ...

Pour que cela fonctionne, vous avez plusieurs options, mais si vous ne savez vraiment pas jusqu'à l'exécution, la manière la plus simple de le faire est probablement d'utiliser XmlAttributeOverrides .

Malheureusement, cela ne fonctionnera qu'avec les classes de base, pas les interfaces. Le mieux que vous puissiez y faire est d'ignorer la propriété qui ne suffit pas à vos besoins.

Si vous devez vraiment rester avec des interfaces, vous avez trois options réelles:

Cachez-le et traitez-le dans une autre propriété

Plaque de chaudière laide et désagréable et beaucoup de répétitions mais la plupart des consommateurs de la classe n'auront pas à faire face au problème:

[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*/ } 
}

Cela risque de devenir un cauchemar de maintenance ...

Implémenter IXmlSerializable

Semblable à la première option en ce que vous prenez le contrôle total des choses, mais

  • Avantages
    • Vous n'avez pas de "fausses" propriétés désagréables qui traînent.
    • vous pouvez interagir directement avec la structure xml en ajoutant flexibilité/versioning
  • Les inconvénients
    • vous pourriez devoir réimplémenter la roue pour toutes les autres propriétés de la classe

Les problèmes de duplication des efforts sont similaires au premier.

Modifiez votre propriété pour utiliser un type d'habillage

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

Son utilisation impliquerait quelque chose comme (dans le projet 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);
    }
}

ce qui vous donne:

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

Ceci est évidemment plus lourd pour les utilisateurs de la classe mais évite beaucoup de plaque de chaudière.

Un juste milieu peut être de fusionner l'idée XmlAnything dans la propriété "backing" de la première technique. De cette façon, la plupart du travail de grognement est fait pour vous, mais les consommateurs de la classe ne subissent aucun impact au-delà de la confusion avec l'introspection.

111
ShuggyCoUk

La solution consiste à utiliser la réflexion avec DataContractSerializer. Vous n'avez même pas à marquer votre classe avec [DataContract] ou [DataMember]. Il sérialisera n'importe quel objet, qu'il ait ou non des propriétés de type d'interface (y compris des dictionnaires) en XML. Voici une méthode d'extension simple qui sérialisera n'importe quel objet en XML même s'il a des interfaces (notez que vous pouvez également l'ajuster pour l'exécuter récursivement).

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

ce que fait l'expression LINQ, elle énumère chaque propriété, retourne chaque propriété qui est une interface, obtient la valeur de cette propriété (l'objet sous-jacent), obtient le type de cet objet concret la place dans un tableau et l'ajoute au sérialiseur liste des types connus.

Le sérialiseur sait maintenant quels types il sérialise afin de pouvoir faire son travail.

40
Despertar

Vous pouvez utiliser ExtendedXmlSerializer . Ce sérialiseur prend en charge la sérialisation de la propriété d'interface sans aucune astuce.

var serializer = new ConfigurationContainer().UseOptimizedNamespaces().Create();

var obj = new Example
                {
                    Model = new Model { Name = "name" }
                };

var xml = serializer.Serialize(obj);

Votre xml ressemblera à:

<?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 prend en charge .net 4.5 et .net Core.

7
Wojtpl2

Si vous connaissez vos implémenteurs d'interface à l'avance, il existe un hack assez simple que vous pouvez utiliser pour obtenir la sérialisation de votre type d'interface sans écrire de code d'analyse:

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

Le xml résultant devrait ressembler à quelque chose comme

 <interface><ofTypeKnownImplementor01><!-- etc... -->
5
hannasm

Remplacer l'interface IModelObject par un type abstrait ou concret et utiliser l'héritage avec XMLInclude est possible, mais semble être une solution de contournement laide.

S'il est possible d'utiliser une base abstraite, je recommanderais cette route. Il sera toujours plus propre que l'utilisation de la sérialisation roulée à la main. Le seul problème que je vois avec la base abstraite est que vous aurez toujours besoin du type de béton? C'est du moins ainsi que je l'ai utilisé dans le passé, quelque chose comme:

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;
}
3
csharptest.net

Malheureusement, il n'y a pas de réponse simple, car le sérialiseur ne sait pas quoi sérialiser pour une interface. J'ai trouvé une explication plus complète sur la façon de contourner cela sur MSDN

2
MattH

Malheureusement pour moi, j'ai eu un cas où la classe à sérialiser avait des propriétés qui avaient aussi des interfaces en tant que propriétés, donc j'avais besoin de traiter récursivement chaque propriété. De plus, certaines des propriétés de l'interface étaient marquées comme [XmlIgnore], donc je voulais les ignorer. J'ai pris des idées que j'ai trouvées sur ce fil et y ai ajouté des choses pour le rendre récursif. Seul le code de désérialisation est affiché ici:

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

dans mon projet, j'ai un
Liste <IFormatStyle> FormatStyleTemplates;
contenant différents types.

J'utilise ensuite la solution 'XmlAnything' d'en haut, pour sérialiser cette liste de différents types. Le xml généré est magnifique.

    [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));
        }
    }
0
Detlef Kroll

J'ai trouvé une solution plus simple (vous n'avez pas besoin du DataContractSerializer), grâce à ce blog ici: XML sérialisation des types dérivés lorsque le type de base est dans un autre espace de noms ou DLL

Mais 2 problèmes peuvent survenir dans cette implémentation:

(1) Que se passe-t-il si DerivedBase n'est pas dans l'espace de noms de la classe Base, ou pire encore dans un projet qui dépend de l'espace de noms Base, de sorte que Base ne peut pas XMLInclude DerivedBase

(2) Que se passe-t-il si nous n'avons que la classe Base en tant que dll, donc encore une fois Base ne peut pas XMLInclude DerivedBase

Jusqu'à maintenant, ...

Ainsi, la solution aux 2 problèmes est d'utiliser XmlSerializer Constructor (Type, array []) :

XmlSerializer ser = new XmlSerializer(typeof(A), new Type[]{ typeof(DerivedBase)});

Un exemple détaillé est fourni ici sur MSDN: XmlSerializer Constructor (Type, extraTypesArray [])

Il me semble que pour les XML DataContracts ou Soap, vous devez vérifiez le XmlRoot comme mentionné ici dans cette SO question .

A ne réponse similaire est ici sur SO mais il n'est pas marqué comme un, car ce n'est pas l'OP qui semble l'avoir déjà considéré.

0
B Charles H