What you need to know when you XmlSerialize classes derived from classes derived from CollectionBase

I started looking at the XmlSerializer when someone asked me to write a chapter for a book that he was working on. I wrote that chapter, started answering questions on the microsoft public newgroups, wrote another chapter and answered more question. Ultimately, the book was cancelled, but I'm still answering serialization questions in the microsoft.public.dotnet.xml newsgroup on news.microsoft.com. Some of the material also made it to www.topxml.com as a number of tutorials. If you are looking for an introduction to serializing classes from CollectionBase, please check there. This article documents one anomaly related to building a hierarchy of classes that derive from CollectionBase and Xml serialization.

So far I've managed to come up with a solution to most serialization problems. However, last week a co-worker of mine asked for some help with serializing collections. Easy, or so I thought, all he wanted was to serialize and deserialize a collection derived from CollectionBase ... and classes derived from that. Serialization goes just fine, but once you try to deserialize things are starting to get wicked. Take a look for yourself. Let's start with a custom collection and another class deriving from it(this is VB.NET since that particular client asked for it):

(FubaredCollection))> _
Public Class
CollectionBaseWithDefaultAccessor
Inherits
CollectionBase

Public Sub New()
End Sub

Public Shadows Function Add(ByVal value As Object) As Integer
 
Return
list.Add(value)
End Function

_
Default Overloads Property Item(ByVal index As Integer) As
Object
  Get
   
Return
List.Item(index)
  End
Get
  Set(ByVal Value As Object
)
    List.Item(index) = Value
  End
Set
End
Property
End
Class

Public Class FubaredCollection
  Inherits
CollectionBaseWithDefaultAccessor

  Public Sub New()
  End Sub

  Public Shadows Function Add(ByVal item As Fubar) As Integer
    Return
List.Add(item)
  End Function

  Default Overloads Property Item(ByVal index As Integer) As Fubar
    Get
      Return CType
(List.Item(index), Fubar)
    End
Get
    Set(ByVal Value As
Fubar)
      List.Item(index) = Value
    End
Set
  End
Property
End
Class

Serializing an instance of the derived class FubaredCollection with an XmlSerializer instantiated for the base typeCollectionBaseWithDefaultAccessor results in an Xml document like this. See if you can spot the problem:


 
    ...
 

Look at the root element and the element created for an object in the collection. You see that XmlSerializer recorded the type of the serialized object that was in referenced by the collection with an xsi:type=”Fubar” attribute. This is necessary because the XmlSerialzier cannot infer an object type to desrializate from the anyType element name. anyType stands for the type object. On the other hand, there is no xsi:type attribute on the root element ArrayOfAnyType, therefore the XmlSerializer will not deserialize a FubaredCollection. It will create a CollectionBaseWithDefaultAccessor, which is the base class of FubaredCollection.

This problem show its full ugliness when a class has a field defined as the base class, CollectionBaseWithDefaultAccessor, but carries an instance of a derived class.In our case we had a property definition like this:

Private m_Collection As CollectionBaseWithDefaultAccessor

(FubaredCollection))> _
Public Property Collection() As
CollectionBaseWithDefaultAccessor
Get
 
Return
m_Collection
End
Get
Set(ByVal Value As CollectionBaseWithDefaultAccessor)
  m_Collection = Value
End
Set
End Property

which correctly serializes to:


 

   ...
 


In this case the XmlSerializer does have enough information to properly deserialize the Xml. The element unambiguously maps the FubaredCollection type, because the XmlElementAttribute defines the name only for FubaredCollection objects. Also, the element name maps to the Fubar class, so you would think we're in better shape than when we serialized the FubaredCollection on its own.

Well, not quite! Deserializing the generated Xml results in an exception with this message:

System.InvalidOperationException:

There is an error in XML document (1, 187). ---> System.InvalidCastException: Specified cast is not valid.
   at Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationReader1.Read1_CollectionWrapper(Boolean isNullable, Boolean checkType) in d:\Documents and Settings\csc\Local Settings\Temp\dysasxuv.0.cs:line 145

Since I had read about the great XmlSerializer debugging helper disclosed by Doug Purdy I was going to check what causes the exception:

The code generated by the XmlSerializer to deserialize Xml does not check the element name when it instantiates the collection object (on line 122). It simply instantiates an object of the defined base class type. Later on, it does check the element name, see the element and tries to cast the previously generated base class object to the derived class on line 145) and that's what causes the exception.

Unfortunately, we didn't find a good work-around for it. The choice was not to go forward with the design of stronger typed collections derived from a weaker typed collection base class that derives from CollectionBase, but I did want to document the problem for the great guys working on the XmlSerializer (Doug, Matt, Stefan and everybody else). I hope we will get this fixed in Whidbey!

No Comments