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

Protobuf how to change inheritance hierarchy without losing backward compatibility

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

假设我们有这些示例 classes(将其标记为版本 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; }
}

以及根据它的模型定义,试图为每个先前定义的 class 保留相同的 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 序列化期间的类型相同。

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

我做错了什么?有办法处理吗?

更新

FooC被版本2反序列化时,尝试调试protobuf-net源代码,方法RuntimeTypeModel.GetKey() 从基数 class (getBaseKey=true) 开始,正确得到 FooA1 (key=2) 但最后得到 FooD 对象而不是 FooC。也许有没有办法以不同的方式处理此方法以允许这样的场景?

我想不出一种在不破坏兼容性的情况下进行更改的方法。当我们陷入困境时,我的建议是:将 "real" 类型和序列化类型拆分为单独的类型模型,这些模型不需要 1:1 映射到 each-other。然后你可以对 "real" 类型(域模型)做任何你想做的事情,你只需 将这些值投影 到序列化模型中,它可能有不同的规则并且更多方便序列化器。在这种情况下,序列化类型可能不是继承树方面的 1:1 映射。

另一种选择是强制迁移旧数据,所以:用旧布局反序列化,re-serialize用新布局反序列化。这是拥抱继承变化,而不是试图假装它没有发生。


以下显示生成的 .proto 布局,以解释为什么 FooC 在 v2 中最终成为 FooD

v1 从 FooA - FooC 开始是 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;
}

v2 从 FooA1 - FooD 开始是 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;
}

感谢@Marc 的解释,我找到了一种方法来处理这个特定的 use-case,方法是使用一些帮助程序 类。

我post我的解决方案是否对其他人有帮助。

版本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");