在protobuf.net中继承,添加一个较低的基类仍然向后兼容

本文关键字:基类 一个 继承 net protobuf 添加 | 更新日期: 2023-09-27 18:18:03

我已经使用protobuf.net一段时间了,它非常棒。我可以有一个继承自基类的类,我可以在基类中使用ProtoInclude语句序列化派生类。如果我的基类最初只有两个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)。但是,现在如果我想要一个比当前基类更低的基类(在本例中是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类?

在protobuf.net中继承,添加一个较低的基类仍然向后兼容

这对静态的probuf -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类创建的对象反序列化为V2类。您将失败,但有以下例外:

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首先被编码到具有其基本类型字段的消息中,在顶层,然后派生类型字段被递归地封装在具有表示其子类型的字段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类而不会丢失数据。示例小提琴。

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

这个解决方法的缺点是您失去了对类型为LowerStillBaseClass的对象进行反序列化的能力,并且让protobuf-net自动推断出正确的派生类型。