Question

NOTE: This question changed a little as I learned more about the problem, so please read it in its entirety. I've decided to leave it in its original form as it better describes how the problem was discovered and ultimately resolved.


Way back in the darkest depths of our project's history when we didn't really understand C# or the CLR as well as we might've, we created a type, let's call it MyType. We created this type as a class, a reference type.

However, it became apparent that MyType should be a struct, a value type so we made some changes to make it so and all was well until one day, we tried to deserialize some data that contained a collection of MyType values. Well, not really, for when it was a reference type, that collection was a collection of references. Now when it deserializes, the collection deserializes fine, using the default constructor of MyType, then later when the actual references deserialize, they are orphaned, leaving us with a collection of empty values.

So, we thought, "let's map the type to a reference type, MyTypeRef on load so that the references properly resolve, then convert back to our real type for use during execution time and reserialization". So we did (using our own binder), but alas, it didn't work because now we get an error that tells us MyTypeRef[] can't be converted to MyType[] even if we have an implicit conversion between MyTypeRef and MyType.

So, we're stuck. How do we get a collection serialized as a collection of reference type MyType to deserialize as a collection of value type MyType?

Update
Some investigation (see comments and code below) has shown that it is the immutable nature of the new MyType and its used of ISerializable for serialization that has caused the real issue. I still don't see why that should be the case, but if I use private set accessors instead of ISerializable, the new MyType will load the old (note that the ISerializable interface is called if I use it but the collection only even contains default values).

Some code

// Use a List<T> in a class that also implements ISerializable and
// save an instance of that class with a BinaryFormatter (code omitted)

// Save with this one.
[Serializable]
public class MyType
{
    private string test;
    public string Test
    {
        get { return this.test; }
        set { this.test = value; }
    }

    public MyType()
    {
    }
}

// Load with this one.
[Serializable]
public class MyType : ISerializable
{
    private string test;
    public string Test
    {
        get { return this.test; }
        set { this.test = value; }
    }

    public MyType()
    {
    }

    public MyType(SerializationInfo info, StreamingContext context)
    {
        info.AddValue("test", this.test);
    }

    public void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        this.test = info.GetString("test");
    }
}

Note that when loading, the elements are all nulls. Remove ISerializable from the second definition and load and all works. Change class to struct on the load code and the same behaviour is visible. It is as though the collection will only deserialize successfully using the set accessors.

Update Two
So, I found the problem (see my answer below) but I doubt anyone would've known the answer from reading my question. I missed out an important detail that I didn't even realise was important at the time. My sincerest apologies to those who tried to help.

The collection that is loaded containing MyType is immediately copied to another collection during GetObjectData. The answer below explains why this turns out to be important. Here is some additional sample code to that given above that should provide a complete example:

public void GetObjectData(SerializationInfo info, StreamingContext context)
{
    this.myTypeCollection = new List<MyType>();
    var loadedCollection = (List<MyType>)info.GetValue(
        "myTypeCollection",
        typeof(List<MyType>));
    this.myTypeCollection.AddRange(loadedCollection);
}
Was it helpful?

Solution 2

I feel somewhat foolish now but I discovered the issue. I have updated the question with additional sample code.

Here is the basics of what the code was doing in the GetObjectData method:

  1. Load the collection
  2. Copy the collection into another collection

Here is the sequence that occurred at execution:

  1. Collection loaded but elements are not
  2. Copied null/default elements to another collection and discarded loaded collection variable
  3. Elements of loaded collection are deserialized

Note that it is only after point 3 that the entire collection is deserialized but I have already done my copy, hence having nothing.

The fix was to use the OnDeserialized attribute to specify a method to call once the entire object was deserialized. Then, in GetObjectData I save away a reference to the collection that is loaded, but copy its contents in the OnDeserialized method once it is fully deserialized.

Side note
In fact, to keep the deserialization code all in GetObjectData, I saved away a delegate to perform the copy and I call that delegate later - that way it is clear in GetObjectData exactly what will happen to finish deserialization.

Code

public void GetObjectData(SerializationInfo info, StreamingContext context)
{
    var loadedCollection = (List<MyType>)info.GetValue(
        "myTypeCollection",
        typeof(List<MyType>));

    this.myTypeCollectionLoader = () =>
    {
        this.myTypeCollection.AddRange(loadedCollection);
    };
}

[OnDeserialized]
private void OnDeserialized(StreamingContext context)
{
    this.myTypeCollectionLoader();
    this.myTypeCollectionLoader = null;
}

OTHER TIPS

Not sure to understand, but if I do this on save:

[Serializable]
public class MyTypeColl
{
    public MyTypeColl()
    {
    }

    public List<MyType> Coll { get; set; }
}

[Serializable]
public class MyType
{
    private string test;
    public string Test
    {
        get { return this.test; }
        set { this.test = value; }
    }

    public MyType()
    {
    }
}

// save code

    MyTypeColl coll = new MyTypeColl();
    coll.Coll = new List<MyType>();
    coll.Coll.Add(new MyType{Test = "MyTest"});

    BinaryFormatter bf = new BinaryFormatter();
    using (FileStream stream = new FileStream("test.bin", FileMode.OpenOrCreate))
    {
        bf.Serialize(stream, coll);
    }

And this on load:

[Serializable]
public struct MyType
{
    private string test;
    public string Test
    {
        get { return this.test; }
        set { this.test = value; }
    }
}

// load code

    BinaryFormatter bf = new BinaryFormatter();
    using (FileStream stream = new FileStream("test.bin", FileMode.Open))
    {
        MyTypeColl coll = (MyTypeColl)bf.Deserialize(stream);
        Console.WriteLine(coll.Coll[0].Test);
    }

It displays "MyTest" successfuly. So what am I missing?

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top