Reputation: 51224
I was trying to add a new enum value for a certain protobuf-serialized class in a new app version, and while testing, noticed that the previous version will throw an exception, given this new file format:
An unhandled exception of type 'ProtoBuf.ProtoException' occurred in protobuf-net.dll Additional information: No {enum-type-name} enum is mapped to the wire-value 3
It is fairly obvious that it's telling me that there is no enum value for the int
value of 3
, but I always had the idea that Protocol Buffers defaulted to the zero-valued ("default") enum value (if such exists), in case that an actual enum value couldn't be mapped to.
To clarify, this can be reproduced using the following example (I am intentionally doing the deserialization step into a different class to mimic old app trying to load the new format):
// --- version 1 ---
public enum EnumV1
{
Default = 0,
One = 1,
Two = 2
}
[ProtoContract]
public class ClassV1
{
[ProtoMember(1)]
public EnumV1 Value { get; set; }
}
// --- version 2 ---
public enum EnumV2
{
Default = 0,
One = 1,
Two = 2,
Three = 3 // <- newly added
}
[ProtoContract]
public class ClassV2
{
[ProtoMember(1)]
public EnumV2 Value { get; set; }
}
And the following code will fail:
// serialize v2 using the new app
var v2 = new ClassV2() { Value = EnumV2.Three };
var v2data = Serialize(v2);
// try to deserialize this inside the old app to v1
var v1roundtrip = Deserialize<ClassV1>(v2data);
Since v1 is out in the open, is there some metadata I can use when serializing in v2 to avoid this issue? I can, of course, get myself out of this trouble by rewriting v2 to use a separate property and leave the enum values unmodified, but I'd like to make enums backwards compatible if possible.
Upvotes: 7
Views: 4662
Reputation: 205619
Since v1 is out in the open, is there some metadata I can use when serializing in v2 to avoid this issue? I can, of course, get myself out of this trouble by rewriting v2 to use a separate property and leave the enum values unmodified, but I'd like to make enums backwards compatible if possible.
What you are experiencing is a protobuf-net bug described here protobuf-net - issue #422: Invalid behaviour while deserializing unknown enum value.
It seems that it's not fixed yet according to here protobuf-net faulty enum exception (issue 422) need a good workaround (and of course your post).
Unfortunately you need to either fix the protobuf-net
source code or use the workarounds mentioned.
UPDATE: I've checked the code in the GitHub repository and confirming that the issue is still not fixed. Here is the problematic code inside the EnumSerializer.cs (the ISSUE #422
comment is mine):
public object Read(object value, ProtoReader source)
{
Helpers.DebugAssert(value == null); // since replaces
int wireValue = source.ReadInt32();
if(map == null) {
return WireToEnum(wireValue);
}
for(int i = 0 ; i < map.Length ; i++) {
if(map[i].WireValue == wireValue) {
return map[i].TypedValue;
}
}
// ISSUE #422
source.ThrowEnumException(ExpectedType, wireValue);
return null; // to make compiler happy
}
Upvotes: 2
Reputation: 5851
Adding [ProtoContract(EnumPassthru=true)]
to your enums will allow protobuf-net to deserialize unknown values.
Unfortunately, there is no way to retroactively fix your v1. You'll have to use a different property.
Upvotes: 3
Reputation: 1125
You could add the DefaultValue attribute to your proto member property.
[ProtoContract]
public class ClassV1
{
[ProtoMember(1), DefaultValue(EnumV1.Default)]
public EnumV1 Value { get; set; }
}
To make clear how the property should be initialized for the default case.
Upvotes: 0
Reputation: 1125
Your ClassV1 lacks forward compatiblity.
I would have implemented the Proto contract in such a way that it serializes/deserializes the string representation of the enum value. This way you can handle the fallback to the default value by yourself. The Value property would not be serialized/deserialized.
public enum EnumV1
{
Default = 0,
One = 1,
Two = 2
}
public enum EnumV2
{
Default = 0,
One = 1,
Two = 2,
Three = 3 // <- newly added
}
[ProtoContract]
public class ClassV1
{
[ProtoMember(1)]
public string ValueAsString
{
get { return Value.ToString(); }
set
{
try
{
Value = (EnumV1) Enum.Parse(typeof (EnumV1), value);
}
catch (Exception)
{
Value = EnumV1.Default;
}
}
}
public EnumV1 Value { get; set; }
}
[ProtoContract]
public class ClassV2
{
[ProtoMember(1)]
public string ValueAsString
{
get { return Value.ToString(); }
set
{
try
{
Value = (EnumV2)Enum.Parse(typeof(EnumV2), value);
}
catch (Exception)
{
Value = EnumV2.Default;
}
}
}
public EnumV2 Value { get; set; }
}
Still it does not solve the problem of having a non-forward-compabtible class in production.
Upvotes: 0