Search code examples
.netvb.netxmlserializer

IEnumerable(of String) ignored by XmlSerializer


For one of my object, I need to do some backward/forward compatibility tricks so that everyone is happy while different versions coexist and share the same config file.

The last trick I tried is to have a bridge collection that maps the old format to the new format using an IEnumerable(Of String). My problem is that I can't get XmlSerializer to recognize my custom collection. I looked at the serializer code generating code and everything seems fine. No matter what I do, as long as I use my custom collection ListeCategories ends up in Unsupported. If I use a basic List(Of String), it works but I miss the logic. What am I missing?

I also tried a non generic custom collection (commented in the second code snippet) with no more success.

Here are the codes for both the main object and the custom collection. Note that the custom enumerable implements and additional Add method as required by XmlSerializer (according to MS Docs).

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

Solution

  • You have a few issues here:

    1. Your property ListeCategories must be declared as the actual, concrete type returned, not an interface. XmlSerializer does not serialize properties declared as interfaces even when preallocated.

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

    2. Your type EnumerableBridge(Of TSource, TTarget) must implement GetEnumerator() As IEnumerator(Of TSource) directly (using the name GetEnumerator()) and implement GetEnumerator() As IEnumerator explicitly, using some other name.

      In your class you do the opposite and implement the generic method explicitly, which causes an odd exception with an unhelpful message to be thrown:

      [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).]
      

      Demo fiddle of the problem here: https://dotnetfiddle.net/PhaeZc

    3. You can throw a NotImplementedException() from the parameterless constructor of EnumerableBridge(), since XmlSerializer won't actually ever call it.

    4. Assuming this question relates back to your earlier question .NET - Is it possible to use both XmlAnyElementAttribute and XmlSerializer.UnknownElement event within the same object, you will need to enhance EnumerableBridge.Add(item As TSource) to check to see whether a CategoryInfo with the incoming SearchTerm was already deserialized and added, and if so, do not add a duplicate.

      If you don't, the contents of the ListeCategoriesExt will get duplicated every time you round-trip to XML.

      Demo fiddle of the problem here: https://dotnetfiddle.net/C0CUV4.

    5. Since Action(Of IList(Of TTarget), TSource) and Func(Of TTarget, TSource) cannot be properly serialized (by BinaryFormatter), I recommend removing <Serializable> from EnumerableBridge.

    Thus your EnumerableBridge should look like:

    ' <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
    

    And OptionsSerializable should be modified as follows:

    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
    

    Demo fiddle here: https://dotnetfiddle.net/bfYd1l