【问题标题】:Protobuf how to change inheritance hierarchy without losing backward compatibilityProtobuf 如何在不失去向后兼容性的情况下更改继承层次结构
【发布时间】:2019-05-30 10:49:51
【问题描述】:

我有一个使用 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 序列化期间的类型相同。

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

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

更新

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

【问题讨论】:

    标签: c# serialization hierarchy protobuf-net


    【解决方案1】:

    我想不出一种在不破坏兼容性的情况下进行更改的方法。当我们任何人陷入困境时,我的建议是:将“真实”类型和序列化类型分成单独的类型模型,这些模型不需要彼此之间的 1:1 映射。然后你可以对“真实”类型(域模型)做任何你想做的事情,你只需将这些值投影到序列化模型中,这可能有不同的规则并且对序列化程序更方便。在这种情况下,就继承树而言,序列化类型可能不是 1:1 映射。

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


    以下是显示生成的 .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,我不明白为什么对象 FooC 被反序列化为版本 2 的 FooD。我试图调试它(参见帖子中的更新),但我不明白为什么会发生这种情况。您能否更好地解释为什么无法处理这种情况?谢谢。
    • @ilCosmico 架构添加(参见Model.GetSchema(Type))以解释FooC/FooD 的事情
    • 嗨@Marc,感谢您的解释,现在更清楚了,我想尝试一下我想到的解决方案。模型中定义的类型顺序重要吗?我的意思是,保留 0=FooA、1=FooB、2=FooC...等很重要,或者只是字段 id 和 subType id 在 Version1 和 Version2 中必须相同?
    • @ilCosmico 数字才是最重要的;这就是为什么您需要提供明确的数字
    • @ilCosmico 哦,添加 root 类型的顺序无关紧要 - 我的意思是添加子类型(和字段)时
    【解决方案2】:

    感谢@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");
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-06-16
      • 2019-08-29
      • 2016-10-29
      • 2013-06-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多