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

6
我正在尝试使用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
     ...
}

Class A 是我们对象的第一次迭代,目前仍在使用。数据以 v1 格式存在,使用 class A 进行序列化。

意识到问题后,我们创建了一个新结构,叫做 class B。A 和 B 之间有很多变化,所以我们认为创建 B 更好,而不是改编原来的 class A。

但是我们的应用程序已经存在,并且正在使用 class A 进行数据序列化。我们准备推出我们的更改,但是我们必须首先对版本 1(使用 class A)创建的数据进行反序列化,并将其实例化为 class B。数据足够重要,我们不能仅仅通过在 class B 中添加默认值来处理缺失的数据,而是必须将数据从 class A 实例转换到 class B。一旦我们有了 class B 实例,应用程序就会再次以 class B 格式(版本 2)进行数据序列化。

我们假设将来会对 class B 进行修改,并希望能够迭代到版本 3,可能在一个新类“C”中。我们有两个目标:处理已经存在的数据,并准备好对象进行未来的迁移。

现有的“过渡”属性(OnSerializing/OnSerialized、OnDeserializing/OnDeserialized 等)没有提供访问先前数据的方法。

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


你能事先确定你正在读取v1还是v2数据吗?v1和v2有多大差异?在您的第四个要点上,为什么要将v1数据读入B中,而不是将其读入A,然后再将A转换为B? - Marc Gravell
我不会知道它。为了进一步说明,我必须假设我可能会遇到V1、V2、V3、V4等版本。而且差异可能是实质性的——可以考虑替换整个类集。 - jro
为什么要将v1数据读入B,而不是先将其读入A,然后将A转换为B?我肯定想这样做,但事先我不知道它是A数据还是B数据。 - jro
V1数据已经存在了吗?如果我们仍然能够控制主根节点(在A之上),我可以想出一些方法来实现这一点。我仍然非常需要一个基本示例,说明A和B。 - Marc Gravell
是的,v1数据已经存在。编辑上面的内容以包括A和B的示例。 - jro
2个回答

5

好的,看起来你已经完全修改了合同。我不知道任何基于合同的序列化程序会喜欢你这样做,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,这就变得困难了。我能想到两种方法:

  • 为兼容性添加很多适配器属性;不太可维护,我不建议使用
  • 首先检测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

现在,您可以使用序列化器,告诉它序列化的版本,通过通用的Serializer.Deserialize<T> 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<VersionStub>,它将为您提供访问Version的权限,然后您可以使用它来执行类型特定的反序列化;这里的主要区别在于ProtoReader代码允许您在拥有版本号时立即进行短路处理。


是的,这就是我一直在走的路--获得一个标准版本,询问它,然后发送到一些基本的转换方法。如果还不明显,这些不是我的要求(耶,继承的代码)。 - jro

2
我没有一个确定的实践方法,但这是我会做的事情。
考虑到您仍然可以访问您的V1类,请在V1类上添加一个属性,该属性提供了一个V2实例。
在V1的ProtoAfterDeserialization中创建一个V2实例,并且由于这是一次迁移,我建议手动传输所需内容(或者如果不太困难,请尝试合并YMMV)。
此外,在您的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; }
}

谢谢Paul。你能提供一些关于onDeserialized调用的代码吗?我没有看到在哪里可以手动传输数据(我的意思是源对象)。 - jro
好的,也许我表达不够清晰。我有一些以V1格式序列化的数据,但我想在V2模式下操作。这些数据将会再次被序列化,采用V2格式。因此,虽然这种方法适用于V1格式的数据,但当它遇到现在以V2格式序列化的相同数据时,就无法使用了。 - jro
V2 数据应反序列化为您的 V2 对象。根据我的理解,V1 是遗留版本,您只需要找到一种从 V1 迁移到 V2 的方法。 - Paul Farry

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接