Дессериализация значения атрибута пустого xml в свойство nullable int с помощью XmlSerializer

Я получаю xml от третьей стороны, и мне нужно десериализовать его на объект С#. Этот xml может содержать атрибуты со значением целочисленного типа или пустым значением: attr = "11" или attr = ". Я хочу десериализовать это значение атрибута в свойстве с типом целочисленного целочисленного значения. Но XmlSerializer не поддерживает десериализацию в типы с нулевым значением. Следующий тестовый код выходит из строя во время создания XmlSerializer с InvalidOperationException {" Произошла ошибка, отражающая тип "TestConsoleApplication.SerializeMe".}.

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

Когда я меняю тип свойства "Значение" на int, десериализация не выполняется с InvalidOperationException:

В документе XML есть ошибка (1, 16).

Может кто-нибудь посоветует, как десериализовать атрибут с пустым значением в nullable type (как null), в то же время десериализуя непустое значение атрибута в целое число? Есть ли какой-либо трюк для этого, поэтому мне не придется делать десериализацию каждого поля вручную (на самом деле их много)?

Обновление после комментария от ahsteele:

  • Xsi: атрибут nil

    Насколько я знаю, этот атрибут работает только с XmlElementAttribute - этот атрибут указывает, что элемент не имеет содержимого, будь то дочерние элементы или основной текст. Но мне нужно найти решение для XmlAttributeAttribute. В любом случае я не могу изменить xml, потому что я не могу контролировать его.

  • bool * Заданное свойство

    Это свойство работает только тогда, когда значение атрибута не пусто или когда атрибут отсутствует. Когда attr имеет пустое значение (attr = ''), конструктор XmlSerializer терпит неудачу (как и ожидалось).

    public class Element
    {
        [XmlAttribute("attr")]
        public int Value { get; set; }
    
        [XmlIgnore]
        public bool ValueSpecified;
    }
    
  • Пользовательский класс Nullable, как в этом блоге Alex Scordellis

    Я попытался перенять класс из этого сообщения в блоге в свою проблему:

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

    Но конструктор XmlSerializer не работает с InvalidOperationException:

    Невозможно выполнить сериализацию элемента "Значение" типа TestConsoleApplication.NullableInt.

    XmlAttribute/XmlText не может использоваться для кодирования типов, реализующих IXmlSerializable}

  • Уродливое суррогатное решение (стыдно за меня, что я написал этот код здесь:)):

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

    Но я не хочу придумывать решение, подобное этому, потому что он нарушает интерфейс моего класса для его потребителей. Я бы лучше вручную реализовал интерфейс IXmlSerializable.

В настоящее время похоже, что я должен реализовать IXmlSerializable для всего класса Element (он большой), и нет простого обходного пути...

Ответ 1

Я решил эту проблему, реализовав интерфейс IXmlSerializable. Я не нашел более легкого способа.

Вот пример тестового кода:

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

Ответ 2

Это должно работать:

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

Ответ 3

В последнее время я очень часто возился с сериализацией и нашел следующие статьи и сообщения полезными при работе с нулевыми данными для типов значений.

Ответ на Как сделать тип значения nullable с XmlSerializer в С# - сериализации, представляет собой довольно изящный трюк XmlSerializer. В частности, XmlSerialier ищет свойство XXXSpecified boolean, чтобы определить, должно ли оно быть включено, которое позволяет игнорировать нули.

Алекс Скорделлис спросил вопрос StackOverflow, который получил хороший ответ. Алекс также сделал хорошую запись в своем блоге о проблеме, которую он пытался решить Использование XmlSerializer для десериализации в Nullable <int> .

Документация MSDN на Xsi: nil Поддержка привязки атрибутов также полезна. Как и документация на IXmlSerializable Interface, хотя ваша собственная реализация должна быть вашим последним средством.

Ответ 4

Думал, что я мог бы также бросить свой ответ в шляпу: Эта проблема была решена путем создания настраиваемого типа, который реализует интерфейс IXmlSerializable:

Скажем, у вас есть объект XML со следующими узлами:

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

Объект для их представления:

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
}

Динамическая нулевая структура для представления любых потенциальных нулевых записей вместе с преобразованием

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