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

时间:2016-11-15 11:30:35

标签: c# serialization protobuf-net

我一直在使用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 

到目前为止一直很好(事实上非常好,谢谢马克)。但是,现在如果我想让基类比我当前的基类更低(在本例中为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类时,我仍然可以反序列化序列化的初始对象吗?

1 个答案:

答案 0 :(得分:1)

这不会完全与静态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类创建的序列反序列化为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首先被编码到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架构兼容,因此可以将A V1消息反序列化为V3类,而不会丢失数据。样本fiddle

当然,如果您要将会员从MarketDataObject移至LowerStillBaseClass,则需要确保字段ID保持不变。

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