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>();
}
}