Deserialisieren eines leeren XML-Attributwerts in eine nullable int-Eigenschaft mithilfe von XmlSerializer

74

Ich erhalte eine XML-Datei von einem Drittanbieter und muss sie in ein C # -Objekt deserialisieren. Diese XML-Datei kann Attribute mit einem Wert vom Typ Integer oder einem leeren Wert enthalten: attr = ”11” oder attr = ””. Ich möchte diesen Attributwert in die Eigenschaft mit dem Typ einer nullbaren Ganzzahl deserialisieren. XmlSerializer unterstützt jedoch keine Deserialisierung in nullfähige Typen. Der folgende Testcode schlägt beim Erstellen von XmlSerializer mit InvalidOperationException fehl {"Es ist ein Fehler aufgetreten, der den Typ 'TestConsoleApplication.SerializeMe' widerspiegelt."}.

[XmlRoot("root")]
public class SerializeMe
{
    [XmlElement("element")]
    public Element Element { get; set; }
}

public class Element
{
    [XmlAttribute("attr")]
    public int? Value { get; set; }
}

class Program {
    static void Main(string[] args) {
        string xml = "<root><element attr=''>valE</element></root>";
        var deserializer = new XmlSerializer(typeof(SerializeMe));
        Stream xmlStream = new MemoryStream(Encoding.ASCII.GetBytes(xml));
        var result = (SerializeMe)deserializer.Deserialize(xmlStream);
    }
}

Wenn ich den Typ der 'Value'-Eigenschaft in int ändere, schlägt die Deserialisierung mit InvalidOperationException fehl:

Im XML-Dokument (1, 16) ist ein Fehler aufgetreten.

Kann jemand raten, wie man ein Attribut mit einem leeren Wert in einen nullbaren Typ (als Null) deserialisiert und gleichzeitig einen nicht leeren Attributwert in eine Ganzzahl deserialisiert? Gibt es dafür einen Trick, damit ich nicht jedes Feld manuell deserialisieren muss (tatsächlich gibt es viele davon)?

Update nach Kommentar von ahsteele:

  1. Xsi: Null-Attribut

    Soweit ich weiß, funktioniert dieses Attribut nur mit XmlElementAttribute - dieses Attribut gibt an, dass das Element keinen Inhalt hat, egal ob untergeordnete Elemente oder Textkörper. Aber ich muss die Lösung für XmlAttributeAttribute finden. Auf jeden Fall kann ich XML nicht ändern, da ich keine Kontrolle darüber habe.

  2. bool * Angegebene Eigenschaft

    Diese Eigenschaft funktioniert nur, wenn der Attributwert nicht leer ist oder wenn das Attribut fehlt. Wenn attr einen leeren Wert hat (attr = ''), schlägt der XmlSerializer-Konstruktor fehl (wie erwartet).

    public class Element
    {
        [XmlAttribute("attr")]
        public int Value { get; set; }
    
        [XmlIgnore]
        public bool ValueSpecified;
    }
    
  3. Benutzerdefinierte Nullable-Klasse wie in diesem Blog-Beitrag von Alex Scordellis

    Ich habe versucht, die Klasse aus diesem Blog-Beitrag auf mein Problem zu übertragen:

    [XmlAttribute("attr")]
    public NullableInt Value { get; set; } 
    

    Der XmlSerializer-Konstruktor schlägt jedoch mit InvalidOperationException fehl:

    Mitglied 'Wert' vom Typ TestConsoleApplication.NullableInt kann nicht serialisiert werden.

    XmlAttribute / XmlText kann nicht zum Codieren von Typen verwendet werden, die IXmlSerializable implementieren.}

  4. Hässliche Ersatzlösung (schade, dass ich diesen Code hier geschrieben habe :)):

    public class Element
    {
        [XmlAttribute("attr")]
        public string SetValue { get; set; }
    
        public int? GetValue()
        {
            if ( string.IsNullOrEmpty(SetValue) || SetValue.Trim().Length <= 0 )
                return null;
    
            int result;
            if (int.TryParse(SetValue, out result))
                return result;
    
            return null;
        }
    }
    

    Aber ich möchte nicht auf diese Lösung kommen, weil sie die Schnittstelle meiner Klasse für ihre Verbraucher bricht. Ich würde besser IXmlSerializable Schnittstelle manuell implementieren.

Derzeit muss ich IXmlSerializable für die gesamte Element-Klasse implementieren (es ist groß) und es gibt keine einfache Problemumgehung…

Aliaksei Kliuchnikau
quelle

Antworten:

61

Das sollte funktionieren:

[XmlIgnore]
public int? Age { get; set; }

[XmlElement("Age")]
public string AgeAsText
{
  get { return (Age.HasValue) ? Age.ToString() : null; } 
  set { Age = !string.IsNullOrEmpty(value) ? int.Parse(value) : default(int?); }
}
abatishchev
quelle
4
Dies wird funktionieren, aber dies ist die gleiche Lösung wie Nummer 4) aus meiner Frage. Ich möchte keine Ersatzfelder in die öffentliche Oberfläche meiner Klasse einführen. Danke
Aliaksei Kliuchnikau
9
FWIW, ich finde diese Lösung besser als die explizite IXmlSerializable-Implementierung (die akzeptierte Lösung), obwohl sie nicht der spezifischen Frage des OP entspricht. Ich vermeide die Implementierung von IXmlSerializable, es sei denn, dies ist unbedingt erforderlich, und stelle fest, dass mich die Wartung auf lange Sicht mehr kostet. In einem einfachen Fall wie diesem und ohne andere mildernde Faktoren werde ich mich für die "hässliche" Ersatzlösung entscheiden, ohne darüber nachzudenken.
Paul Prewett
gibt ein bisschen zusätzlichen Overhead, aber Sie könnten natürlich zwei Klassen haben - eine für die Deserialisierung, die all diese zusätzlichen Eigenschaften hat, und die andere, die nur die tatsächlichen Werte hat. Erstellen Sie eine implizite Konvertierung, die nur eine neue Instanz der Klasse zurückgibt, in die mit allen korrekten Informationen konvertiert wird.
TheHitchenator
21

Ich habe dieses Problem durch die Implementierung der IXmlSerializable-Schnittstelle gelöst. Einfacheren Weg habe ich nicht gefunden.

Hier ist das Testcodebeispiel:

[XmlRoot("root")]
public class DeserializeMe {
    [XmlArray("elements"), XmlArrayItem("element")]
    public List<Element> Element { get; set; }
}

public class Element : IXmlSerializable {
    public int? Value1 { get; private set; }
    public float? Value2 { get; private set; }

    public void ReadXml(XmlReader reader) {
        string attr1 = reader.GetAttribute("attr");
        string attr2 = reader.GetAttribute("attr2");
        reader.Read();

        Value1 = ConvertToNullable<int>(attr1);
        Value2 = ConvertToNullable<float>(attr2);
    }

    private static T? ConvertToNullable<T>(string inputValue) where T : struct {
        if ( string.IsNullOrEmpty(inputValue) || inputValue.Trim().Length == 0 ) {
            return null;
        }

        try {
            TypeConverter conv = TypeDescriptor.GetConverter(typeof(T));
            return (T)conv.ConvertFrom(inputValue);
        }
        catch ( NotSupportedException ) {
            // The conversion cannot be performed
            return null;
        }
    }

    public XmlSchema GetSchema() { return null; }
    public void WriteXml(XmlWriter writer) { throw new NotImplementedException(); }
}

class TestProgram {
    public static void Main(string[] args) {
        string xml = @"<root><elements><element attr='11' attr2='11.3'/><element attr='' attr2=''/></elements></root>";
        XmlSerializer deserializer = new XmlSerializer(typeof(DeserializeMe));
        Stream xmlStream = new MemoryStream(Encoding.ASCII.GetBytes(xml));
        var result = (DeserializeMe)deserializer.Deserialize(xmlStream);
    }
}
Aliaksei Kliuchnikau
quelle
12

Ich habe in letzter Zeit selbst viel mit der Serialisierung herumgespielt und fand die folgenden Artikel und Beiträge hilfreich, wenn es um Nulldaten für Werttypen geht.

Die Antwort auf Wie man einen Werttyp mit XmlSerializer in C # nullbar macht - Serialisierung beschreibt einen ziemlich raffinierten Trick des XmlSerializer. Insbesondere sucht der XmlSerialier nach einer XXXSpecified-Booleschen Eigenschaft, um zu bestimmen, ob sie enthalten sein soll, sodass Sie Nullen ignorieren können.

Alex Scordellis stellte eine StackOverflow-Frage, die eine gute Antwort erhielt . Alex hat in seinem Blog auch einen guten Bericht über das Problem verfasst, das er mit XmlSerializer lösen wollte, um es in ein Nullable <int> zu deserialisieren .

Die MSDN-Dokumentation zur Unterstützung der Xsi: nil-Attributbindung ist ebenfalls hilfreich. Wie die Dokumentation zu IXmlSerializable Interface , sollte das Schreiben einer eigenen Implementierung Ihr letzter Ausweg sein.

ahsteele
quelle
Der Link "Verwenden von XmlSerializer zum Deserialisieren in eine Nullable" ist nicht mehr verfügbar. Hier ist eine zwischengespeicherte Version von Google
Anttu
@Anttu Ich habe den Link in der Antwort auf das Wayback Machine-Archiv des Originals umgeschaltet. Verwenden von XmlSerializer zum Deserialisieren in ein Nullable <int> .
Ahsteele
2

Ich dachte, ich könnte meine Antwort genauso gut in den Hut werfen: Dieses Problem wurde behoben, indem ein benutzerdefinierter Typ erstellt wurde, der die IXmlSerializable-Schnittstelle implementiert:

Angenommen, Sie haben ein XML-Objekt mit den folgenden Knoten:

<ItemOne>10</Item2>
<ItemTwo />

Das Objekt, um sie darzustellen:

public class MyItems {
    [XmlElement("ItemOne")]
    public int ItemOne { get; set; }

    [XmlElement("ItemTwo")]
    public CustomNullable<int> ItemTwo { get; set; } // will throw exception if empty element and type is int
}

Dynamische nullfähige Struktur zur Darstellung potenzieller nullbarer Einträge zusammen mit der Konvertierung

public struct CustomNullable<T> : IXmlSerializable where T: struct {
    private T value;
    private bool hasValue;

    public bool HasValue {
        get { return hasValue; }
    }

    public T Value {
        get { return value; }
    }

    private CustomNullable(T value) {
        this.hasValue = true;
        this.value = value;
    }

    public XmlSchema GetSchema() {
        return null;
    }

    public void ReadXml(XmlReader reader) {
        string strValue = reader.ReadString();
        if (String.IsNullOrEmpty(strValue)) {
            this.hasValue = false;
        }
        else {
            T convertedValue = strValue.To<T>();
            this.value = convertedValue;
            this.hasValue = true;
        }
        reader.ReadEndElement();

    }

    public void WriteXml(XmlWriter writer) {
        throw new NotImplementedException();
    }

    public static implicit operator CustomNullable<T>(T value) {
        return new CustomNullable<T>(value);
    }

}

public static class ObjectExtensions {
    public static T To<T>(this object value) {
        Type t = typeof(T);
        // Get the type that was made nullable.
        Type valueType = Nullable.GetUnderlyingType(typeof(T));
        if (valueType != null) {
            // Nullable type.
            if (value == null) {
                // you may want to do something different here.
                return default(T);
            }
            else {
                // Convert to the value type.
                object result = Convert.ChangeType(value, valueType);
                // Cast the value type to the nullable type.
                return (T)result;
            }
        }
        else {
            // Not nullable.
            return (T)Convert.ChangeType(value, typeof(T));
        }
    }
}
Levi Fuller
quelle
1

Sie können dies auch tun, indem Sie das xmlin ein laden XmlDocumentund es dann deserialisieren Json, um das gesuchte Objekt zu erhalten T.

        public static T XmlToModel<T>(string xml)
        {

            XmlDocument doc = new XmlDocument();
            doc.LoadXml(xml);

            string jsonText = JsonConvert.SerializeXmlNode(doc);

            T result = JsonConvert.DeserializeObject<T>(jsonText);

            return result;
        }

ErdemS
quelle
Antwort vom Link
CrudaLilium