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");
我有一个使用 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");