I am performing serialization using System.Runtime.Serialization.Formatters.Binary.BinaryFormatter. I have a collection which wraps a List. When I deserialize the collection (which deserializes the List), all of the List‘s elements are null if any of the elements implements ISerializable, except those items which I serialize alongside the List. Here’s a test case:
Main
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
static class Program {
static void Main(string[] args) {
var collection = new PluginCollection();
collection.Add(new NullPlugin());
collection.Add(new NotNullPlugin());
Clone(collection);
}
private static byte[] Serialize(object obj, IFormatter formatter) {
using (var writer = new MemoryStream()) {
formatter.Serialize(writer, obj);
return writer.GetBuffer().Take((int) writer.Length).ToArray();
}
}
private static object Deserialize(byte[] data, IFormatter formatter) {
using (var stream = new MemoryStream(data)) {
return formatter.Deserialize(stream);
}
}
public static T Clone<T>(T obj) {
var cloner = new BinaryFormatter {
Context = new StreamingContext(StreamingContextStates.Clone)
};
return (T) Deserialize(Serialize(obj, cloner), cloner);
}
}
IPlugin and implementors
interface IPlugin {
}
[Serializable]
sealed class NullPlugin : IPlugin, ISerializable {
public NullPlugin() {
}
private NullPlugin(SerializationInfo info, StreamingContext context) {
}
public void GetObjectData(SerializationInfo info, StreamingContext context) {
}
}
[Serializable]
sealed class NotNullPlugin : IPlugin {
}
PluginCollection
[Serializable]
sealed class PluginCollection : ISerializable {
private readonly IList<IPlugin> plugins = new List<IPlugin>();
public PluginCollection() {
}
private PluginCollection(SerializationInfo info, StreamingContext context) {
var plugins = (IEnumerable<IPlugin>) info.GetValue("Plugins", typeof(IEnumerable<IPlugin>));
//var plugin = (IPlugin) info.GetValue("Plugin", typeof(IPlugin));
System.Diagnostics.Debug.Assert(!plugins.Any((p) => p == null));
AddRange(plugins);
//Add(plugin);
}
public void GetObjectData(SerializationInfo info, StreamingContext context) {
info.AddValue("Plugins", this.plugins);
//info.AddValue("Plugin", this.plugins.First());
}
public void Add(IPlugin plugin) {
if (plugin == null) {
throw new ArgumentNullException("plugin");
}
this.plugins.Add(plugin);
}
private void AddRange(IEnumerable<IPlugin> plugins) {
if (plugins == null) {
throw new ArgumentNullException("plugins");
}
foreach (var plugin in plugins) {
Add(plugin);
}
}
}
PluginCollection is the (stripped-down) collection with the List. NullPlugin is an IPlugin which implements ISerializable, and NotNullPlugin is an IPlugin which does not.
- Leaving the code alone, the deserialized collection its
{ null, null }. - Uncommenting the lines in
PluginCollectioncausesNullPluginto be successfully deserialized, whileNotNullPluginis still not deserialized. The collection is{ NullPlugin, null }. -
Commenting the
collection.Add(new NullPlugin());line inMaincausesNotNullPluginto be successfully deserialized. The collection is{ NotNullPlugin }. -
When
NullPluginis not successfully serialized, the private deserialization constructor is never called. -
Serializing the
Listdirectly (e.g. by makingPluginCollection.pluginspublic and cloningcollection.pluginsinMain) produces the expected results (i.e. deserialization is successful and there are no null elements in the returned list).
I expect the collection of the current code to return { NullPlugin, NotNullPlugin }, and I see no reason why it shouldn’t. What could be causing the failure?
(This problem occurs outside of directly cloning the object, but cloning is the easiest way to show this bug.)
The first thing I see is the assumption that SerializationInfo.GetValue would return the initialized list when called in the deserialization constructor. This is not the case, you’re only receiving an reference to a list that will be populated when everything’s done. You may want to use the IDeserializationCallback interface which triggers when the entire object graph has been deserialized.
What if the list contains your instance, how would it be fully instantiated before calling your constructor?