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 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(); 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 ComplexThings {get;set;} //Also a DataContract ... } 

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时的预期做法是什么?

对; 看着他们,你确实完全改变了合同。 我知道没有基于合同的序列化器能够为你所爱,而protobuf-net也不例外。 如果你已经有了一个根节点,你可以做一些事情(在伪代码中):

 [Contract] class Wrapper { [Member] public AA {get;set;} [Member] public BB {get;set;} [Member] public CC {get;set;} } 

只需选择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 

现在您可以使用序列化程序,告诉它序列化的version ; 通过通用的Serializer.Deserialize API,或通过两个非通用API中的Type实例( Serializer.NonGeneric.DeserializeRuntimeTypeModel.Default.Deserialize – 无论哪种方式,你到达同一个地方;它确实是通用或非通用是最方便的情况)。

那么你需要一些A / B / C之间A转换代码 – 通过你自己的自定义操作符/方法,或者像auto-mapper这样的东西。

如果你不想要任何ProtoReader代码,你也可以这样做:

 [DataContract] class VersionStub { [DataMember(Order=VERSION_FIELD_NUMBER)] public int Version {get;set;} } 

并运行Deserialize ,它将允许您访问Version ,然后您可以使用它来执行特定于类型的反序列化; 这里的主要区别是ProtoReader代码允许您在拥有版本号时立即短路。

我没有预期的练习,但这就是我要做的。

鉴于您仍然可以访问V1类,在V1类上添加一个提供V2实例的属性。

在V1的ProtoAfterDeserialization中创建一个V2的实例并看到它是一个迁移我建议手动转移你需要的东西(或者如果它不太难,请尝试Merge YMMV)。

同样在ProtoBeforeSerialization抛出某种forms的exception,这样你就不会再尝试写出旧的exception了。

编辑:使用这些的例子(VB代码)

  Private Sub BeforeSerialisaton() End Sub  Private Sub AfterSerialisaton() End Sub  Private Sub BeforeDeserialisation() End Sub  Private Sub AfterDeserialisation() End Sub 

在看到你的榜样后,我希望这能满足你的目标。 Class1是你加载/转换的方式。

 using ProtoBuf; using System.Collections.Generic; using System.IO; public class Class1 { public Class1() { using (FileStream fs = new FileStream("c:\\formatADataFile.dat", FileMode.Open, FileAccess.Read)) { A oldA = Serializer.Deserialize(fs); B newB = oldA.ConvertedToB; } } } [ProtoContract()] public class B { public B(A a) { //Property now has a different name this.Enabled = a.IsActive; //Property requires math for correctness this.TimeInMilliseconds = a.TimeInSeconds * 1000; this.Name = a.Name; //Reference objects change, too this.CustomObject2 = new CustomObject2(a.CustomObj); this.ComplexThings = new List(); this.ComplexThings.AddRange(a.ComplexThings); //... } public B() { } //[DataMember] [ProtoMember(1)] public bool Enabled { get; set; } //[DataMember] public int Version { get { return 2; } private set { } } [ProtoMember(2)] public double TimeInMilliseconds { get; set; } [ProtoMember(3)] public string Name { get; set; } [ProtoMember(4)] public CustomObject2 CustomObject { get; set; } //Also a DataContract [ProtoMember(5)] public List ComplexThings { get; set; } //Also a DataContract ///... } [ProtoContract()] public class CustomObject2 { public CustomObject2() { Something = string.Empty; } [ProtoMember(1)] public string Something { get; set; } } [ProtoContract()] public class A { public A() { mBConvert = new B(); } [ProtoMember(1)] public bool IsActive { get; set; } [ProtoMember(2)] public int VersionNumber { get { return 1; } private set { } } [ProtoBeforeSerialization()] private void NoMoreSavesForA() { throw new System.InvalidOperationException("Do Not Save A"); } private B mBConvert; [ProtoAfterDeserialization()] private void TranslateToB() { mBConvert = new B(this); } public B ConvertedToB { get { return mBConvert; } } [ProtoMember(3)] public int TimeInSeconds { get; set; } [ProtoMember(4)] public string Name { get; set; } [ProtoMember(5)] public CustomObject CustomObj { get; set; } //Also a DataContract [ProtoMember(6)] public List ComplexThings { get; set; } //Also a DataContract //... } [ProtoContract()] public class CustomObject { public CustomObject() { } [ProtoMember(1)] public int Something { get; set; } } [ProtoContract()] public class ComplexThing { public ComplexThing() { } [ProtoMember(1)] public int SomeOtherThing { get; set; } }