protobuf-net,版本控制和代理类型的最佳实践

jro*_*jro 5 c# versioning protobuf-net

我正在尝试使用protobuf-net(Marc Gravell的实现)来确定如何解决这个用例.

  • 我们有A类,它被认为是版本1
  • A类的实例已序列化为磁盘
  • 我们现在有了B类,它被认为是A类的第2版(A类有很多错误,我们必须为下一个版本创建B类).A类仍然存在于代码中,但仅用于传统目的.
  • 我想将版本:1数据(存储到磁盘)反序列化为B类实例,并使用逻辑例程将数据从先前的A类实例转换为B类的新实例.
  • B类实例将在操作期间序列化为磁盘.
  • 应用程序应该期望反序列化A类和B类的实例.

数据契约代理的概念和DataContractSerializer浮现在脑海中.目标是将版本:1数据转换为新的B类结构.

一个例子:

[DataContract]
public class A {

     public A(){}

     [DataMember]
     public bool IsActive {get;set;]

     [DataMember]
     public int VersionNumber {
          get { return 1; }
          set { }
     }

     [DataMember]
     public int TimeInSeconds {get;set;}

     [DataMember]
     public string Name {get;set;}

     [DataMember]
     public CustomObject CustomObj {get;set;} //Also a DataContract

     [DataMember]
     public List<ComplexThing> ComplexThings {get;set;} //Also a DataContract
     ...
}

[DataContract]
public class B {

     public B(A a) {
          this.Enabled = a.IsActive; //Property now has a different name
          this.TimeInMilliseconds = a.TimeInSeconds * 1000; //Property requires math for correctness
          this.Name = a.Name;
          this.CustomObject2 = new CustomObject2(a.CustomObj); //Reference objects change, too
          this.ComplexThings = new List<ComplexThings>();
          this.ComplexThings.AddRange(a.ComplexThings);
          ...
     }

     public B(){}

     [DataMember]
     public bool Enabled {get;set;]

     [DataMember]
     public int Version {
          get { return 2; }
          set { }
     }

     [DataMember]
     public double TimeInMilliseconds {get;set;}

     [DataMember]
     public string Name {get;set;}

     [DataMember]
     public CustomObject2 CustomObject {get;set;} //Also a DataContract

     [DataMember]
     public List<ComplexThing> ComplexThings {get;set;} //Also a DataContract
     ...
}
Run Code Online (Sandbox Code Playgroud)

A类是我们对象的第一次迭代,并且正在积极使用.数据以v1格式存在,使用类A进行序列化.

在意识到我们的方法错误后,我们创建了一个名为B类的新结构.A和B之间有很多变化,我们觉得创建B更好,而不是调整原来的A类.

但是我们的应用程序已经存在,并且正在使用A类来序列化数据.我们已经准备好将我们的更改推向世界,但我们必须首先对在版本1下创建的数据进行反序列化(使用类A)并将其实例化为类B.数据非常重要,我们不能只假设类中的默认值B表示缺少数据,但我们必须将数据从A类实例转换为B类.一旦我们有了B类实例,应用程序将再次以B类格式(版本2)序列化该数据.

我们假设我们将来会对B类进行修改,我们希望能够迭代到版本3,也许是在新的类"C"中.我们有两个目标:已经存在的地址数据,并为将来的迁移准备我们的对象.

现有的"转换"属性(OnSerializing/OnSerialized,OnDeserializing/OnDeserialized等)不提供对先前数据的访问.

在这种情况下使用protobuf-net时的预期做法是什么?

Mar*_*ell 5

正确的; 看看他们,你确实彻底改变了合同。据我所知,没有哪个基于合约的序列化器会因此而喜欢你,protobuf-net 也不例外。如果您已经有根节点,您可以执行类似的操作(以伪代码形式):

[Contract]
class Wrapper {
    [Member] public A A {get;set;}
    [Member] public B B {get;set;}
    [Member] public C C {get;set;}
}
Run Code Online (Sandbox Code Playgroud)

然后选择 A/B/C 中非空的一个,也许在它们之间添加一些转换运算符。然而,如果旧数据中只有一个裸露的 A,这会变得很困难。我能想到的有两种方法:

  • 添加大量垫片属性以实现兼容性;不太可维护,我不推荐它
  • Version作为初始步骤嗅探,并告诉序列化器会发生什么。

例如,您可以这样做:

int version = -1;
using(var reader = new ProtoReader(inputStream)) {
    while(reader.ReadFieldHeader() > 0) {
        const int VERSION_FIELD_NUMBER = /* todo */;
        if(reader.FieldNumber == VERSION_FIELD_NUMBER) {
            version = reader.ReadInt32();
            // optional short-circuit; we're not expecting 2 Version numbers
            break;
        } else {
            reader.SkipField();
        }
    }
}
inputStream.Position = 0; // rewind before deserializing
Run Code Online (Sandbox Code Playgroud)

现在您可以使用序列化器,告诉它version它被序列化为什么;要么通过通用Serializer.Deserialize<T>API,要么通过Type两个非通用 API 的实例(Serializer.NonGeneric.Deserialize或者RuntimeTypeModel.Default.Deserialize- 无论哪种方式,您都会到达同一个地方;这实际上是通用或非通用是否最方便的情况)。

然后你需要一些A/ B/之间的转换代码C- 或者通过你自己的自定义运算符/方法,或者通过自动映射器之类的东西。

如果您不希望任何ProtoReader代码乱七八糟,您也可以这样做:

[DataContract]
class VersionStub {
    [DataMember(Order=VERSION_FIELD_NUMBER)]
    public int Version {get;set;}
}
Run Code Online (Sandbox Code Playgroud)

并运行Deserialize<VersionStub>,这将使您能够访问Version,然后您可以使用它来执行特定于类型的反序列化;这里的主要区别在于,ProtoReader一旦有了版本号,代码就允许您短路。