意外的protobuf-net序列化程序行为

时间:2017-12-14 17:57:57

标签: c# serialization protobuf-net

我们正在使用protobuf-net v.2.3.2来序列化和反序列化我们项目中的一些复杂对象(里面有列表,字典等)。大多数情况下,一切都很好,但在极少数情况下,我们遇到了非常奇怪的行为:如果调用序列化程序.FromProto<SomeComplexType>(bytes),则在一个进程中序列化的对象会导致其他进程中的反序列化错误第二个过程中的方法之前没有调用.ToProto(someComplexObject)

这是一个例子:让我们说我们的流程1看起来像这样:

class Program1 {
    public static void Main()
    {
        SomeComplexType complexObject = new SomeComplexType();

        // Here goes some code filling complexObject with data

        byte[] serialized = ToProto(complexObject);

        File.WriteAllBytes("serialized.data", serialized);
    }

    public static byte[] ToProto(object value)
    {
        using (var stream = new MemoryStream())
        {
            ProtoBuf.Serializer.Serialize(stream, value);
            return stream.ToArray();
        }
    }

    public static T FromProto<T>(byte[] value)
    {
        using (var stream = new MemoryStream(value))
        {
            return ProtoBuf.Serializer.Deserialize<T>(stream);
        }
    }
}

现在,我们正在尝试在流程2中读取该对象:

class Program2 {
    public static void Main()
    {
        byte[] serialized = File.ReadAllBytes("serialized.data");

        SomeComplexType complexObject =                
            FromProto<SomeComplexType>(serialized);
    }

    public static byte[] ToProto(object value)
    {
        using (var stream = new MemoryStream())
        {
            ProtoBuf.Serializer.Serialize(stream, value);
            return stream.ToArray();
        }
    }

    public static T FromProto<T>(byte[] value)
    {
        using (var stream = new MemoryStream(value))
        {
            return ProtoBuf.Serializer.Deserialize<T>(stream);
        }
    }
}

我们看到的是,在极少数情况下,进程1生成的文件使进程2在调用FromProto时失败(我们观察到各种错误,从&#39;缺少无参数构造函数开始直到StackOverflowException)。

但是,在调用FromProto之前的某处添加这样的行:ToProto(new SomeComplexType());会使错误消失,并且正在反序列化相同的字节集而没有任何障碍。没有其他方法(我们尝试过PrepareSerializer,GetSchema)似乎可以解决问题。

看起来ToProto和FromProto在如何解析对象模型方面存在一些细微差别。另一点是ProtoBuf似乎记得&#34;调用ToProto之后的状态,可以帮助它进行后续反序列化。

更新 这是更多细节: 我们看到的类结构与此类似(非常简化):

[ProtoContract(ImplicitFields = ImplicitFields.AllPublic)]
[ProtoInclude(1, typeof(A))]
[ProtoInclude(2, typeof(B))]
public interface IBase
{
    [ProtoIgnore]
    string Id { get; }
}

[ProtoContract(ImplicitFields=ImplicitFields.AllPublic, AsReferenceDefault=true)]
public class A : IBase
{
    [ProtoIgnore]
    public string Id { get; }

    public string PropertyA { get; set; }
}

[ProtoContract(ImplicitFields=ImplicitFields.AllPublic, AsReferenceDefault=true)]
public class B : IBase
{
    [ProtoIgnore]
    public string Id { get; }

    public string PropertyB { get; set; }
}

[ProtoContract(ImplicitFields=ImplicitFields.AllPublic, AsReferenceDefault=true)]
public class C
{
    public List<IBase> ListOfBase = new List<IBase>();
}

[ProtoContract(ImplicitFields=ImplicitFields.AllPublic, AsReferenceDefault=true)]
public class D
{
    public C PropertyC { get; set; }
    public Dictionary<string, B> DictionaryOfBs { get; set; }
}

问题的根本原因似乎是某种非确定性的方式,其中Protobuf-net为类型准备序列化器。这是我们观察到的。

假设我们有两个项目:生产者和消费者。 Producer创建D的实例,添加一些数据并使用protobuf-net序列化该实例。消费者获取序列化数据并将其反序列化为D的实例。

在生产者中,protobuf有时会在发现IBase之前发现B类,因此它会为B生成序列化器并将DictionaryOfBs中的值序列化为B的直接实例。

在消费者中,protobuf-net可能会首先发现IBase,因此当它为B准备(de)序列化器时,它会将其视为IBase的子类。因此,在对DictionaryOfBs的值进行反序列化时,它会尝试将它们作为IBase的子类读取,期望字段编号区分A和B.流中的数据可能是IBase序列化程序决定它看到的是一个实例A,尝试将其转换为B(使用Merge方法)并进入无限递归,尝试将A转换为B转换为A转换为B等,从而导致最终的StackOverflowException。

在反序列化之前添加 Serializer.Serialize(stream,new D())会更改序列化程序的创建顺序,因此在这种情况下没有错误,尽管这似乎是一个幸运的巧合。不幸的是,在我们的情况下,即使这不能用作令人满意的解决方法,因为这会偶尔导致&#34;内部错误;发生密钥不匹配&#34; 反序列化错误。

1 个答案:

答案 0 :(得分:1)

序列化代码使用通用API,但由于泛型类型推断而使用<object>。这可能会让事情变得混乱。要尝试的第一件事是使用ToProto方法Serializer.NonGeneric.Serialize - 这将使用.GetType()等,并且应该更少地混淆它。

或者:通过ToProto制作T value通用。

注意:我没有对此进行过测试 - 但这是首先尝试的。