XmlTextReader重写时跳过子元素问题及解决方案咨询
现有通过模型XML注解序列化生成的XML文件,需用新建的POCO结构反序列化。因无法修改POCO类及属性名称,决定在读取XML时动态重命名元素和属性。目前根元素属性可正常读取,但读取到ChildElementList时进入Skip逻辑,无法反序列化列表内元素。不修改元素名称时可正常工作,动态添加POCO后缀后失效(例如POCO类名为SampleObjectPOCO时,XML中元素名为SampleElementPOCO可正常反序列化,动态修改则失败)。
相关代码片段
XML示例
<?xml version="1.0"?> <RootElement xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" SomeAttribute="123"> <ChildElementList> <SampleElement xsi:type="SampleElementX" SampleElementSomeAttribute="456"> <AnotherSampleElementList/> </SampleElement > </ChildElementList> </RootElement >
CustomTextReader代码
Public Class CustomTextReader Inherits XmlTextReader Protected ReadOnly provider As XmlReadWriteProvider Public Sub New(input As Stream) MyBase.New(input) End Sub Public Sub New(provider As XmlReadWriteProvider, input As Stream) MyBase.New(input) Me.provider = provider End Sub Public Overrides Sub Skip() MyBase.Skip() End Sub Public Overrides ReadOnly Property LocalName As String Get Return GetElementName(MyBase.LocalName) End Get End Property Public Overrides ReadOnly Property Name As String Get Return GetElementName(MyBase.Name) End Get End Property Public Overrides Function GetAttribute(localName As String, namespaceURI As String) As String Dim att As String = MyBase.GetAttribute(localName, namespaceURI) Return If(att IsNot Nothing, If(att = att.EndsWith("X"), att.Replace(att.Substring(-1, 1), "POCO"), att + "POCO"), att) End Function Function IsExcludedNode(nodeName As String) As Boolean Return excludeNodes.Contains(nodeName) End Function Function GetElementName(elementName As String) As String If MyBase.NodeType <> XmlNodeType.Element Then Return elementName If IsExcludedNode(elementName) Then Return elementName Return elementName + "POCO" End Function End Class
反序列化代码
Public Shared Function Deserialize(Of T)(fileName As String, extraTypes As Type()) As T Using xmlFileStream As FileStream = New FileStream(fileName, FileMode.Open) Using customXmlReader As XmlReader = XmlReadWriteProvider.GetXmlReader(GetType(T), xmlFileStream) Dim xmlSerializer As XmlSerializer = GetXmlSerializer(GetType(T), GetXmlAttributeOverrides(extraTypes), extraTypes, Nothing, Nothing) Deserialize = CType(xmlSerializer.Deserialize(customXmlReader), T) End Using End Using Return Deserialize End Function Shared Function GetXmlSerializer(type As Type, [overrides] As XmlAttributeOverrides, extraTypes() As Type, root As XmlRootAttribute, defaultNamespace As String) As XmlSerializer Dim customXmlSerializer As XmlSerializer = Nothing customXmlSerializer = New XmlSerializer(type, [overrides], extraTypes, root, defaultNamespace) Return customXmlSerializer End Function 'Add XML attribute tag to the properties of all known extra types. Private Shared Function GetXmlAttributeOverrides(extraTypes As Type()) As XmlAttributeOverrides Dim xmlAttributeOverrides = New XmlAttributeOverrides For Each type In extraTypes AddAttributeOverrides(type, xmlAttributeOverrides) Next Return xmlAttributeOverrides End Function Private Shared Sub AddAttributeOverrides(type As Type, xmlAttributeOverrides As XmlAttributeOverrides) For Each propertyInfo In type.GetProperties xmlAttributeOverrides.Add(type, propertyInfo.Name, GetXmlAttribute(propertyInfo)) Next End Sub 'If object is a primitive type it's an attribute, if it's a list it's an element Shared Function GetXmlAttribute(propertyInfo As PropertyInfo) As XmlAttributes If propertyInfo.IsPrimitive() Then Return New XmlAttributes With {.XmlAttribute = New XmlAttributeAttribute With {.AttributeName = propertyInfo.Name}} End If If GetType(IEnumerable).IsAssignableFrom(propertyInfo.PropertyType) Then Return New XmlAttributes With {.XmlArray = New XmlArrayAttribute With {.ElementName = propertyInfo.Name}} End If Return New XmlAttributes End Function <System.Runtime.CompilerServices.Extension> Private Function IsPrimitiveInternal(t As Type) As Boolean Return t.IsPrimitive OrElse t.IsValueType OrElse (t = GetType(String)) End Function <System.Runtime.CompilerServices.Extension> Function IsPrimitive(p As MemberInfo) As Boolean Return IsPrimitiveInternal(If(TryCast(p, PropertyInfo)?.PropertyType, p)) End Function <System.Runtime.CompilerServices.Extension> Public Function GetListItemType(IEnumerableType As Type) As Type If IEnumerableType.IsGenericType AndAlso IEnumerableType.GetGenericTypeDefinition() Is GetType(List(Of)) Then Return IEnumerableType.GetGenericArguments(0) End If Return Nothing End Function
问题原因分析
元素名称匹配不完整:自定义
CustomTextReader的GetElementName方法仅对XmlNodeType.Element(开始元素)添加POCO后缀,但XmlSerializer会验证开始元素和结束元素的名称一致性。当读取到</SampleElement>时,NodeType是XmlNodeType.EndElement,直接返回原名称,导致序列化器认为元素不匹配,触发Skip逻辑。xsi:type属性处理逻辑错误:
GetAttribute方法中att = att.EndsWith("X")是语法错误,EndsWith返回布尔值,不可能等于字符串att,导致xsi:type属性无法正确替换为POCO对应的类型名,序列化器无法识别列表内元素的类型。列表元素的XmlArrayItem配置缺失:当前的
XmlAttributeOverrides仅配置了XmlArray的名称,但未指定列表内元素(XmlArrayItem)的对应名称,序列化器无法将动态修改后的元素名与POCO类型关联,因此跳过列表内容。
解决方案
1. 修复元素名称重命名逻辑
修改GetElementName方法,对开始元素和结束元素都应用后缀:
Function GetElementName(elementName As String) As String ' 对开始和结束元素统一添加POCO后缀 If MyBase.NodeType = XmlNodeType.Element Or MyBase.NodeType = XmlNodeType.EndElement Then If IsExcludedNode(elementName) Then Return elementName Return elementName + "POCO" End If Return elementName End Function
2. 修正xsi:type属性处理
修复GetAttribute方法中的逻辑错误,正确处理类型替换:
Public Overrides Function GetAttribute(localName As String, namespaceURI As String) As String Dim attValue As String = MyBase.GetAttribute(localName, namespaceURI) If attValue IsNot Nothing Then ' 专门处理xsi:type属性,将末尾的X替换为POCO If localName = "type" And namespaceURI = "http://www.w3.org/2001/XMLSchema-instance" Then If attValue.EndsWith("X") Then Return attValue.Substring(0, attValue.Length - 1) + "POCO" End If Else ' 其他属性根据POCO属性名规则添加后缀(若需) Return attValue + "POCO" End If End If Return attValue End Function
3. 添加XmlArrayItem配置
修改AddAttributeOverrides方法,为列表类型属性添加XmlArrayItem配置,关联动态修改后的元素名:
Private Shared Sub AddAttributeOverrides(type As Type, xmlAttributeOverrides As XmlAttributeOverrides) For Each propertyInfo In type.GetProperties Dim xmlAttrs = GetXmlAttribute(propertyInfo) xmlAttributeOverrides.Add(type, propertyInfo.Name, xmlAttrs) ' 处理列表类型的子元素配置 If GetType(IEnumerable).IsAssignableFrom(propertyInfo.PropertyType) Then Dim itemType = propertyInfo.PropertyType.GetListItemType() If itemType IsNot Nothing Then ' 原XML元素名 = POCO类型名去掉POCO后缀 Dim originalElementName = itemType.Name.Replace("POCO", "") ' 动态修改后的元素名 = 原名称 + POCO Dim arrayItemAttr = New XmlArrayItemAttribute(originalElementName + "POCO", itemType) Dim itemAttrs = New XmlAttributes() itemAttrs.XmlArrayItems.Add(arrayItemAttr) xmlAttributeOverrides.Add(type, propertyInfo.Name, itemAttrs) End If End If Next End Sub
4. 移除不必要的Skip重写
当前Skip方法仅调用基类实现,无自定义逻辑,可直接删除该重写,避免潜在干扰:
' 移除以下代码 Public Overrides Sub Skip() MyBase.Skip() End Sub
内容的提问来源于stack exchange,提问作者Emre Utku Solak

