IEnumerable(of String) 被 XmlSerializer 忽略

IEnumerable(of String) ignored by XmlSerializer

对于我的一个object,我需要做一些backward/forward兼容性技巧,以便每个人都开心,同时不同版本共存并共享相同配置文件。

我尝试的最后一个技巧是使用 bridge collection 使用 IEnumerable(Of String) 将旧格式映射到新格式。我的问题是我无法让 XmlSerializer 识别我的自定义 collection。我查看了生成代码的序列化程序代码,一切似乎都很好。无论我做什么,只要我使用我的自定义 collection ListeCategories 最终会变成 Unsupported。如果我使用基本的 List(Of String),它可以工作,但我错过了逻辑。我错过了什么?

我还尝试了非通用自定义 collection(在第二个代码片段中进行了评论)但没有成功。

这是主要 object 和自定义 collection 的代码。 请注意,XmlSerializer(根据 MS Docs)要求的自定义可枚举实现和附加 Add 方法。

The XmlSerializer gives special treatment to classes that implement IEnumerable or ICollection. A class that implements IEnumerable must implement a public Add method that takes a single parameter. The Add method's parameter must be of the same type as is returned from the Current property on the value returned from GetEnumerator, or one of that type's bases. A class that implements ICollection (such as CollectionBase) in addition to IEnumerable must have a public Item indexed property (indexer in C#) that takes an integer, and it must have a public Count property of type integer. The parameter to the Add method must be the same type as is returned from the Item property, or one of that type's bases. For classes that implement ICollection, values to be serialized are retrieved from the indexed Item property, not by calling GetEnumerator.

Public Class OptionsSerializable
    Public Sub New()
    End Sub

    <XmlAnyElement>
    Public Property Unsupported As List(Of XmlElement)

    <System.Xml.Serialization.XmlElement(ElementName:="ListeCategoriesExt")>
    Public ReadOnly Property Categories As List(Of CategoryInfo)

    Public ReadOnly Property ListeCategories As IEnumerable(Of String) = New EnumerableBridge(Of String, CategoryInfo)(_categories, Function(s) New CategoryInfo(s), Function(c) c.SearchTerm)

    Public Property ServeurExchangeUrl As String
    Public Property VersionExchange As String
    Public Property AdresseBoitePartager As New List(Of String)
    Public Property GroupingMasks As New List(Of String)
    Public Property TFSLinks As New XmlSerializableDictionary(Of String, String)
    Public Property EstAlertageActif As Boolean
    Public Property Laps As Integer
    Public Property NbErreursMaximum As Integer
    Public Property DerniereVerification As Date
    Public Property ListeEnvoi As String
    Public Property DernierEnvoi As Date
    Public Property LogonDernierEnvoi As String
    Public Property IntervalEnvoiMinimum As Integer
End Class
'Public Class SpecificCollectionBridge
'    Inherits CollectionBridge(Of String, CategoryInfo)

'    Public Sub New(refCollection As IEnumerable(Of CategoryInfo))
'        MyBase.New(refCollection, Function(s) New CategoryInfo(s), Function(c) c.SearchTerm)
'    End Sub

'End Class

<Serializable>
Public Class EnumerableBridge(Of TSource, TTarget)
    Implements IEnumerable(Of TSource)

    Private _refCollection As IList(Of TTarget)
    Private _converterTo As Func(Of TSource, TTarget)
    Private _converterFrom As Func(Of TTarget, TSource)

    Public Sub New()

    End Sub

    Public Sub New(refCollection As IList(Of TTarget), converterTo As Func(Of TSource, TTarget), converterFrom As Func(Of TTarget, TSource))
        _refCollection = refCollection
        _converterTo = converterTo
        _converterFrom = converterFrom
    End Sub

    Public Sub Add(item As TSource) 'Implements ICollection(Of TSource).Add
        _refCollection.Add(_converterTo(item))
    End Sub

    Public Function GetEnumerator() As IEnumerator Implements IEnumerable.GetEnumerator
        Return GetEnumerator()
    End Function

    Public Function IEnumerableOfT_GetEnumerator() As IEnumerator(Of TSource) Implements IEnumerable(Of TSource).GetEnumerator
        Return _refCollection.Select(_converterFrom).GetEnumerator()
    End Function
End Class

你这里有几个问题:

  1. 您的 属性 ListeCategories 必须声明为返回的实际具体类型,而不是接口。 XmlSerializer 即使在预分配时也不会序列化声明为接口的属性。

    参见:Serializing a List<> exported as an ICollection<> to XML

  2. 您的类型 EnumerableBridge(Of TSource, TTarget) 必须直接实现 GetEnumerator() As IEnumerator(Of TSource)(使用名称 GetEnumerator())并显式实现 GetEnumerator() As IEnumerator,使用其他名称。

    在您的 class 中,您做相反的事情并显式地实现泛型方法,这会导致一个奇怪的异常并抛出一条无用的消息:

    [System.InvalidOperationException: To be XML serializable, types which inherit from IEnumerable must have an implementation of Add(System.Object) at all levels of their inheritance hierarchy. EnumerableBridge`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[CategoryInfo, 58be8a1c-a824-4133-9ef8-b2552cccedab, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]] does not implement Add(System.Object).]
    

    问题的演示fiddle:https://dotnetfiddle.net/PhaeZc

  3. 您可以从 EnumerableBridge() 的无参数构造函数中抛出一个 NotImplementedException(),因为 XmlSerializer 实际上不会调用它。

  4. 假设这个问题与您之前的问题 相关,您将需要增强 EnumerableBridge.Add(item As TSource) 来检查是否带有传入 SearchTermCategoryInfo 已经被反序列化并添加,如果是这样,请不要添加重复项。

    如果不这样做,每次往返 XML 时 ListeCategoriesExt 的内容都会重复。

    问题的演示fiddle:https://dotnetfiddle.net/C0CUV4.

  5. 由于Action(Of IList(Of TTarget), TSource)Func(Of TTarget, TSource)无法正确序列化(通过BinaryFormatter),我建议从EnumerableBridge中删除<Serializable>

因此您的 EnumerableBridge 应该如下所示:

' <Serializable> Removed since _converterFrom and _add are not really serializable
Public Class EnumerableBridge(Of TSource, TTarget)
    Implements IEnumerable(Of TSource)

    Private _refCollection As IList(Of TTarget)
    Private _add As Action(Of IList(Of TTarget), TSource)
    Private _converterFrom As Func(Of TTarget, TSource)

    Sub New()
        Throw New NotImplementedException()
    End Sub

    Public Sub New(refCollection As IList(Of TTarget), add As Action(Of IList(Of TTarget), TSource), converterFrom As Func(Of TTarget, TSource))
        _refCollection = refCollection
        _add = add
        _converterFrom = converterFrom
    End Sub

    Public Sub Add(item As TSource) 'Implements ICollection(Of TSource).Add
        _add(_refCollection, item)
    End Sub

    Public Function IEnumerable_GetEnumerator() As IEnumerator Implements IEnumerable.GetEnumerator
        Return GetEnumerator()
    End Function

    Public Function GetEnumerator() As IEnumerator(Of TSource) Implements IEnumerable(Of TSource).GetEnumerator
        Return _refCollection.Select(_converterFrom).GetEnumerator()
    End Function
End Class

OptionsSerializable修改如下:

Public Class OptionsSerializable
    Public Sub New()
    End Sub

    <XmlAnyElement>
    Public Property Unsupported As List(Of XmlElement)

    <System.Xml.Serialization.XmlElement(ElementName:="ListeCategoriesExt")>
    Public ReadOnly Property Categories As List(Of CategoryInfo) = New List(Of CategoryInfo)

    Shared Sub AddCategory(Categories as IList(Of CategoryInfo), Name as String)
        If Not Categories.Any(Function(c) c.SearchTerm = Name) Then
            Categories.Add(New CategoryInfo(Name))
        End If
    End Sub

    Public ReadOnly Property ListeCategories As EnumerableBridge(Of String, CategoryInfo) = New EnumerableBridge(Of String, CategoryInfo)(_categories, AddressOf AddCategory, Function(c) c.SearchTerm)

    ' Remainder unchanged

演示 fiddle 此处:https://dotnetfiddle.net/bfYd1l