protobuf.net 中的继承,添加较低的基础 class 仍然向后兼容?

Inheritance in protobuf.net, adding a lower base class still backward compatible?

我已经使用 protobuf.net 一段时间了,它非常好。我可以有一个从基础 class 继承的 class,我可以通过在基础 class 中使用 ProtoInclude 语句来序列化派生的 class。如果我的基础 class 最初在对象被序列化时只说了两个 ProtoInclude 语句,那么说

[ProtoInclude(100, typeof(Vol_SurfaceObject))]
[ProtoInclude(200, typeof(CurveObject))]
internal abstract class MarketDataObject 

我仍然可以将同一个对象反序列化为具有更多派生的代码:

[ProtoInclude(100, typeof(Vol_SurfaceObject))]
[ProtoInclude(200, typeof(CurveObject))]
[ProtoInclude(300, typeof(StaticDataObject))]
internal abstract class MarketDataObject 

到目前为止一切顺利(事实上非常好,感谢 Marc)。但是,现在如果我想让基数 class 比我当前的基数 class 更低怎么办(在本例中为 MarketDataObject)。这样我就会

[ProtoInclude(100, typeof(Vol_SurfaceObject))]
[ProtoInclude(200, typeof(CurveObject))]
[ProtoInclude(300, typeof(StaticDataObject))]
internal abstract class MarketDataObject : LowerStillBaseClass
{ blah }

[ProtoInclude(10, typeof(MarketDataObject))]
internal abstract class LowerStillBaseClass
{ blah }

虽然代码当然可以工作,但当对象只有 2 个 ProtoInclude 语句到这种新形式的 MarketDataObject class 时,我是否仍然能够反序列化已序列化的初始对象?

这不适用于纯粹的静态 protbuf-net 属性。稍微简化一下,假设您从以下内容开始:

namespace V1
{
    [ProtoContract]
    internal class MarketDataObject
    {
        [ProtoMember(1)]
        public string Id { get; set; }
    }
}

并将其重构为以下内容:

namespace V2
{
    [ProtoInclude(10, typeof(MarketDataObject))]
    [ProtoContract]
    internal abstract class LowerStillBaseClass
    {
        [ProtoMember(1)]
        public string LowerStillBaseClassProperty { get; set; }
    }

    [ProtoContract]
    internal class MarketDataObject : LowerStillBaseClass
    {
        [ProtoMember(1)]
        public string Id { get; set; }
    }
}

接下来,尝试将从 V1 class 创建的反序列化为 V2 class。您将失败并出现以下异常:

ProtoBuf.ProtoException: No parameterless constructor found for LowerStillBaseClass

这不起作用的原因是类型层次结构是基优先而不是派生优先序列化的。要查看这一点,请通过调用 Console.WriteLine(RuntimeTypeModel.Default.GetSchema(type)); 转储每种类型的 protobuf-net 合约 对于 V1.MarketDataObject 我们得到:

message MarketDataObject {
   optional string Id = 1;
}

对于V2.MarketDataObject

message LowerStillBaseClass {
   optional string LowerStillBaseClassProperty = 1;
   // the following represent sub-types; at most 1 should have a value
   optional MarketDataObject MarketDataObject = 10;
}
message MarketDataObject {
   optional string Id = 1;
}

MarketDataObject 被编码成 message,首先在顶层使用其基本类型字段,然后派生类型字段被递归封装在嵌套的可选消息中,其字段 id 表示它的亚型。所以当 V1 消息反序列化为 V2 对象时,没有遇到子类型字段,没有推断出正确的派生类型,并且派生类型值丢失。

一种解决方法 是避免使用 [ProtoInclude(10, typeof(MarketDataObject))],而是使用 RuntimeTypeModel API :

namespace V3
{
    [ProtoContract]
    internal abstract class LowerStillBaseClass
    {
        [ProtoMember(1)]
        public string LowerStillBaseClassProperty { get; set; }
    }

    [ProtoContract]
    internal class MarketDataObject : LowerStillBaseClass
    {
        static MarketDataObject()
        {
            AddBaseTypeProtoMembers(RuntimeTypeModel.Default);
        }

        const int BaseTypeIncrement = 11000;

        public static void AddBaseTypeProtoMembers(RuntimeTypeModel runtimeTypeModel)
        {
            var myType = runtimeTypeModel[typeof(MarketDataObject)];
            var baseType = runtimeTypeModel[typeof(MarketDataObject).BaseType];
            if (!baseType.GetSubtypes().Any(s => s.DerivedType == myType))
            {
                foreach (var field in baseType.GetFields())
                {
                    myType.Add(field.FieldNumber + BaseTypeIncrement, field.Name);
                }
            }
        }

        [ProtoMember(1)]
        public string Id { get; set; }
    }
}

(这里我在 MarketDataObject 的静态构造函数中填充契约。您可能想在其他地方这样做。)V3. 的架构如下所示:

message MarketDataObject {
   optional string Id = 1;
   optional string LowerStillBaseClassProperty = 11001;
}

此架构与 V1 架构兼容,因此 V1 消息可以反序列化为 V3 class 而不会丢失数据。示例 fiddle.

当然,如果您要将成员从 MarketDataObject 移动到 LowerStillBaseClass,您将需要确保字段 ID 保持不变。

此解决方法的缺点是您无法反序列化 LowerStillBaseClass 类型的对象,并且让 protobuf-net 自动推断出正确的派生类型。