【问题标题】:Protobuf-net enum backwards compatibilityProtobuf-net 枚举向后兼容
【发布时间】:2016-02-24 11:12:47
【问题描述】:

我试图在一个新的应用版本中为某个 protobuf 序列化类添加一个新的枚举值,并且在测试时注意到,鉴于这种新的文件格式,以前的版本会抛出异常:

protobuf-net.dll 中发生了“ProtoBuf.ProtoException”类型的未处理异常 附加信息:没有 {enum-type-name} 枚举映射到线值 3

很明显,它告诉我3int 值没有枚举值,但我一直认为Protocol Buffers defaulted to the zero-valued ("default") enum value(如果存在),以防实际枚举值无法映射到。

为了澄清,这可以使用以下示例重现(我有意将反序列化步骤放入不同的类中,以模仿尝试加载新格式的旧应用程序):

// --- 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; }
}

而且下面的代码会失败:

// 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);

由于 v1 已公开,在 v2 中序列化时是否可以使用一些元数据来避免此问题?当然,我可以通过重写 v2 以使用单独的属性并使枚举值保持不变来摆脱这个麻烦,但如果可能的话,我想让枚举向后兼容。

【问题讨论】:

  • 发送EnumV2.Three 后,v1roundtrip.Value 会发生什么情况?
  • @Caramiriel:根据我的理解(在this thread中解释),应该设置为EnumV1.Default而不是抛出异常。如果我想确保格式向后兼容,这就是我所期望的。例如,this user 似乎有同样的问题,并通过添加默认(零)枚举值来修复它,不需要额外的 protobuf 属性。
  • @jgauffin:正是我要评论的内容,这似乎是 Marc 获得这些观点的一种简单方法(我假设我没有配置正确)。

标签: c# enums protocol-buffers protobuf-net backwards-compatibility


【解决方案1】:

[ProtoContract(EnumPassthru=true)] 添加到您的枚举将允许 protobuf-net 反序列化未知值。

很遗憾,无法追溯修复您的 v1.1 版本。您必须使用不同的属性。

【讨论】:

  • 但这不会将反序列化值设置为3(即使它没有匹配的枚举),而不是默认值?我仍然希望在无法解析实际枚举的情况下使用默认值?
  • 是的。如果您需要旧版本上的值为默认值,而新版本上的值为某个新的枚举值(在本例中为 3),那么每次添加新属性时都必须切换到新属性对您的枚举的价值。
【解决方案2】:

由于 v1 已公开,在 v2 中序列化时是否可以使用一些元数据来避免此问题?当然,我可以通过重写 v2 以使用单独的属性并使枚举值保持不变来摆脱这个麻烦,但如果可能的话,我想让枚举向后兼容。

您遇到的是一个 protobuf-net 错误,此处描述为 protobuf-net - issue #422: Invalid behaviour while deserializing unknown enum value

根据这里的protobuf-net faulty enum exception (issue 422) need a good workaround(当然还有你的帖子),它似乎还没有修复。

不幸的是,您需要修复protobuf-net 源代码或使用提到的解决方法。

更新:我检查了GitHub 存储库中的代码,并确认问题仍未解决。这是EnumSerializer.cs 内有问题的代码(ISSUE #422 注释是我的):

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
}

【讨论】:

    【解决方案3】:

    您的 ClassV1 缺乏前向兼容性。

    我会以序列化/反序列化枚举值的字符串表示的方式实现 Proto 合约。这样您就可以自己处理回退到默认值。 Value 属性不会被序列化/反序列化。

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

    它仍然没有解决在生产环境中存在非前向兼容类的问题。

    【讨论】:

    • 有很多方法可以确保向后/向前兼容。一种更简单的方法(对于枚举)是对int 值进行反序列化/序列化。但这不是我所做的,因为我依靠 protobuf 来处理这个问题。我设计了 v1 合同,认为 Protocol Buffers 默认为零值枚举值,以防实际的枚举值无法映射到,我想看看 1)为什么这样做' t工作和2)至少如何在反序列化时拦截和处理这种情况。您的回答既没有解释 为什么 ClassV1 缺乏固件兼容性,也没有解释现在该做什么。
    • 缺乏前向兼容性是因为 ClassV1 不支持新的枚举值,可以将其视为由编译时常量组成的值类型。这就像在没有默认分支的情况下实现一个 switch/case 语句并让它处理不受支持的情况。
    • 我不认为设置默认值是protobuf的责任。我会让 ClassV1 类通过在实例化期间初始化属性来处理这个问题,而不是让它单化(布尔字段也隐式地用 false 初始化)。 proto-buf 会抛出 nonthless 这很好,因为它表明存在兼容性问题。
    • 首先,according to documentation,protobuf 应该能够处理新的枚举值。接下来,我不是在谈论默认值。很明显,属性在我实例化的时候会被初始化为默认值,与属性无关,与序列化无关。作为 protobuf 状态的开发人员指南,如果您有一个使用协议缓冲区作为其数据格式的通信协议,您可以扩展您的协议而不必担心破坏现有代码。这是协议缓冲区的主要卖点之一。
    【解决方案4】:

    您可以将 DefaultValue 属性添加到您的 proto 成员属性。

    [ProtoContract]
    public class ClassV1
    {
        [ProtoMember(1), DefaultValue(EnumV1.Default)]
        public EnumV1 Value { get; set; }
    }
    

    为了明确在默认情况下应该如何初始化属性。

    【讨论】:

    • 你试过了吗?我认为这与异常无关。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-04-01
    • 2011-05-07
    • 1970-01-01
    • 1970-01-01
    • 2013-07-18
    • 1970-01-01
    相关资源
    最近更新 更多