我从第 3 方获得一个 xml,我需要将其反序列化为 C# 对象。该 xml 可能包含具有整数类型值或空值的属性:attr=”11” 或 attr=””。我想将此属性值反序列化为可空整数类型的属性。但 XmlSerializer 不支持反序列化为可为 null 的类型。以下测试代码在创建 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 评论后更新:
据我所知,该属性仅适用于 XmlElementAttribute - 该属性指定该元素没有内容,无论是子元素还是正文文本。但我需要找到 XmlAttributeAttribute 的解决方案。无论如何,我无法更改 xml,因为我无法控制它。
此属性仅在属性值非空或属性缺失时才起作用。当 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 类型的成员“Value”。
XmlAttribute/XmlText 不能用于编码实现 IXmlSerialized 的类型}
丑陋的代理解决方案(为我在这里编写这段代码感到羞耻:)):
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;
}
}
但我不想提出这样的解决方案,因为它破坏了我的类为其消费者提供的接口。我最好手动实现 IXmlSerialized 接口。
目前看来我必须为整个 Element 类实现 IXmlSerialized (它很大)并且没有简单的解决方法......
这应该有效:
[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?); }
}
我通过实现 IXmlSerialized 接口解决了这个问题。我没有找到更简单的方法。
这是测试代码示例:
[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);
}
}
我自己最近一直在搞序列化,发现以下文章和帖子在处理值类型的空数据时很有帮助。
如何在 C# 中使用 XmlSerializer 使值类型可为空 - 序列化的答案详细介绍了 XmlSerializer 的一个非常漂亮的技巧。具体来说,XmlSerialier 会查找 XXXSpecified 布尔属性来确定是否应包含它,这允许您忽略空值。
Alex Scordellis 提出了一个 StackOverflow 问题,得到了一个很好的答案。 Alex 还在他的博客上写了一篇关于他试图解决的问题的好文章 使用 XmlSerializer 反序列化为 Nullable
关于 Xsi:nil 属性绑定支持 的 MSDN 文档也很有用。正如 IXmlSerialized Interface 上的文档一样,尽管编写自己的实现应该是最后的手段。
我想我不妨把我的答案扔进帽子里: 通过创建实现 IXmlSerialized 接口的自定义类型解决了此问题:
假设您有一个包含以下节点的 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));
}
}
}
您还可以通过将
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;
}
我需要反序列化无法修改的xml,它缺少一些属性,我需要使它们可以为空。
我长期以来一直在寻找一个简单且可行的解决方案,但不幸的是没有这样的解决方案。我必须为具有可为空字段的类编写自己的 IXmlSerialized 实现。
#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; }
}
然后你只需从 XmlNullableSerialized 继承你的类。 这只适用于反序列化,您的类可以包含结构类型的可为空字段。 示例:
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);