4

I'd like to implement ISerializable for a C# class which contains a list of similar typed children. Consider the following example:

using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;

namespace serialisation
{
    [Serializable]
    internal class Nested : ISerializable
    {
        public string Name { get; set; }

        public List<Nested> Children { get; set; }

        public Nested(string name)
        {
            Name = name;
            Children = new List<Nested>();
        }

        protected Nested(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context)
        {
            Name = info.GetString("Name");

            // This doesn't work:
            Nested[] children = (Nested[])info.GetValue("Children", typeof(Nested[]));
            Children = new List<Nested>(children);

            // This works:
            // Children = (List<Nested>)info.GetValue("Children", typeof(List<Nested>));
        }

        public void GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context)
        {
            info.AddValue("Name", Name);

            // This doesn't work:
            info.AddValue("Children", Children.ToArray());

            // This works:
            //info.AddValue("Children", Children);
        }
    }

    internal class Program
    {
        private static void Main(string[] args)
        {
            // Generate a hierarchy
            Nested root = new Nested("root");
            Nested child1 = new Nested("child1");
            Nested child2 = new Nested("child2");
            Nested child3 = new Nested("child3");
            child1.Children.Add(child2);
            child1.Children.Add(child3);
            root.Children.Add(child1);

            Nested deserialized;
            BinaryFormatter binaryFmt = new BinaryFormatter();

            // Serialize
            using (var fs = new FileStream("Nested.xml", FileMode.OpenOrCreate))
            {
                binaryFmt.Serialize(fs, root);
            }

            // Deserialize
            using (var fs = new FileStream("Nested.xml", FileMode.OpenOrCreate))
            {
                deserialized = (Nested)binaryFmt.Deserialize(fs);
            }

            // deserialized.Children contains one null child
            Console.WriteLine("Original Name: {0}", root.Name);
            Console.WriteLine("New Name: {0}", deserialized.Name);
        }
    }
}

In the sample above, Nested.GetObjectData and the serializer constructor for Nested are invoked 4 times, one after another.

Adding the children to the serializer as a Nested array will return a correctly sized array on de-serialization, but all the elements will be null.

However, changing the type from Nested array to Nested List will magically fix up the null elements after the constructors for the children have been called.

What I'd like to know is:

  1. What's special about Nested List?
  2. What is the recommended way to serialize a class with a recursive structure such as this?

Update:

It seems there is an additional interface, IDeserializationCallback.OnDeserialization, which is called after de-serialization has taken place (the calling order is non-deterministic). You can store the de-serialized array in a temp member variable in the constructor and then assign it to a list in this method. Unless I'm missing something, this seems less than ideal as you must clutter your implementation with temp vars.

  • I believe this is a similar question that has been answered: http://stackoverflow.com/questions/4339602/deserialization-of-an-array-always-gives-an-array-of-nulls – o_weisman Nov 12 '15 at 09:52
  • I found the same question a few minutes ago, too. I must not have been specific enough with my earlier search terms... – ChocolatePocket Nov 12 '15 at 10:04

1 Answers1

0

I'd go with the composite pattern. The solution below solves both the BinaryFormatter (like in your Main) and the XmlSerializer approach, if you were to use that instead. Composite and Component replace your Nested class.

[Serializable()]
[XmlRoot("component", Namespace="", IsNullable=false)]
public partial class CT_Component 
{
    [XmlAttribute("name")]
    public string Name { get; set;}
}

[Serializable()]
[XmlRoot("composite", Namespace="", IsNullable=false)]
public partial class CT_Composite 
{
    [XmlElement("component", typeof(CT_Component))]
    [XmlElement("composite", typeof(CT_Composite))]
    public object[] Items { get; set; }

    [XmlAttribute("name")]
    public string Name { get; set; }
}

I created those from the following xsd, I always go from xsd to generated classes since I cannot ever get the attribute decoration right. The gist of it is the recursive CT_Composite type:

<xs:element name="component" type="CT_Component" />
<xs:element name="composite" type="CT_Composite" />
<xs:complexType name="CT_Component">
  <xs:attribute name="name" type="xs:string" use="required" />
</xs:complexType>
<xs:complexType name="CT_Composite" >
  <xs:choice minOccurs="1" maxOccurs="unbounded">
    <xs:element ref="component" />
    <xs:element name="composite" type="CT_Composite" />
  </xs:choice>
  <xs:attribute name="name" type="xs:string" use="required" />
</xs:complexType>

Serialization code is the same. Declaration of variable:

var composite = new CT_Composite() {
            Name = "root",
            Items = new object[] {
                new CT_Composite() {
                    Name = "child1",
                    Items = new object[] {
                        new CT_Component() {Name="child2"},
                        new CT_Component() {Name="child3"}
                    } } } };

Should you be even more orthodox about the pattern, you can use:

[Serializable()]
[XmlRoot("component", Namespace="", IsNullable=false)]
public class Component {
    [XmlAttribute("name")] public string Name { get; set;}
}

[Serializable()]
[XmlRoot("composite", Namespace="", IsNullable=false)]
public class Composite : Component {
    [XmlElement("component", typeof(Component))]
    [XmlElement("composite", typeof(Composite))]
    public object[] Items { get; set; }
}
andrei.ciprian
  • 2,895
  • 1
  • 19
  • 29