Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dictionary is empty on deserialization

I'm currently writing a bidirectional map class, and I'm having some troubles with the serialization/deserialization of the class (question at bottom).

Here's the parts of the class that's relevant.

/// <summary>
/// Represents a dictionary where both keys and values are unique, and the mapping between them is bidirectional.
/// </summary>
/// <typeparam name="TKey"> The type of the keys in the dictionary. </typeparam>
/// <typeparam name="TValue"> The type of the values in the dictionary. </typeparam>
[Serializable]
public class BidirectionalDictionary<TKey, TValue> : IDictionary<TKey, TValue>, IEquatable<BidirectionalDictionary<TKey, TValue>>, ISerializable, IDeserializationCallback
{

        /// <summary>
        /// A dictionary that maps the keys to values.
        /// </summary>
        private readonly Dictionary<TKey, TValue> forwardMap;

        /// <summary>
        /// A dictionary that maps the values to keys.
        /// </summary>
        private readonly Dictionary<TValue, TKey> inverseMap;

        /// <summary>
        /// An instance of the dictionary where the values are the keys, and the keys are the values. 
        /// </summary>
        private readonly BidirectionalDictionary<TValue, TKey> inverseInstance;

        /// <summary>
        /// Initializes a new instance of the dictionary class with serialized data. </summary>
        /// </summary>
        /// <param name="info"> The serialization info. </param>
        /// <param name="context">  The sserialization context. </param>
        protected BidirectionalDictionary(SerializationInfo info, StreamingContext context)
        {
            this.forwardMap = (Dictionary<TKey, TValue>)info.GetValue("UnderlyingDictionary", typeof(Dictionary<TKey, TValue>));
            this.inverseMap = new Dictionary<TValue, TKey>(
                forwardMap.Count,
                (IEqualityComparer<TValue>)info.GetValue("InverseComparer", typeof(IEqualityComparer<TValue>)));

            // forwardMap is always empty at this point.
            foreach (KeyValuePair<TKey, TValue> entry in forwardMap)
                inverseMap.Add(entry.Value, entry.Key);

            this.inverseInstance = new BidirectionalDictionary<TValue, TKey>(this);
        }

        /// <summary>
        /// Gets the data needed to serialize the dictionary.
        /// </summary>
        /// <param name="info"> The serialization info. </param>
        /// <param name="context">  The serialization context. </param>
        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.AddValue("UnderlyingDictionary", forwardMap);
            info.AddValue("InverseComparer", inverseMap.Comparer);
        }
 }

Since the forward- and inverseMap dictionaries contain the exact same data, my idea was to only serialize one of them (forwardMap), and then build the other (inverseMap) from it's data on deserialization. However, the inverseMap isn't getting populated with any data in the deserialization constructor. It seems the forwardMap dictionary is only completely deserialized after the class' deserialization constructor has already executed.

Any idea on how to fix this?

like image 390
Henrik Avatar asked Aug 30 '14 13:08

Henrik


1 Answers

I am assuming you are using BinaryFormatter.

BinaryFormatter is a graph serializer. Rather than objects being stored in a pure tree, they are assigned temporary object ids and stored as they are encountered. Thus when an object is deserialized, it is not guaranteed that all referenced objects have been previously deserialized. Thus it's possible the entries in your forwardMap have not been filled in yet.

The normal workaround is to add IDeserializationCallback logic to your class, and build your inverseMap and inverseInstance after everything has been deserialized in the OnDeserialization method. But, Dictionary<TKey, TValue> also implements IDeserializationCallback, which introduces an additional sequencing problem: it is not guaranteed to have been called before yours is. On this topic, Microsoft writes:

Objects are reconstructed from the inside out, and calling methods during deserialization can have undesirable side effects, since the methods called might refer to object references that have not been deserialized by the time the call is made. If the class being deserialized implements the IDeserializationCallback, the OnSerialization method will automatically be called when the entire object graph has been deserialized. At this point, all the child objects referenced have been fully restored. A hash table is a typical example of a class that is difficult to deserialize without using the event listener described above. It is easy to retrieve the key/value pairs during deserialization, but adding these objects back to the hash table can cause problems since there is no guarantee that classes that derived from the hash table have been deserialized. Calling methods on a hash table at this stage is therefore not advisable.

Thus there are a couple things you could do:

  1. Rather than storing a Dictionary<TKey,TValue>, store an array of KeyValuePair<TKey,TValue>. This has the advantage of making your binary data simpler but does require you to allocate the array in your GetObjectData() method.

  2. Or follow the advice in the dictionary reference source:

    // It might be necessary to call OnDeserialization from a container if the container object also implements
    // OnDeserialization. However, remoting will call OnDeserialization again.
    // We can return immediately if this function is called twice. 
    // Note we set remove the serialization info from the table at the end of this method.
    

    I.e. in your callback, call the OnDeserialization method of your nested dictionary before using it:

    public partial class BidirectionalDictionary<TKey, TValue> : IDeserializationCallback
    {
        public void OnDeserialization(object sender)
        {
            this.forwardMap.OnDeserialization(sender);
            foreach (KeyValuePair<TKey, TValue> entry in forwardMap)
            {
                this.inverseMap.Add(entry.Value, entry.Key);
            }
            // inverseInstance will no longer be able to be read-only sicne it is being allocated in a post-deserialization callback.
            this.inverseInstance = new BidirectionalDictionary<TValue, TKey>(this);
        }
    

    (You could do it in an [OnDeserialied] method instead if you prefer.)

Incidentally, this blog post claims that it is safe to call the OnDeserialization method of a HashTable from the deserialization constructor of a containing class, rather than later from OnDeserialization, so you might give that a try.

like image 192
dbc Avatar answered Nov 03 '22 07:11

dbc