Sunday, April 3, 2011

Protocol Buffers In C#: How Are Boxed Value Types Handled

In the following examples:

public class RowData
{
    public object[] Values;
}

public class FieldData
{
    public object Value;
}

I am curious as how either protobuf-net or dotnet-protobufs would handle such classes. I am more familiar with protobuf-net, so what I actually have is:

[ProtoContract]
public class RowData
{
    [ProtoMember(1)]
    public object[] Values;
}
[ProtoContract]
public class FieldData
{
    [ProtoMember(1)]
    public object Value;
}

However I get an error saying "No suitable Default Object encoding found". Is there an easy way to treat these classes, that I am just not aware of?

To elaborate more on the use case:

This is a scaled down version of a data class used in remoting. So essentially it looks like this:

FieldData data = new FieldData();
data.Value = 8;

remoteObject.DoSomething(data);

Note: I've omitted the ISerializable implementation for simplicity, but it is as you'd expect.

From stackoverflow
  • Re protobuf-net, which I maintain:

    The issue here isn't value-types (which it will often handle fine) - it is the open object usage, which means it simply doesn't know what data to expect, and thus how to encode/decode it.

    At the moment, I can't think of an easy/clean way to handle that. It will handle a range of common value-type scenarios, lists, and any level of hierarchy based on contracts (data-contract, proto-contracts, or some xml-schemas), but it needs a clue.

    Perhaps if you can clarify the use-case, I might be able to help more? For example, the above wouldn't work very with DataContractSerializer or XmlSerializer either...

    Re dotnet-protobufs; I can't really comment, but I'm pretty sure it would be even less forgiving; it is intended to be used with classes generated from a .proto file, so object would simply never enter into the model (Jon: correct me if I am wrong).

    If you do leave more info, could you kindly post a comment here? So I can find it easily... Alternatively, drop me a mail directly (see my SO profile).


    edit - here's the hacky thing I had in mind - it isn't working at the moment, but I'll figure out why tomorrow (probably). Note that in theory the extra members could all be private - I'm just trying to make it easy while debugging. Note that this doesn't take any extra storage. Like I say, it doesn't work today, but it should - I'll find out why...

    [ProtoContract]
    public class FieldData
    {
        public object Value {get;set;}
    
        [ProtoMember(1)]
        public int ValueInt32 {
            get { return (int)Value; } set { Value = value; } }
        public bool ValueInt32Specified {
            get { return Value != null && Value is int; } set { } }
    
        [ProtoMember(2)]
        public float ValueSingle {
            get { return (float)Value; } set { Value = value; } }
        public bool ValueSingleSpecified {
            get { return Value != null && Value is float; } set { } }
    
        // etc for expected types
    }
    
    Greg Dean : I can't comment on DataContractSerializer, but the XmlSerializer has no problem with this.
    Greg Dean : I edited the question to provide a little more insight.
    Marc Gravell : But it cheats ;-p It isn't truly contract-based when it does this...
    Greg Dean : Yea - I was afraid of this
    Greg Dean : I was thinking more along the lines of inheritance. I'll post what I had in mind.
    Greg Dean : in a few minutes...
  • This is something like what I had in mind. Let me know what you think. Naturally I'd have to add a subclass for each value type I need to support. What do you think? Is there a better way, do you see any inefficiencies with this method?

    [ProtoContract, Serializable]
    [ProtoInclude(1, typeof(Int32FieldData))]
    public abstract class FieldDataBase : ISerializable
    {
     [ProtoIgnore]
     public abstract object Value { get; set;}
     protected FieldDataBase()
     { }
    
     #region ISerializable Members
     protected FieldDataBase(SerializationInfo info, StreamingContext context)
     {
      Serializer.Merge<FieldDataBase>(info, this);
     }
     public void GetObjectData(SerializationInfo info, StreamingContext context)
     {
      Serializer.Serialize<FieldDataBase>(info, this);
     }
    
     #endregion
    }
    
    [ProtoContract, Serializable]
    public class Int32FieldData : FieldDataBase
    {
     [ProtoMember(1)]
     public int? Int32Value;
    
     [ProtoIgnore]
     public override object Value
     {
      get { return this.Int32Value.HasValue ? this.Int32Value : null; }
      set { this.Int32Value = (int?)value; }
     }
     public Int32FieldData() { }
     protected Int32FieldData(SerializationInfo info, StreamingContext context)
      :base(info, context)
     { }
    }
    
    Greg Dean : Hmm, looks like there may be an issue with nulls?
    Marc Gravell : In which way "an issue"? Unfortunately, the wire-format has no concept of nulls, so the closest we can do is not transmit it - but either way, the inheritance should work. Are you getting a specific issue? (I've no time to try it right now...)
    Marc Gravell : For info - due to the encoding details, direct encapsulation would be more efficient than inheritance (by 2-4 bytes per value).
    Greg Dean : well - the issue is, as you stated, nulls are not transmitted. I'm working on a work around, but it's getting to be hacky^2 :-)
    Greg Dean : I guess I should point out, I had moved on to int?[] in which case I ended up serializing {1,2,3,4,null} and getting {1,2,3,4} back
    Greg Dean : So If I understand what you are saying additional properties cost no overhead if they are null. So direct encapsulation would be much more efficient/straight forward.
  • Direct encapsulation seems to work fine with no additional overhead from all the properties, in the following manner:

    [ProtoContract, Serializable]
    public class ObjectWrapper : ISerializable
    {
     public ObjectWrapper()
     { }
     [ProtoIgnore]
     public object Value
     {
      get
      {
       if (Int32Value.HasValue)
        return Int32Value.Value;
       else if (BinaryValue != null)
        return BinaryValue;
       else
        return StringValue;
      }
      set
      {
       if (value is int)
        this.Int32Value = (int)value;
       else if (value is byte[])
        this.BinaryValue = (byte[])value;
       else if (value is string)
        this.StringValue = (string)value;
      }
     }
     [ProtoMember(1)]
     private int? Int32Value;
     [ProtoMember(2)]
     private string StringValue;
     [ProtoMember(3)]
     private byte[] BinaryValue;
                // etc
    
     #region ISerializable Members
     protected ObjectWrapper(SerializationInfo info, StreamingContext context)
     {
      Serializer.Merge<ObjectWrapper>(info, this);
     }
     public void GetObjectData(SerializationInfo info, StreamingContext context)
     {
      Serializer.Serialize<ObjectWrapper>(info, this);
     }
    
     #endregion
    }
    
    Greg Dean : string and byte[] aren't really necessary in a general sense - I just know I will need them in my specific case
    Marc Gravell : Takes a bit more field storage in the client, though...
  • (updated)

    Right; figured it out... the main problem in my sample above was the value-getters; they were throwing exceptions. There were also some library glitches (now fixed).

    However, the simplest approach is Nullable<T> pass-thru properties:

        [ProtoMember(1)]
        private int? ValueInt32
        {
            get { return Get<int>(); }
            set { Value = value; }
        }
    

    etc, with:

        private T? Get<T>() where T : struct
        {
            return (Value != null && Value is T) ? (T?)Value : (T?)null;
        }
    

    Both this and the *Specified approach have been tested, and now work fine.

0 comments:

Post a Comment