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

天大地大妈咪最大 提交于 2019-12-24 08:26:28

问题


I have been using protobuf.net for a while and it is excellent. I can have a class which is inherited from a base class, I can serialise the derived class by using ProtoInclude statements in the base class. If my base class originally had only say two ProtoInclude statements when the object was serialised, say

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

I can still deserialise that same object in to code that has evolved to have more derivations:

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

So far so good (in fact excellent, thanks Marc). However, now what if I want to have a base class even lower then my current base class here (in this case, MarketDataObject). Such that I would have

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

Whilst the code will of course work, will I be still be able to deserialise the initial objects that were serialised when the object had only 2 ProtoInclude statements to this new form of the MarketDataObject class?


回答1:


This will not work purely with static protbuf-net attributes. Simplifying somewhat, imagine you start with the following :

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

And refactor it to be the following:

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

Next, try to deserialize a created from the V1 class into a V2 class. You will fail with the following exception:

ProtoBuf.ProtoException: No parameterless constructor found for LowerStillBaseClass

The reason this does not work is that type hierarchies are serialized base-first rather than derived-first. To see this, dump the protobuf-net contracts for each type by calling Console.WriteLine(RuntimeTypeModel.Default.GetSchema(type)); For V1.MarketDataObject we get:

message MarketDataObject {
   optional string Id = 1;
}

And for 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 is getting encoded into a message with its base type fields first, at the top level, then derived type fields are recursively encapsulated inside a nested optional message with a field id that represents its subtype. So when a V1 message is deserialized to a V2 object, no subtype field is encountered, the correct derived type is not inferred, and derived type values are lost.

One workaround is to avoid using [ProtoInclude(10, typeof(MarketDataObject))] and instead populate the base class members in the derived type's contract programmatically using the 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; }
    }
}

(Here I am populating the contract inside the static constructor for MarketDataObject. You might want to do it elsewhere.) The schema for V3. looks like:

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

This schema is compatible with the V1 schema, and so A V1 message can be deserialized into a V3 class without data loss. Sample fiddle.

Of course, if you are moving a member from MarketDataObject to LowerStillBaseClass you will need to ensure that the field id stays the same.

The disadvantage of this workaround is that you lose the ability to deserialize an object of type LowerStillBaseClass and have protobuf-net automatically infer the correct derived type.



来源:https://stackoverflow.com/questions/40608767/inheritance-in-protobuf-net-adding-a-lower-base-class-still-backward-compatible

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!