Protobuf如何在不失去向后兼容性的情况下更改继承层次结构

时间:2019-05-30 10:49:52

标签: c# serialization hierarchy protobuf-net

我有一个使用protobuf-net进行继承的特定用例,我不知道(如果可能)如何处理。

假设我们有以下示例类(将其标记为版本1):

public class FooA
{
    public double A { get; set; }
    public double B { get; set; }
}

public class FooB : FooA
{
    public double C { get; set; }
}

public class FooC : FooB
{        
}

public class FooD : FooC
{
    public double D { get; set; }
}

public class FooA1 : FooA
{        
}

具有以下protobuf的模型定义:

        Model = RuntimeTypeModel.Create();

        Model.Add(typeof(FooA), false)
            .AddSubType(201, typeof(FooB))
            .AddSubType(202, typeof(FooA1))
            .Add(1, "A")
            .Add(2, "B");

        Model[typeof(FooB)]
            .AddSubType(201, typeof(FooC))
            .Add(1, "C");

        Model[typeof(FooC)]
            .AddSubType(201, typeof(FooD));                

        Model[typeof(FooD)]
            .Add(1, "D");

我将它们序列化如下

        FooA a = new FooA() {A = 10, B = 20};
        FooB b = new FooB() {A = 10, B = 20, C = 30};
        FooA1 b1 = new FooA1() {A = 100, B = 200};
        FooC c = new FooC() {A = 10, B = 20, C = 30};
        FooD d = new FooD() {A = 10, B = 20, C = 30, D = 40};

        using (var stream = File.Open(fileName, FileMode.Create, FileAccess.Write))
        {
            SerializeWithLengthPrefix(stream, a);
            SerializeWithLengthPrefix(stream, b);
            SerializeWithLengthPrefix(stream, b1);
            SerializeWithLengthPrefix(stream, c);
            SerializeWithLengthPrefix(stream, d);
        }

辅助方法

    public void SerializeWithLengthPrefix<T>(Stream stream, T obj)
    {
        var serializationContext = new ProtoBuf.SerializationContext() { Context = this };
        Model.SerializeWithLengthPrefix(stream, obj, typeof(T), PrefixStyle.Base128, 1, serializationContext);
    }

    public T DeserializeWithLengthPrefix<T>(Stream stream, out long bytesRead, out bool haveObject)
    {
        var serializationContext = new ProtoBuf.SerializationContext() { Context = this };

        return (T)Model.DeserializeWithLengthPrefix(stream, null, typeof(T), PrefixStyle.Base128, 1, null, out bytesRead, out haveObject, serializationContext);
    }

现在,我需要以这种方式更改继承层次结构(将其标记为版本2):

public class FooA
{
    public double A { get; set; }
    public double B { get; set; }
}

public class FooB : FooA
{
    public double C { get; set; }
}

public class FooC : FooA1/*FooB*/
{
    public double C { get; set; }
}

public class FooD : FooC
{
    public double D { get; set; }
}

public class FooA1 /*: FooA*/
{
    public double A { get; set; }
    public double B { get; set; }
}

还有模型定义,试图为先前定义的每个类保留相同的ID。

       Model = RuntimeTypeModel.Create();

        Model.Add(typeof(FooA), false)
            .AddSubType(201, typeof(FooB))
            //.AddSubType(202, typeof(FooA1))
            .Add(1, "A")
            .Add(2, "B");

        Model.Add(typeof(FooA1), false)
            .Add(1, "A")
            .Add(2, "B");

        Model[typeof(FooB)]
            //.AddSubType(201, typeof(FooC))
            .Add(1, "C");

        Model[typeof(FooA1)]
            .AddSubType(201, typeof(FooC));

        Model[typeof(FooC)]
            .Add(1, "C")
            .AddSubType(201, typeof(FooD));                

        Model[typeof(FooD)]
            .Add(1, "D");

现在,我反序列化版本1中存储的文件,并检查模型中定义的类型:它们与版本1序列化期间的类型相同。

Model types definition

但是当我检查对象值时,我看到FooC已反序列化为FooD,并且D值始终等于0。

Deserialized values

我在做什么错?有办法解决吗?

更新

使用版本2反序列化 FooC 时尝试调试protobuf-net源代码,方法 RuntimeTypeModel。GetKey()从基类开始(getBaseKey = true),可以正确获取 FooA1 (key = 2),但最终它将获得 FooD 对象,而不是 FooC 。也许有一种方法可以以不同的方式处理这种方法,以允许出现这种情况?

protobuf debug

2 个答案:

答案 0 :(得分:0)

我想不出一种改变而不破坏兼容性的方法。当我们陷入困境时,我的建议是:将“真实”类型和序列化类型拆分为单独的类型模型,这些模型不必彼此成对1:1映射。然后,您可以对“实际”类型(域模型)执行任何操作,然后将这些值投影到序列化模型中,该模型可能具有不同的规则,并且对于序列化程序更方便。在这种情况下,就继承树而言,序列化类型可能不是1:1映射。

另一个选择是强制迁移旧数据,因此:用旧布局反序列化它,并用新布局重新序列化它。这是在接受继承性更改,而不是假装没有发生。


以下内容显示了最终的.proto布局,以解释为什么FooC在v2中最终以FooD的形式出现:

FooA-FooC开始的v1是201,201(FooD是201,201,201)

syntax = "proto2";

message FooA {
   optional double A = 1;
   optional double B = 2;
   oneof subtype {
      FooB FooB = 201;
      FooA1 FooA1 = 202;
   }
}
message FooA1 {
}
message FooB {
   optional double C = 1;
   oneof subtype {
      FooC FooC = 201;
   }
}
message FooC {
   oneof subtype {
      FooD FooD = 201;
   }
}
message FooD {
   optional double D = 1;
}
FooA1-FooD开始的

v2是201,201:

syntax = "proto2";

message FooA1 {
   optional double A = 1;
   optional double B = 2;
   oneof subtype {
      FooC FooC = 201;
   }
}
message FooC {
   optional double C = 1;
   oneof subtype {
      FooD FooD = 201;
   }
}
message FooD {
   optional double D = 1;
}

答案 1 :(得分:0)

由于@Marc的解释,我找到了一种通过使用一些帮助器类来处理此特定用例的方法。

我将解决方案发布到这里对其他人有帮助。

版本2可以定义如下:

public class FooA
{
    public double A { get; set; }
    public double B { get; set; }
}

public class FooAFake
{
    public double A { get; set; }
    public double B { get; set; }
}

public class FooB : FooA
{
    public double C { get; set; }
}

public class FooBFake : FooAFake
{
    public double C { get; set; }
}

public class FooC : FooBFake
{        
}

public class FooD : FooC
{
    public double D { get; set; }
}

public class FooA1 : FooAFake
{        
}

并以这种方式建模:

        Model = RuntimeTypeModel.Create();

        Model.Add(typeof(FooA), false)
            .AddSubType(201, typeof(FooB))
            //.AddSubType(202, typeof(FooA1))
            .Add(1, "A")
            .Add(2, "B");

        Model[typeof(FooB)]
            //.AddSubType(201, typeof(FooC))
            .Add(1, "C");

        Model[typeof(FooC)]
            .AddSubType(201, typeof(FooD));                

        Model[typeof(FooD)]
            .Add(1, "D");

        Model.Add(typeof(FooAFake), false)
            .AddSubType(201, typeof(FooBFake))
            .AddSubType(202, typeof(FooA1))
            .Add(1, "A")
            .Add(2, "B");

        Model[typeof(FooBFake)]
            .AddSubType(201, typeof(FooC))
            .Add(1, "C");