XML-Serialisierung der Schnittstelleneigenschaft

82

Ich möchte ein Objekt, das (unter anderem) eine Eigenschaft vom Typ IModelObject (eine Schnittstelle) hat, in XML serialisieren .

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

Wenn ich versuche, ein Objekt dieser Klasse zu serialisieren, wird die folgende Fehlermeldung
angezeigt : "Mitglied Example.Model vom Typ Example kann nicht serialisiert werden, da es sich um eine Schnittstelle handelt."

Ich verstehe, dass das Problem darin besteht, dass eine Schnittstelle nicht serialisiert werden kann. Der konkrete Modellobjekttyp ist jedoch bis zur Laufzeit unbekannt.

Das Ersetzen der IModelObject- Schnittstelle durch einen abstrakten oder konkreten Typ und die Verwendung der Vererbung mit XMLInclude ist möglich, scheint jedoch eine hässliche Problemumgehung zu sein.

Irgendwelche Vorschläge?

Elad
quelle

Antworten:

116

Dies ist einfach eine inhärente Einschränkung der deklarativen Serialisierung, bei der Typinformationen nicht in die Ausgabe eingebettet sind.

Beim Versuch, <Flibble Foo="10" />wieder in umzuwandeln

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

Woher weiß der Serializer, ob es sich um ein Int, einen String, ein Double (oder etwas anderes) handeln soll?

Damit dies funktioniert, haben Sie mehrere Möglichkeiten. Wenn Sie es jedoch erst zur Laufzeit wirklich wissen, ist es wahrscheinlich am einfachsten, die XmlAttributeOverrides zu verwenden .

Leider funktioniert dies nur mit Basisklassen, nicht mit Schnittstellen. Das Beste, was Sie dort tun können, ist, die Eigenschaft zu ignorieren, die für Ihre Anforderungen nicht ausreicht.

Wenn Sie wirklich bei Schnittstellen bleiben müssen, haben Sie drei echte Optionen:

Verstecke es und kümmere dich in einer anderen Eigenschaft darum

Hässliche, unangenehme Kesselplatte und viel Wiederholung, aber die meisten Verbraucher der Klasse werden sich nicht mit dem Problem befassen müssen:

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

Dies wird wahrscheinlich zu einem Alptraum für die Instandhaltung ...

Implementieren Sie IXmlSerializable

Ähnlich wie bei der ersten Option übernehmen Sie jedoch die volle Kontrolle über die Dinge

  • Vorteile
    • Sie haben keine bösen "falschen" Eigenschaften herumhängen.
    • Sie können direkt mit der XML-Struktur interagieren und so Flexibilität / Versionierung hinzufügen
  • Nachteile
    • Möglicherweise müssen Sie das Rad für alle anderen Eigenschaften der Klasse erneut implementieren

Probleme der Doppelarbeit sind ähnlich wie beim ersten.

Ändern Sie Ihre Eigenschaft, um einen Umhüllungstyp zu verwenden

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

Dies zu verwenden würde ungefähr Folgendes beinhalten (in Projekt 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);
    }
}

was dir gibt:

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

Dies ist für Benutzer der Klasse offensichtlich umständlicher, vermeidet jedoch viel Kesselplatte.

Ein glückliches Medium kann darin bestehen, die XmlAnything-Idee in die Backing-Eigenschaft der ersten Technik zu integrieren. Auf diese Weise wird der größte Teil der Grunzarbeit für Sie erledigt, aber die Verbraucher der Klasse leiden nicht unter Auswirkungen, die über die Verwechslung mit der Selbstbeobachtung hinausgehen.

ShuggyCoUk
quelle
Ich habe versucht, Ihren Ansatz mit Wrapping-Eigenschaften zu implementieren, habe aber leider ein Problem :( Könnten Sie sich bitte diesen Beitrag ansehen: stackoverflow.com/questions/7584922/…
SOReader
Gibt es eine künstlerische Einführung in die FooSerialized-Eigenschaft?
Gqqnbig
42

Die Lösung hierfür ist die Verwendung der Reflexion mit dem DataContractSerializer. Sie müssen Ihre Klasse nicht einmal mit [DataContract] oder [DataMember] markieren. Es wird jedes Objekt serialisieren, unabhängig davon, ob es Schnittstellentyp-Eigenschaften (einschließlich Wörterbücher) in XML hat. Hier ist eine einfache Erweiterungsmethode, mit der jedes Objekt in XML serialisiert wird, auch wenn es über Schnittstellen verfügt (beachten Sie, dass Sie dies optimieren können, um es auch rekursiv auszuführen).

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

Der LINQ-Ausdruck listet jede Eigenschaft auf, gibt jede Eigenschaft zurück, die eine Schnittstelle ist, ruft den Wert dieser Eigenschaft (des zugrunde liegenden Objekts) ab, ruft den Typ dieses konkreten Objekts ab, fügt ihn in ein Array ein und fügt ihn dem Serializer hinzu Liste bekannter Typen.

Jetzt weiß der Serializer, welche Typen er serialisiert, damit er seine Arbeit erledigen kann.

Despertar
quelle
Sehr elegante und einfache Lösung des Problems. Vielen Dank!
Ghlouw
2
Dies scheint für eine generische IList und Schnittstelle nicht zu funktionieren. zB IList <IMyInterface>. Der Concreate-Wert für IMyInterface muss jedoch zu den KnownTypes hinzugefügt werden, stattdessen wird die IList <IMyInterface> hinzugefügt.
Galford13x
6
@ galford13x Ich habe versucht, dieses Beispiel so einfach wie möglich zu gestalten, während ich den Punkt noch demonstrierte. Das Hinzufügen in jedem Einzelfall, wie Rekursion oder Schnittstellentypen, macht das Lesen weniger klar und entfernt sich vom Hauptpunkt. Bitte fügen Sie zusätzliche Überprüfungen hinzu, um die erforderlichen bekannten Typen zu ermitteln. Um ehrlich zu sein, glaube ich nicht, dass es irgendetwas gibt, das man mit Reflexion nicht bekommen kann. Dies wird zum Beispiel den Typ des generischen Parameters Stackoverflow.com/questions/557340/…
Despertar
Ich verstehe, ich habe dies nur erwähnt, da die Frage nach der Serialisierung der Schnittstelle gestellt wurde. Ich dachte, ich würde andere wissen lassen, dass der Fehler ohne Modifikation zu erwarten ist, um zu verhindern, dass ihr Kopf auf den Kopf schlägt. Ich habe Ihren Code jedoch sehr geschätzt, da ich das Attribut [KnownType ()] hinzugefügt habe und Ihr Code mich zum Ergebnis geführt hat.
Galford13x
1
Gibt es eine Möglichkeit, das Namesapce beim Serialisieren wegzulassen? Ich habe versucht, xmlwriterSettings stattdessen mit einem xmlwriter zu verwenden. Ich verwende die Überladung, bei der ich die zusätzlichen Typen übergeben kann, aber es funktioniert nicht ...
Legends
8

Sie können ExtendedXmlSerializer verwenden . Dieser Serializer unterstützt die Serialisierung von Schnittstelleneigenschaften ohne Tricks.

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

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

var xml = serializer.Serialize(obj);

Ihre XML sieht folgendermaßen aus:

<?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 unterstützt .net 4.5 und .net Core.

Wojtpl2
quelle
8

Wenn Sie Ihre Schnittstellenimplementierer im Voraus kennen, gibt es einen ziemlich einfachen Hack, mit dem Sie Ihren Schnittstellentyp zur Serialisierung bringen können, ohne Parsing-Code zu schreiben:

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

Die resultierende XML sollte ungefähr so ​​aussehen wie

 <interface><ofTypeKnownImplementor01><!-- etc... -->
hannasm
quelle
1
Sehr nützlich, danke. In den meisten Situationen kenne ich die Klassen, die die Schnittstelle implementieren. Diese Antwort sollte imo höher sein.
Jonah
Dies ist die einfachste Lösung. Danke dir!
mKay
3

Das Ersetzen der IModelObject-Schnittstelle durch einen abstrakten oder konkreten Typ und die Verwendung der Vererbung mit XMLInclude ist möglich, scheint jedoch eine hässliche Problemumgehung zu sein.

Wenn es möglich ist, eine abstrakte Basis zu verwenden, würde ich diese Route empfehlen. Es ist immer noch sauberer als die handgerollte Serialisierung. Das einzige Problem, das ich mit der abstrakten Basis sehe, ist, dass Sie immer noch den konkreten Typ benötigen? Zumindest habe ich es in der Vergangenheit so benutzt, so etwas wie:

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

Leider gibt es keine einfache Antwort, da der Serializer nicht weiß, was er für eine Schnittstelle serialisieren soll. Ich habe eine ausführlichere Erklärung gefunden, wie dies auf MSDN umgangen werden kann

MattH
quelle
1

Leider hatte ich einen Fall, in dem die zu serialisierende Klasse Eigenschaften hatte, die auch Schnittstellen als Eigenschaften hatten, sodass ich jede Eigenschaft rekursiv verarbeiten musste. Außerdem wurden einige der Schnittstelleneigenschaften als [XmlIgnore] markiert, sodass ich diese überspringen wollte. Ich nahm Ideen, die ich in diesem Thread gefunden hatte, und fügte einige Dinge hinzu, um ihn rekursiv zu machen. Hier wird nur der Deserialisierungscode angezeigt:

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

In meinem Projekt habe ich eine
Liste <IFormatStyle> FormatStyleTemplates;
mit verschiedenen Typen.

Ich benutze dann die Lösung 'XmlAnything' von oben, um diese Liste verschiedener Typen zu serialisieren. Die generierte XML ist wunderschön.

    [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
quelle
0

Ich habe dank dieses Blogs hier eine einfachere Lösung gefunden (Sie benötigen den DataContractSerializer nicht): XML-Serialisierung abgeleiteter Typen, wenn sich der Basistyp in einem anderen Namespace oder einer anderen DLL befindet

Bei dieser Implementierung können jedoch zwei Probleme auftreten:

(1) Was ist, wenn sich DerivedBase nicht im Namespace der Klasse Base befindet oder noch schlimmer in einem Projekt, das vom Base-Namespace abhängt, sodass Base DerivedBase nicht XMLInclude kann?

(2) Was ist, wenn wir nur die Klasse Base als DLL haben, so dass Base XMLInclude DerivedBase nicht kann?

Bis jetzt, ...

Die Lösung für die beiden Probleme ist die Verwendung des XmlSerializer-Konstruktors (Typ, Array []) :

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

Ein detailliertes Beispiel finden Sie hier auf MSDN: XmlSerializer Constructor (Type, extraTypesArray [])

Mir scheint, dass Sie für DataContracts oder Soap XMLs den XmlRoot überprüfen müssen, wie hier in dieser SO-Frage erwähnt .

Eine ähnliche Antwort gibt es hier auf SO, aber sie ist nicht als eine markiert, da es nicht das OP bereits in Betracht gezogen zu haben scheint.

B Charles H.
quelle