You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

XmlTextReader重写时跳过子元素问题及解决方案咨询

问题:XML动态重命名元素后反序列化列表失效

现有通过模型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

问题原因分析

  1. 元素名称匹配不完整:自定义CustomTextReader的GetElementName方法仅对XmlNodeType.Element(开始元素)添加POCO后缀,但XmlSerializer会验证开始元素和结束元素的名称一致性。当读取到</SampleElement>时,NodeType是XmlNodeType.EndElement,直接返回原名称,导致序列化器认为元素不匹配,触发Skip逻辑。

  2. xsi:type属性处理逻辑错误:GetAttribute方法中att = att.EndsWith("X")是语法错误,EndsWith返回布尔值,不可能等于字符串att,导致xsi:type属性无法正确替换为POCO对应的类型名,序列化器无法识别列表内元素的类型。

  3. 列表元素的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.21 00:46:09