4

Thanks for taking a look!

I'm working on a new version of a product that is deployed in the field. I need to maintain the ability to deserialize exiting files from the older software.

Here is a contrived example:

I have a existing customer base with serialized files that they need to access. For the purposes of this question, they have a "Zoo" file with a List of Giraffes in it.

[Serializable]
public class Giraffe
    : ISerializable
{
    public int Age { get; private set; }

    public Giraffe(int age)
    {
        Age = age;
    }

    void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
    {
        info.AddValue("Age", Age);
    }

    [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
    private Giraffe(SerializationInfo info, StreamingContext context)
    {
        Age = info.GetInt32("Age");
    }
}

Now, we are deploying a new version of our "Zoo" software, and we are going to support anaimals other than Giraffes, we should have done this to begin with, but due to time constrains, we had to release 1.0 with a Giraffe-only feature set.

public interface IAnimal
{
    int Age { get; }
}

[Serializable]
public class Animal
    : IAnimal,
    ISerializable
{
    public int Age { get; private set; }

    public Animal (int age)
    {
        Age = age;
    }

    void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
    {
        info.AddValue("Age", Age);
    }

    [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
    private Animal(SerializationInfo info, StreamingContext context)
    {
        Age = info.GetInt32("Age");
    }
}

I'm using a custom serializationBinder to have old Giraffes deserialized as Animals

public class LegacyZooSerializationBinder
    : SerializationBinder
{
    public override Type BindToType(string assemblyName, string typeName)
    {
        if (typeName.StartsWith("Test.Giraffe"))
            return typeof(Animal);
        else if (typeName.StartsWith("System.Collections.Generic.List`1[[Test.Giraffe"))
            return typeof(List<Animal>);
        else return null;
    }
}

The problem is that I wan't to have my Zoo class use a List as it's storage, not a List. I want to do this for two reasons, for future extendability, also, so that I can more easily mock things out for unit testing.

Deserializing the new IAnimal list is no problem. The problem comes when I want to deserialize the old style items. The Binder returns the correct type, the correct deserialization constructor is called, everything looks ok, but the List is actually full of null items. Once the IDeserializationCallback.OnDeserialization callback is called, the list is correct. I can't simply call IEnumerable.ConvertAll<>() on it, because it looks like the serialization framework is trying to find the exact same instance when it comes back to clean everything up, and ConvertAll will create a new list.

I have it working as of now, but I hope someone out there can help me clean this up, as it is not all that maintainable as of now. Here is what it takes to do it:

[Serializable]
public class Zoo
    : ISerializable,
    IDeserializationCallback
{
    List<IAnimal> m_List = null;

    List<Giraffe> m_LegacyList = null; //Just so that we can save an old-style zoo

    //Temp copy of the list
    List<Animal> m_List_Deserialization_Temp_Copy = null;

    public Zoo(bool legacy)
    {
        m_List = new List<IAnimal>();

        if (legacy)
        {
            //Create an old style zoo, just for the example
            m_LegacyList = new List<Giraffe>();
            m_LegacyList.Add(new Giraffe(0));
            m_LegacyList.Add(new Giraffe(1));
        }
        else
        {
            m_List.Add(new Animal(0));
            m_List.Add(new Animal(1));
        }
    }

    public List<IAnimal> List
    {
        get { return m_List; }
    }

    void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
    {
        if(m_LegacyList != null) //Save as an old style list if we have old data
            info.AddValue("list", m_LegacyList);
        else
            info.AddValue("list", m_List);
    }

    [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
    private Zoo(SerializationInfo info, StreamingContext context)
    {
        try
        {
            //New style
            m_List = (List<IAnimal>)info.GetValue("list", typeof(List<IAnimal>));
        }
        catch (InvalidCastException)
        {
            //Old style
            //Put it in a temp list, until the OnDeserialization callback is called, this will be a list, full of null items!
            m_List_Deserialization_Temp_Copy = (List<Animal>)info.GetValue("list", typeof(List<Animal>));
        }
    }

    void IDeserializationCallback.OnDeserialization(object sender)
    {
        if (m_List_Deserialization_Temp_Copy != null)
        {
            m_List = new List<IAnimal>();

            //This works because IEnumerable<Animal> is covariant to IEnumerable<IAnimal>
            m_List.AddRange(m_List_Deserialization_Temp_Copy);
        }
    }
}

Here is a basic test console app that shows serialization and deserialization for both cases:

    static void Main(string[] args)
    {
        {
            var item = new Zoo(false);

            var formatter = new BinaryFormatter();
            var stream = new MemoryStream();

            formatter.Serialize(stream, item);

            stream.Position = 0;

            formatter.Binder = new LegacyZooSerializationBinder();

            var deserialized = (Zoo)formatter.Deserialize(stream);

            Debug.Assert(deserialized.List.Count == 2);
            Debug.Assert(deserialized.List[0] != null);
            Debug.Assert(deserialized.List[0].Age == 0);

            Debug.Assert(deserialized.List[1] != null);
            Debug.Assert(deserialized.List[1].Age == 1);

            Console.WriteLine("New-style Zoo serialization OK.");
        }

        {
            var item = new Zoo(true);

            var formatter = new BinaryFormatter();
            var stream = new MemoryStream();

            formatter.Serialize(stream, item);

            stream.Position = 0;

            formatter.Binder = new LegacyZooSerializationBinder();

            var deserialized = (Zoo)formatter.Deserialize(stream);

            Debug.Assert(deserialized.List.Count == 2);
            Debug.Assert(deserialized.List[0] != null);
            Debug.Assert(deserialized.List[0].Age == 0);

            Debug.Assert(deserialized.List[1] != null);
            Debug.Assert(deserialized.List[1].Age == 1);

            Console.WriteLine("Old-style Zoo serialization OK.");
        }

        Console.ReadKey();
    }

Any suggestions would be greatly appreciated. I'm having a hard time finding good resources on this type of thing. Thanks!

Brandon
  • 1,239
  • 9
  • 17
  • 2
    +1 for "Giraffe-only feature set". I enjoyed that. – Chris Shain Mar 13 '12 at 19:20
  • 1
    Have you considered doing a one-time giraffe-to-animal conversion at install time (with a backup of course) to save you the headache of having to maintain a giraffe special case for eternity? – Chris Shain Mar 13 '12 at 19:22
  • Chris, I hadn't considered a one-shot conversion. It would obviously clean things up in the code quite a bit. I'd prefer to keep it transparent to the user, especially since it's a back-end change. If all else fails, that could be a really good solution. – Brandon Mar 13 '12 at 19:37
  • Alright, well, I will put it in as an answer in case you end up going that way – Chris Shain Mar 13 '12 at 19:40

1 Answers1

3

Consider doing a one time conversion from the old files to the new format, preferably at install time and definitely after backing them up. That way you dont have to support this weird one-off serialization forever, and your codebase becomes drastically simpler.

Chris Shain
  • 50,833
  • 6
  • 93
  • 125