1

I have some objects that must be serialized:

class Displayable{
  string name;
  Sprite icon;
}

The icon field requires custom serialization since Sprites are already serialized (in different files, with their own format, by a game engine) and I only need to store a way to reference them (let's say a string, being the key inside a Dictionary<string, Sprite>).

Using BinaryFormatter I tried implementing ISerializable and ISerializationSurrogate but both of these methods create a new instance of the Sprite object on deserialization so they are not suitable for my case. I would like to have the same functionality of ISerializationSurrogate except I don't want the first parameter in SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector) because I need to return an object I already have in memory instead of receiving a new instance from the deserializer and fill it with data.

I also tried external libraries like SharpSerializer but I don't like the constraints of having a public parameterless constructor and it doesn't really let you customize the serialization of special classes. I've read about ProtoBuffers but that doesn't support inheritance and I need it somewhere else.

So my requirements are:

  • Inheritance support
  • Being able to define custom serialization for some types
  • Deserialization of those custom types shouldn't create instances on it's own

Is there any library doing this?

Otherwise, am I being too picky? How do you usually achieve serialization of references to objects stored somewhere else?

Thank you in advance.

Edit: Here's what I'd like to have

public class SerializableSprite : ISerializationSurrogate
{
  public static Dictionary<string, Sprite> sprites;
  public void GetObjectData(object obj, SerializationInfo info, StreamingContext context) {
    Sprite sprite = obj as Sprite;
    info.AddValue("spriteKey", sprite.name);
  }
  // The first parameter in this function is a newly instantiated Sprite, which I don't need
  public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector) {
    return sprites[info.GetString("spriteKey")];
  }
}
dbc
  • 104,963
  • 20
  • 228
  • 340
dogiordano
  • 691
  • 6
  • 13
  • Have you actually tried binary serialization (through `BinaryFormmatter`)? It covers al your requirements. For instance, if the `Sprite` instance is shared between several object being serialized/deserialized, it will be serialized/deserialized just once - you don't need any custom logic for that. – Ivan Stoev Mar 14 '17 at 19:30
  • Json.NET supports this via [`PreserveReferencesHandling`](http://www.newtonsoft.com/json/help/html/PreserveReferencesHandlingObject.htm) and [`TypeNameHandling`](http://www.newtonsoft.com/json/help/html/SerializeTypeNameHandling.htm). `DataContractSerializer` supports this via [`[DataContract(IsReference=true]`](https://msdn.microsoft.com/en-us/library/hh241056.aspx) and [data contract known types](https://msdn.microsoft.com/en-us/library/ms730167(v=vs.110).aspx). But can you give details about what you have tried so far? Your question seems like a recommendation question. – dbc Mar 14 '17 at 19:36
  • I know BinaryFormatter supports serialization by reference. In my case, Sprites are serialized somewhere else, meaning in different files and with a different format (by a game Engine, not my code). I just ensure to have them loaded BEFORE deserializing references to them. I also know how to manually serialize and deserialize these references, the only problem is I don't want new instances of Sprites on deserialization since I already have them in memory. – dogiordano Mar 14 '17 at 20:00
  • @dbc I tried implementing `ISerializable` and `ISerializationSurrogate` but both of these methods create a new instance of the Sprite object on deserialization so they are not suitable for my case. I would like to have the same functionality of `ISerializationSurrogate` except I don't want the first parameter in `SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector)` because I need to return an object I already have in memory instead of receiving a new instance from the deserializer and fill it with data. – dogiordano Mar 14 '17 at 20:13
  • @dogiordano - **I tried implementing `ISerializable` and `ISerializationSurrogate`** - what serializer were you using when you did this? Are you asking for recommendations for a replacement serializer or asking for help in correctly implementing `ISerializationSurrogate`? If the latter, can you [edit] your question to show what serializer you are currently using and what you tried that failed? – dbc Mar 14 '17 at 20:44
  • I was using the `BinaryFormatter`. I've read how to implement `ISerializationSurrogate`, I just don't want it to create an instance on deserialization. So if there's a way to avoid this, I'd like to know that way. Otherwise, I'm asking for a replacement library. The point really is I don't want the Deserializer (whichever it is) to create an instance of the deserialized object since I didn't really serialized it on serialization. – dogiordano Mar 14 '17 at 21:29
  • @dogiordano - then you might want to narrow down your question into something specific about `BinaryFormatter`, and include a full [mcve] so we don't have to reconstruct your problem. The general idea should be to use a surrogate class that implements `IObjectReference` as is shown in option 2 of [this answer](https://stackoverflow.com/a/30702847/3744182). – dbc Mar 14 '17 at 22:28

1 Answers1

4

In order to prevent BinaryFormatter from creating a new instance of Sprite during deserialization, during serialization you can call SerializationInfo.SetType(Type) to specify alternate type information -- typically some proxy type -- to insert into the serialization stream. During deserialization SetObjectData() will be passed an instance of the proxy rather than the "real" type to initialize. This proxy type must in turn implement IObjectReference so that the "real" object can eventually be inserted into the object graph, specifically by looking it up in your table of sprites.

The following does this:

class ObjectReferenceProxy<T> : IObjectReference
{
    public T RealObject { get; set; }

    #region IObjectReference Members

    object IObjectReference.GetRealObject(StreamingContext context)
    {
        return RealObject;
    }

    #endregion
}

public sealed class SpriteSurrogate : ObjectLookupSurrogate<string, Sprite>
{
    static Dictionary<string, Sprite> sprites = new Dictionary<string, Sprite>();
    static Dictionary<Sprite, string> spriteNames = new Dictionary<Sprite, string>();

    public static void AddSprite(string name, Sprite sprite)
    {
        if (name == null || sprite == null)
            throw new ArgumentNullException();
        sprites.Add(name, sprite);
        spriteNames.Add(sprite, name);
    }

    public static IEnumerable<Sprite> Sprites
    {
        get
        {
            return sprites.Values;
        }
    }

    protected override string GetId(Sprite realObject)
    {
        if (realObject == null)
            return null;
        return spriteNames[realObject];
    }

    protected override Sprite GetRealObject(string id)
    {
        if (id == null)
            return null;
        return sprites[id];
    }
}

public abstract class ObjectLookupSurrogate<TId, TRealObject> : ISerializationSurrogate where TRealObject : class
{
    public void Register(SurrogateSelector selector)
    {
        foreach (var type in Types)
            selector.AddSurrogate(type, new StreamingContext(StreamingContextStates.All), this);
    }

    IEnumerable<Type> Types
    {
        get
        {
            yield return typeof(TRealObject);
            yield return typeof(ObjectReferenceProxy<TRealObject>);
        }
    }

    protected abstract TId GetId(TRealObject realObject);

    protected abstract TRealObject GetRealObject(TId id);

    #region ISerializationSurrogate Members

    public void GetObjectData(object obj, SerializationInfo info, StreamingContext context)
    {
        var original = (TRealObject)obj;
        var id = GetId(original);
        info.AddValue("id", id);
        // use Info.SetType() to force the serializer to construct an object of type ObjectReferenceWrapper<TRealObject> during deserialization.
        info.SetType(typeof(ObjectReferenceProxy<TRealObject>));
    }

    public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector)
    {
        // Having constructed an object of type ObjectReferenceWrapper<TRealObject>, 
        // look up the real sprite using the id in the serialization stream.
        var wrapper = (ObjectReferenceProxy<TRealObject>)obj;
        var id = (TId)info.GetValue("id", typeof(TId));
        wrapper.RealObject = GetRealObject(id);
        return wrapper;
    }

    #endregion
}

Then apply it to a BinaryFormatter as follows:

        var selector = new SurrogateSelector();
        var spriteSurrogate = new SpriteSurrogate();
        spriteSurrogate.Register(selector);

        BinaryFormatter binaryFormatter = new BinaryFormatter(selector, new StreamingContext());

Sample fiddle.

Update

While the code above works in .Net 3.5 and above, it apparently does not work in despite compiling successfully there because of some problem with IObjectReference. The following also works in .Net 3.5 and above and also avoids the use of IObjectReference by returning the real object from ISerializationSurrogate.SetObjectData(). Thus it should work in unity3d as well (confirmed in comments):

public sealed class SpriteSurrogate : ObjectLookupSurrogate<string, Sprite>
{
    static Dictionary<string, Sprite> sprites = new Dictionary<string, Sprite>();
    static Dictionary<Sprite, string> spriteNames = new Dictionary<Sprite, string>();

    public static void AddSprite(string name, Sprite sprite)
    {
        if (name == null || sprite == null)
            throw new ArgumentNullException();
        sprites.Add(name, sprite);
        spriteNames.Add(sprite, name);
    }

    public static IEnumerable<Sprite> Sprites
    {
        get
        {
            return sprites.Values;
        }
    }

    protected override string GetId(Sprite realObject)
    {
        if (realObject == null)
            return null;
        return spriteNames[realObject];
    }

    protected override Sprite GetRealObject(string id)
    {
        if (id == null)
            return null;
        return sprites[id];
    }
}

public abstract class ObjectLookupSurrogate<TId, TRealObject> : ISerializationSurrogate where TRealObject : class
{
    class SurrogatePlaceholder
    {
    }

    public void Register(SurrogateSelector selector)
    {
        foreach (var type in Types)
            selector.AddSurrogate(type, new StreamingContext(StreamingContextStates.All), this);
    }

    IEnumerable<Type> Types
    {
        get
        {
            yield return typeof(TRealObject);
            yield return typeof(SurrogatePlaceholder);
        }
    }

    protected abstract TId GetId(TRealObject realObject);

    protected abstract TRealObject GetRealObject(TId id);

    #region ISerializationSurrogate Members

    public void GetObjectData(object obj, SerializationInfo info, StreamingContext context)
    {
        var original = (TRealObject)obj;
        var id = GetId(original);
        info.AddValue("id", id);
        // use Info.SetType() to force the serializer to construct an object of type SurrogatePlaceholder during deserialization.
        info.SetType(typeof(SurrogatePlaceholder));
    }

    public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector)
    {
        // Having constructed an object of type SurrogatePlaceholder, 
        // look up the real sprite using the id in the serialization stream.
        var id = (TId)info.GetValue("id", typeof(TId));
        return GetRealObject(id);
    }

    #endregion
}

Sample fiddle #2.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • 1
    Thank you, this seems exactly what I was looking for. I'll test it soon and give feedback. – dogiordano Mar 15 '17 at 20:03
  • @dogiordano - answer updated with an alternate version that might work on unity3d as well as .net. – dbc Mar 20 '17 at 17:15
  • 1
    Wow... It worked! You've been far more than useful. I don't know how to thank you. I'd upvote you more if I could. – dogiordano Mar 20 '17 at 19:34