.net 使用XmlSerializer将空xml属性值初始化为可空的int属性

wsewodh2  于 2023-10-21  发布在  .NET
关注(0)|答案(6)|浏览(131)

我从第三方得到一个XML,我需要将其转换为C#对象。此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);
    }
}

当我将“Value”属性的类型更改为int时,无效化失败,并出现InvalidOperationException:
XML文档中存在错误(1,16)。
有没有人可以建议如何将空值属性转换为可空类型(作为null),同时将非空属性值转换为整数?有没有什么技巧,这样我就不必手动对每个字段进行格式化(实际上有很多字段)?
在ahsteele评论后更新:

  1. Xsi:nil attribute
    据我所知,这个属性只对XmlElementAttribute起作用--这个属性指定元素没有内容,无论是子元素还是正文文本。但是我需要找到XmlAttributeAttribute的解决方案。无论如何,我不能改变XML,因为我无法控制它。
  2. bool * 指定属性
    仅当属性值非空或缺少属性时,此属性才有效。当attr具有空值(attr ='')时,XmlSerializer构造函数失败(如预期的那样).
public class Element
{
    [XmlAttribute("attr")]
    public int Value { get; set; }

    [XmlIgnore]
    public bool ValueSpecified;
}
  1. Custom Nullable class like in this blog post by Alex Scordellis
    我试着从这篇博客文章中采用类来解决我的问题:
[XmlAttribute("attr")]
public NullableInt Value { get; set; }

但XmlSerializer构造函数失败,并出现InvalidOperationException:
无法序列化TestConsoleApplication. NullableInt类型的成员“Value”。
XmlAttribute/XmlText不能用于编码实现IXmlSerializable的类型
1.丑陋的代理解决方案(我为我在这里写的代码感到羞耻:)):

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接口。
目前看来,我必须为整个Element类实现IXmlSerializable(它很大),而且没有简单的解决办法。

ws51t4hk

ws51t4hk1#

这应该是可行的:

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

shyt4zoc2#

我通过实现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);
    }
}
dwbf0jvd

dwbf0jvd3#

最近我自己也一直在搞序列化,发现下面的文章和帖子在处理值类型的空数据时很有帮助。
如何在C#中使用XmlSerializer使值类型可为空- serialization* 的答案详细介绍了XmlSerializer的一个非常漂亮的技巧。具体来说,XmlSerialier查找XXXSpecified布尔属性,以确定是否应包含它,这允许您忽略空值。
Alex Scordellis问了一个StackOverflow问题,得到了很好的回答。Alex还在他的博客上写了一篇关于他试图解决 * Using XmlSerializer to deserialize into a Nullable * 问题的文章。
有关Xsi:nil Attribute Binding Support的MSDN文档也很有用。就像IXmlSerializable Interface的文档一样,尽管编写自己的实现应该是最后的手段。

r8uurelv

r8uurelv4#

我想我最好把答案扔到帽子里:通过创建实现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));
        }
    }
}
z0qdvdin

z0qdvdin5#

您也可以通过将xml加载到XmlDocument中,然后将其转换为Json来获得您正在查找的对象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;
        }
to94eoyn

to94eoyn6#

我需要将不能修改的xml格式化,它缺少一些属性,我需要使它们可以为空。
我一直在寻找一个简单和有效的解决方案很长一段时间,但不幸的是没有这样的解决方案。我不得不为一个具有可空字段的类编写自己的IXmlSerializable实现。

#nullable enable
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Schema;
using System.Xml.Serialization;
using System.Xml.XPath;
    
public abstract class XmlNullableSerializable : IXmlSerializable
{
    private static ConcurrentDictionary<int, XmlSerializer> SerializerCache { get; } = new();

    public virtual void ReadXml(XmlReader reader)
    {
        var xmlObject = XElement.Parse(reader.ReadOuterXml());
        var props = GetType().GetProperties().Where(x => x.CanWrite);

        foreach (var prop in props)
        {
            switch (prop.PropertyType)
            {
                case { } type when IsEnumerableType(type):
                {
                    var propName = GetXmlElementName(prop);
                    var xmlProps = xmlObject.XPathSelectElements(propName).ToList();

                    var value = GetValueFromEnumerableType(xmlProps, prop);
                    prop.SetValue(this, value ?? prop.GetValue(this), null);

                    break;
                }
                case { IsClass: true }:
                {
                    var propName = GetXmlElementName(prop);
                    var xmlProp = xmlObject.XPathSelectElement(propName);

                    var value = GetValueFromRefType(xmlProp, prop);
                    prop.SetValue(this, value ?? prop.GetValue(this), null);

                    break;
                }
                case { IsValueType: true }:
                {
                    var attrName = GetXmlAttrName(prop);
                    var xmlAttr = xmlObject.Attributes().FirstOrDefault(x => x.Name == attrName);

                    var value = GetValueFromValueType(xmlAttr, prop);
                    prop.SetValue(this, value ?? prop.GetValue(this), null);

                    break;
                }
                default: throw new NotImplementedException($"Type {prop.PropertyType} from {prop} not support");
            }
        }
    }

    private static object? GetValueFromValueType(XAttribute? xmlAttr, PropertyInfo prop)
    {
        var underlyingType = Nullable.GetUnderlyingType(prop.PropertyType);
        var isNullable = underlyingType != null;
        var type = underlyingType ?? prop.PropertyType;

        var result = xmlAttr?.Value switch
        {
            null when isNullable => null,
            { } value when typeof(Guid) == type => TypeDescriptor.GetConverter(type).ConvertFromInvariantString(value),
            { } value when typeof(bool) == type => value switch
            {
                _ when string.IsNullOrEmpty(value) => default,
                "1" => true,
                "0" => false,
                _ when bool.TryParse(value, out var boolResult) => boolResult,
                _ => default
            },
            { } value when type.IsEnum => EnumConvertor(value, type),
            _ => xmlAttr?.Value is null ? null : Convert.ChangeType(xmlAttr.Value, type)
        };

        return result;
    }
    
    private static object? GetValueFromRefType(XNode? xmlProp, PropertyInfo prop)
    {
        if (xmlProp is null) return null;
        using var propReader = xmlProp.CreateReader();
        var serializer = new XmlSerializer(prop.PropertyType);
        return serializer.Deserialize(propReader);
    }
    
    private static object? GetValueFromEnumerableType(List<XElement> xmlElements, PropertyInfo prop)
    {
        if (!xmlElements.Any()) return null;

        var sb = new StringBuilder();
        var type = typeof(EnumerableWrapper<>).MakeGenericType(prop.PropertyType);

        sb.AppendLine($"<{nameof(EnumerableWrapper<object>.Items)}>");
        foreach (var xmlProp in xmlElements)
            sb.AppendLine(xmlProp.ToString());
        sb.AppendLine($"</{nameof(EnumerableWrapper<object>.Items)}>");

        using var arrayXmlReader = new StringReader(sb.ToString());

        var overrides = new XmlAttributeOverrides();
        overrides.Add(type, nameof(EnumerableWrapper<object>.Items), new XmlAttributes()
        {
            XmlElements =
            {
                new XmlElementAttribute()
                {
                    ElementName = xmlElements[0].Name.LocalName,
                }
            }
        });

        var serializer = GetXmlSerializer(type, overrides);

        var result = serializer.Deserialize(arrayXmlReader);
        return result is null
            ? null
            : type.GetProperty(nameof(EnumerableWrapper<object>.Items))!.GetValue(result, null);
    }

    private static object EnumConvertor(object? value, Type type)
    {
        if (value is string s)
            value = Enum.Parse(type, s, true);

        value = Enum.ToObject(type, Convert.ToUInt64(value));

        if (!Enum.IsDefined(type, value))
            throw new InvalidCastException($"Cannot cast {value} to enum type {type.Name}.");

        return value;
    }
    
    private static XmlSerializer GetXmlSerializer(Type type, XmlAttributeOverrides attributeOverrides)
    {
        var key =
            $"{type.FullName!}-{attributeOverrides[type, nameof(EnumerableWrapper<object>.Items)].XmlElements[0].ElementName}"
                .GetHashCode();
        
        if (SerializerCache.TryGetValue(key, out var serializer)) return serializer;
        serializer = new XmlSerializer(type, attributeOverrides);
        SerializerCache.AddOrUpdate(key, x => serializer, (i, xmlSerializer) => serializer);
        return serializer;
    }
    
    private static bool IsEnumerableType(Type type)
    {
        return type.Name != nameof(String)
               && type.GetInterface(nameof(IEnumerable)) != null;
    }

    protected virtual string GetXmlElementName(PropertyInfo prop)
    {
        var xmlElement =
            prop.GetCustomAttributes(typeof(XmlElementAttribute), true).FirstOrDefault() as XmlElementAttribute;

        return xmlElement?.ElementName ?? prop.Name;
    }

    protected virtual string GetXmlAttrName(PropertyInfo prop)
    {
        var xmlAttribute =
            prop.GetCustomAttributes(typeof(XmlAttributeAttribute), true).FirstOrDefault() as XmlAttributeAttribute;

        return xmlAttribute?.AttributeName ?? prop.Name;
    }

    public virtual XmlSchema GetSchema() => throw new NotImplementedException();
    public virtual void WriteXml(XmlWriter writer) => throw new NotImplementedException();
}

[XmlRoot(nameof(Items))]
public class EnumerableWrapper<T>
{
    public T Items { get; set; }
}

然后,您只需从XmlNullableSerializable继承您的类。这只适用于非空化,你的类可以包含结构类型的可空字段。范例:

public class A : XmlNullableSerializable
{
    public int Int {get;set;}
    public bool? Bool {get;set;}
}

public class B : XmlNullableSerializable
{
    public A Obj {get;set;}
    public List<A> Enumerable {get;set;}
    public DateTime? Dt {get;set;}
}

var xml = "...";
using var sr = new StringReader(xml);
var serializer = new XmlSerializer(typeof(B));
var obj = serializer.Deserialize(sr);

相关问题