32

we have some problems with serializing an empty list. here some code in .NET using CF 2.0

//Generating the protobuf-msg
ProtoBufMessage msg = new ProtoBufMessage();
msg.list = new List<AnotherProtobufMessage>();
// Serializing and sending throw HTTP-POST
MemoryStream stream = new MemoryStream();
Serializer.Serialize(stream, msg);
byte[] bytes = stream.ToArray();
HttpWebRequest request = createRequest();
request.ContentLength = bytes.Length ;

using (Stream httpStream = request.GetRequestStream())
{              
      httpStream.Write(bytes, 0, bytes.Length);
}

we got a exception, when we try to write on the stream (bytes.length out of range). But a type with an empty List should not be 0 bytes, right (type-information?)?

We need this type of sending, because in the Response are the messages from the Server for our client.

John Saunders
  • 160,644
  • 26
  • 247
  • 397
bopa
  • 1,105
  • 1
  • 12
  • 20

3 Answers3

40

The wire format (defined by google - not inside my control!) only sends data for items. It makes no distinction between an empty list and a null list. So if there is no data to send - yes, the length is 0 (it is a very frugal format ;-p).

Protocol buffers do not include any type metadata on the wire.

Another common gotcha here is that you might assume your list property is automatically instantiated as empty, but it won't be (unless your code does it, perhaps in a field initializer or constructor).

Here's a workable hack:

[ProtoContract]
class SomeType {

    [ProtoMember(1)]
    public List<SomeOtherType> Items {get;set;}

    [DefaultValue(false), ProtoMember(2)]
    private bool IsEmptyList {
        get { return Items != null && Items.Count == 0; }
        set { if(value) {Items = new List<SomeOtherType>();}}
    }
}

Hacky maybe, but it should work. You could also lose the Items "set" if you want and just drop the bool:

    [ProtoMember(1)]
    public List<SomeOtherType> Items {get {return items;}}
    private readonly List<SomeOtherType> items = new List<SomeOtherType>();

    [DefaultValue(false), ProtoMember(2)]
    private bool IsEmptyList {
        get { return items.Count == 0; }
        set { }
    }
Joel Coehoorn
  • 399,467
  • 113
  • 570
  • 794
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • 1
    Although Google define the serialization in that way does mean that it is logical (it could be for some cases and not for others). If we use protobuf for persisting objects hierarchy, peoples would expect to get a copy of their hierarchy without any difference if they serialize and de-serialize). I think it would be a great feature to add an option to force the serialization of empty collection in order to re-create the same objects hierarchy on deserialization. I think that the number of upvote on the question should justify partly my request ;-) – Eric Ouellet Oct 20 '20 at 20:42
  • Another feature that would be more generics is to have the possibility to mark a method as "[OnDeserialized]" and being call after the serialization would have been completed on that object. – Eric Ouellet Oct 20 '20 at 20:45
  • My 2 previous comments are based on my understanding that serialization do process a collection being null or empty the same way (serializing nothing). – Eric Ouellet Oct 20 '20 at 20:50
  • 1
    @EricOuellet "I think it would be a great feature to add an option to force the serialization of empty collection in order to re-create the same objects hierarchy on deserialization" - that is an interesting aim, but there is *literally no way of expressing that* in the data protocol; the collection itself doesn't appear in the stream - just the contents, so if there are zero contents... – Marc Gravell Oct 21 '20 at 07:34
  • 1
    Thanks Marc for the answer. I find that really sad. According to my expectation and probably most peoples, it is the responsibility of the serializer to persist the exact state of the object hierarchy. Everybody that is using Protobuf and have one or more empty collection will have to write additional code that should not exists with a fully functional serializer. I really like your serializer (the performance is awesome) but that "miss behavior" really remove lots of my enthusiasm to use it. – Eric Ouellet Oct 21 '20 at 13:56
3

As @Marc said, the wire format only sends data for items, so in order to know if the list was empty or null, you have to add that bit of information to the stream.
Adding extra property to indicate whether the original collection was empty or not is easy but if you don't want to modify the original type definition you have another two options:

Serialize Using Surrogate

The surrogate type will have the extra property (keeping your original type untouched) and will restore the original state of the list: null, with items or empty.

    [TestMethod]
    public void SerializeEmptyCollectionUsingSurrogate_RemainEmpty()
    {
        var instance = new SomeType { Items = new List<int>() };

        // set the surrogate
        RuntimeTypeModel.Default.Add(typeof(SomeType), true).SetSurrogate(typeof(SomeTypeSurrogate));

        // serialize-deserialize using cloning
        var clone = Serializer.DeepClone(instance);

        // clone is not null and empty
        Assert.IsNotNull(clone.Items);
        Assert.AreEqual(0, clone.Items.Count);
    }

    [ProtoContract]
    public class SomeType
    {
        [ProtoMember(1)]
        public List<int> Items { get; set; }
    }

    [ProtoContract]
    public class SomeTypeSurrogate
    {
        [ProtoMember(1)]
        public List<int> Items { get; set; }

        [ProtoMember(2)]
        public bool ItemsIsEmpty { get; set; }

        public static implicit operator SomeTypeSurrogate(SomeType value)
        {
            return value != null
                ? new SomeTypeSurrogate { Items = value.Items, ItemsIsEmpty = value.Items != null && value.Items.Count == 0 }
                : null;
        }

        public static implicit operator SomeType(SomeTypeSurrogate value)
        {
            return value != null
                ? new SomeType { Items = value.ItemsIsEmpty ? new List<int>() : value.Items }
                : null;
        }
    }


Make Your Types Extensible

protobuf-net suggest the IExtensible interface which allow you to extend types so that fields can be added to a message without anything breaking (read more here). In order to use protobuf-net extension you can inherit the Extensible class or implement the IExtensible interface to avoid inheritance constraint.
Now that your type is "extensible" you define [OnSerializing] and [OnDeserialized] methods to add the new indicators that will be serialized to the stream and deserialized from it when reconstructing the object with its original state.
The pros is that you don't need to define new properties nor new types as surrogates, the cons is that IExtensible isn't supported if your type have sub types defined in your type model.

    [TestMethod]
    public void SerializeEmptyCollectionInExtensibleType_RemainEmpty()
    {
        var instance = new Store { Products = new List<string>() };

        // serialize-deserialize using cloning
        var clone = Serializer.DeepClone(instance);

        // clone is not null and empty
        Assert.IsNotNull(clone.Products);
        Assert.AreEqual(0, clone.Products.Count);
    }

    [ProtoContract]
    public class Store : Extensible
    {
        [ProtoMember(1)]
        public List<string> Products { get; set; }

        [OnSerializing]
        public void OnDeserializing()
        {
            var productsListIsEmpty = this.Products != null && this.Products.Count == 0;
            Extensible.AppendValue(this, 101, productsListIsEmpty);
        }

        [OnDeserialized]
        public void OnDeserialized()
        {
            var productsListIsEmpty = Extensible.GetValue<bool>(this, 101);
            if (productsListIsEmpty)
                this.Products = new List<string>();
        }
    }
Tamir
  • 3,833
  • 3
  • 32
  • 41
0
public List<NotificationAddress> BccAddresses { get; set; }

you can replace with:

private List<NotificationAddress> _BccAddresses;
public List<NotificationAddress> BccAddresses {
   get { return _BccAddresses; }
   set { _BccAddresses = (value != null && value.length) ? value : null; }
}
Vlad Mysla
  • 1,181
  • 12
  • 15