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

Posted

技术标签:

【中文标题】protobuf-net、版本控制和代理类型的最佳实践【英文标题】:Best practice for protobuf-net, versioning and surrogate types 【发布时间】:2013-01-23 23:10:47 【问题描述】:

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

我们有 A 类,它被认为是版本 1 A 类的一个实例已序列化到磁盘 我们现在有了 B 类,它被认为是 A 类的第 2 版(A 类有很多问题,我们必须为下一个版本创建 B 类)。 A 类仍然存在于代码中,但仅用于遗留目的。 我想将 version:1 数据(存储到磁盘)反序列化为 B 类实例,并使用逻辑例程将数据从以前的 A 类实例转换为 B 类的新实例。 B 类的实例将在运行期间序列化到磁盘。 应用程序应该反序列化 A 类和 B 类的实例。

想到了数据契约代理和 DataContractSerializer 的概念。目标是将 version: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
     ...

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

【问题讨论】:

你提前知道你是在读v1 vs v2数据吗? v1 和 v2 有什么不同?在您的第 4 个项目符号上 - 为什么要将 v1 数据读入 B,而不是将其读入 A 然后将 A 转换为 B? 我不会知道的。为了扩展这一点,我必须假设我可能会遇到 V1、V2、V3、V4 等。差异可能很大——想想替换整组类。 “为什么要将 v1 数据读入 B,而不是将其读入 A 然后将 A 转换为 B”- 肯定想这样做,但我不知道它是否是 A或 B 数据提前。 v1 da 是否已经存在?如果我们仍然有能力控制一个主根节点(A 以上),我可以想办法做到这一点。我仍然可以用一个基本的例子来说明 A 和 B 是的,v1 数据已经存在。在上面进行编辑以包含 A 和 B 的示例。 【参考方案1】:

对;看着他们,你确实完全改变了合同。我知道没有基于合约的序列化程序会因此而爱你,protobuf-net 也不例外。如果您已经有一个根节点,您可以执行类似的操作(在伪代码中):

[Contract]
class Wrapper 
    [Member] public A A get;set;
    [Member] public B B get;set;
    [Member] public C C get;set;

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

添加 lots 的 shim 属性以实现兼容性; 非常可维护,我不推荐它 首先嗅探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&lt;T&gt; API,或通过来自两个非通用API(Serializer.NonGeneric.DeserializeRuntimeTypeModel.Default.Deserialize)的Type 实例 - 无论哪种方式,您都会到达同一个地方;这实际上是一个是否通用的案例或非泛型最方便)。

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

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

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

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

【讨论】:

是的,这就是我一直在努力的方向——获得一个围绕版本的标准,询问它,然后发送到一些基本的转换方法。如果还不明显,这些不是我的要求(是的,继承的代码。)【参考方案2】:

我没有预期的做法,但这是我要做的。

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

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

还在您的ProtoBeforeSerialization 中抛出某种形式的异常,以便您不再尝试写出旧的异常。

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

<ProtoBeforeSerialization()>
Private Sub BeforeSerialisaton()

End Sub

<ProtoAfterSerialization()>
Private Sub AfterSerialisaton()

End Sub

<ProtoBeforeDeserialization()>
Private Sub BeforeDeserialisation()

End Sub

<ProtoAfterDeserialization()>
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<A>(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<ComplexThings>();
        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<ComplexThing> 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<ComplexThing> 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; 

【讨论】:

谢谢保罗。你能提供一些关于 onDeserialized 调用的代码吗?我没有看到可以在哪里手动传输数据(我的意思是源对象)。 好吧,也许我不是很清楚。我有在 V1 中序列化的数据,但我想在 V2 模式下操作。数据将被再次序列化,以 V2 格式继续。因此,虽然这种方法适用于 V1 格式的数据,但当它遇到现在以 V2 格式序列化的相同数据时,它就不起作用了。 V2 数据应该反序列化到您的 V2 对象中。根据我的理解,V1 是遗留问题,您只需要一种从 V1 -> V2 迁移的方法

以上是关于protobuf-net、版本控制和代理类型的最佳实践的主要内容,如果未能解决你的问题,请参考以下文章

使用带有 protobuf-net 异常的代理对 List<T> 进行序列化

protobuf-net 版本容差

protobuf-net:如何注释派生类型的属性?

微服务版本最佳实践

使用 protobuf-net 反序列化 int& 类型

有没有办法在 protobuf-net 代理类中定义替代转换函数(从/到接口)