Protobuf-net не будет десериализовать данные из Protobuf.js

Я использую Protobuf для связи между моим веб-клиентом и сервером (C #) с помощью WebSocket. На клиенте де / сериализация выполняется с помощью Protobuf.js, а на сервере используя protobuf-net.

Проблема в том, что при использовании агрегации с абстрактными классами protobuf-net не может десериализовать данные, отправленные Protobuf.js.

Это трассировка стека:

ProtoException: No parameterless constructor found for Base.
at ProtoBuf.Meta.TypeModel.ThrowCannotCreateInstance(Type type) na c:\Dev\protobuf-net\protobuf-net\Meta\TypeModel.cs:line 1397
at proto_6(Object , ProtoReader )
at ProtoBuf.Serializers.CompiledSerializer.ProtoBuf.Serializers.IProtoSerializer.Read(Object value, ProtoReader source) na c:\Dev\protobuf-net\protobuf-net\Serializers\CompiledSerializer.cs:line 57
at ProtoBuf.Meta.RuntimeTypeModel.Deserialize(Int32 key, Object value, ProtoReader source) na c:\Dev\protobuf-net\protobuf-net\Meta\RuntimeTypeModel.cs:line 775
at ProtoBuf.ProtoReader.ReadTypedObject(Object value, Int32 key, ProtoReader reader, Type type) na c:\Dev\protobuf-net\protobuf-net\ProtoReader.cs:line 579
at ProtoBuf.ProtoReader.ReadObject(Object value, Int32 key, ProtoReader reader) na c:\Dev\protobuf-net\protobuf-net\ProtoReader.cs:line 566
at proto_2(Object , ProtoReader )
at ProtoBuf.Serializers.CompiledSerializer.ProtoBuf.Serializers.IProtoSerializer.Read(Object value, ProtoReader source) na c:\Dev\protobuf-net\protobuf-net\Serializers\CompiledSerializer.cs:line 57
at ProtoBuf.Meta.RuntimeTypeModel.Deserialize(Int32 key, Object value, ProtoReader source) na c:\Dev\protobuf-net\protobuf-net\Meta\RuntimeTypeModel.cs:line 775
at ProtoBuf.Meta.TypeModel.DeserializeCore(ProtoReader reader, Type type, Object value, Boolean noAutoCreate) na c:\Dev\protobuf-net\protobuf-net\Meta\TypeModel.cs:line 700
at ProtoBuf.Meta.TypeModel.Deserialize(Stream source, Object value, Type type, SerializationContext context) na c:\Dev\protobuf-net\protobuf-net\Meta\TypeModel.cs:line 589
at ProtoBuf.Meta.TypeModel.Deserialize(Stream source, Object value, Type type) na c:\Dev\protobuf-net\protobuf-net\Meta\TypeModel.cs:line 566
at ProtoBuf.Serializer.Deserialize[T](Stream source) na c:\Dev\protobuf-net\protobuf-net\Serializer.cs:line 77
at ProtobufPolymorphismTest.Program.Main(String[] args) na c:\Desenvolvimento\Testes\ProtobufPolymorphismTest\ProtobufPolymorphismTest\Program.cs:line 30
at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)
at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
at System.Threading.ThreadHelper.ThreadStart()

Это контракт:

[ProtoContract]
[ProtoInclude(100, typeof(Child))]
abstract class Base
{
    [ProtoMember(1)]
    public int BaseProperty { get; set; }
}

[ProtoContract]
class Child : Base
{
    [ProtoMember(1)]
    public float ChildProperty { get; set; }
}

[ProtoContract]
class Request
{
    [ProtoMember(1)]
    public Base Aggregate { get; set; }
}

И это код для воспроизведения ошибки. Поскольку сериализация работает, я предоставляю результат только в виде байтового массива. Если это поможет, я могу предоставить шаги, которые я предпринял, чтобы перейти к сериализованным значениям.

// This is the object serialized
Child child = new Child() { ChildProperty = 0.5f, BaseProperty = 10 };
Request request = new Request() { Aggregate = child };

// This is the byte representation generated by protobuf-net and Protobuf.js
byte[] protoNet = new byte[] { 10, 10, 162, 6, 5, 13, 0, 0, 0, 63, 8, 10 };
byte[] protoJS = new byte[] { 10, 10, 8, 10, 162, 6, 5, 13, 0, 0, 0, 63 };

// Try to deserialize the protobuf-net data
using (System.IO.MemoryStream ms = new System.IO.MemoryStream(protoNet))
{
    request = Serializer.Deserialize<Request>(ms); // Success
}

// Try to deserialize the Protobuf.js data
using (System.IO.MemoryStream ms = new System.IO.MemoryStream(protoJS))
{
    request = Serializer.Deserialize<Request>(ms); // ProtoException: No parameterless constructor found for Base.
}

Если я добавлю SkipConstructor = true в определение базового класса, ошибка изменится на «MemberAccessException: невозможно создать абстрактный класс» со следующей трассировкой стека. Если я удалю абстракцию из определения базового класса, она будет работать должным образом.

System.MemberAccessException: Cannot create an abstract class.
at System.Runtime.Serialization.FormatterServices.nativeGetUninitializedObject(RuntimeType type)
at ProtoBuf.BclHelpers.GetUninitializedObject(Type type) na c:\Dev\protobuf-net\protobuf-net\BclHelpers.cs:line 38
at proto_6(Object , ProtoReader )
at ProtoBuf.Serializers.CompiledSerializer.ProtoBuf.Serializers.IProtoSerializer.Read(Object value, ProtoReader source) na c:\Dev\protobuf-net\protobuf-net\Serializers\CompiledSerializer.cs:line 57
at ProtoBuf.Meta.RuntimeTypeModel.Deserialize(Int32 key, Object value, ProtoReader source) na c:\Dev\protobuf-net\protobuf-net\Meta\RuntimeTypeModel.cs:line 775
at ProtoBuf.ProtoReader.ReadTypedObject(Object value, Int32 key, ProtoReader reader, Type type) na c:\Dev\protobuf-net\protobuf-net\ProtoReader.cs:line 579
at ProtoBuf.ProtoReader.ReadObject(Object value, Int32 key, ProtoReader reader) na c:\Dev\protobuf-net\protobuf-net\ProtoReader.cs:line 566
at proto_2(Object , ProtoReader )
at ProtoBuf.Serializers.CompiledSerializer.ProtoBuf.Serializers.IProtoSerializer.Read(Object value, ProtoReader source) na c:\Dev\protobuf-net\protobuf-net\Serializers\CompiledSerializer.cs:line 57
at ProtoBuf.Meta.RuntimeTypeModel.Deserialize(Int32 key, Object value, ProtoReader source) na c:\Dev\protobuf-net\protobuf-net\Meta\RuntimeTypeModel.cs:line 775
at ProtoBuf.Meta.TypeModel.DeserializeCore(ProtoReader reader, Type type, Object value, Boolean noAutoCreate) na c:\Dev\protobuf-net\protobuf-net\Meta\TypeModel.cs:line 700
at ProtoBuf.Meta.TypeModel.Deserialize(Stream source, Object value, Type type, SerializationContext context) na c:\Dev\protobuf-net\protobuf-net\Meta\TypeModel.cs:line 589
at ProtoBuf.Meta.TypeModel.Deserialize(Stream source, Object value, Type type) na c:\Dev\protobuf-net\protobuf-net\Meta\TypeModel.cs:line 566
at ProtoBuf.Serializer.Deserialize[T](Stream source) na c:\Dev\protobuf-net\protobuf-net\Serializer.cs:line 77
at ProtobufPolymorphismTest.Program.Main(String[] args) na c:\Desenvolvimento\Testes\ProtobufPolymorphismTest\ProtobufPolymorphismTest\Program.cs:line 30
at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)
at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
at System.Threading.ThreadHelper.ThreadStart()

Я не уверен, почему двоичное представление, сгенерированное с помощью protobuf-net и Protobuf.js, отличается, но они оба кажутся действительными, поскольку работают, если базовый класс не является абстрактным.

Любые идеи о том, почему это происходит, или способ обойтись без удаления абстрактного из базового класса?

Заранее спасибо!

ОБНОВЛЕНИЕ

Это код, который я использовал для генерации байтовой сериализации через Protobuf.js:

<script src="//raw.githubusercontent.com/dcodeIO/ByteBuffer.js/master/dist/ByteBufferAB.min.js"></script>
<script src="//cdn.rawgit.com/dcodeIO/ProtoBuf.js/master/dist/ProtoBuf.js"></script>
<script type="text/javascript">
    // Proto file
    var proto = "";
    proto += "package ProtobufPolymorphismTest;\r\n\r\n";
    proto += "message Base {\r\n";
    proto += "    optional int32 BaseProperty = 1 [default = 0];\r\n";
    proto += "    // the following represent sub-types; at most 1 should have a value\r\n";
    proto += "    optional Child Child = 100;\r\n";
    proto += "}\r\n\r\n";
    proto += "message Child {\r\n";
    proto += "    optional float ChildProperty = 1 [default = 0];\r\n";
    proto += "}\r\n\r\n";
    proto += "message Request {\r\n";
    proto += "    optional Base Aggregate = 1;\r\n";
    proto += "}";

    // Build the entities
    var protoFile = dcodeIO.ProtoBuf.loadProto(proto);
    var requestClass = protoFile.build("ProtobufPolymorphismTest.Request");
    var baseClass = protoFile.build("ProtobufPolymorphismTest.Base");
    var childClass = protoFile.build("ProtobufPolymorphismTest.Child");

    // Build the request
    var base = new baseClass();
    base.BaseProperty = 10;
    base.Child = new childClass();
    base.Child.ChildProperty = 0.5;
    var request = new requestClass();
    request.Aggregate = base;

    // Serialize
    var bytes = new Uint8Array(request.toArrayBuffer());
    var str = "new byte[] { " + bytes.join(", ") + " };";
    console.log(str);
</script>

РЕШЕНИЕ

Как объяснил Марк, protobuf-net не поддерживает полиморфизм при инвертировании порядка полей. В качестве обходного пути, специфичного для Protobuf.js, вы можете изменить порядок полей в файле .proto, чтобы он сериализовался в правильном порядке.

В моем случае изменение файла .proto на следующее решило проблему:

package ProtobufPolymorphismTest;

message Base {
   // the following represent sub-types; at most 1 should have a value
   optional Child Child = 100;
   optional int32 BaseProperty = 1 [default = 0];
}
message Child {
   optional float ChildProperty = 1 [default = 0];
}
message Request {
   optional Base Aggregate = 1;
}

(Обратите внимание на optional Child Child = 100; перед BaseProperty)


person Pedro Villa Verde    schedule 24.09.2015    source источник
comment
Какой класс вы ожидаете от сериализации, когда Base является абстрактным, и как вы ожидаете, что он это узнает?   -  person Jon Skeet    schedule 24.09.2015
comment
Как он работает, когда он сериализуется через protobuf-net, я думал, что это будет при сериализации через другие библиотеки. Но, похоже, он знает, какой класс использовать, поскольку он работает, если он не отмечен как абстрактный.   -  person Pedro Villa Verde    schedule 24.09.2015
comment
Как выглядит ваш код protobuf.js?   -  person psv    schedule 28.09.2015
comment
@petersv Обновил вопрос, включив в него код сериализации.   -  person Pedro Villa Verde    schedule 28.09.2015
comment
@PedroVillaVerde Я не уверен, но разве вы не должны кодировать ваше сообщение перед сериализацией? См. этот пример. var buffer = car.encode(); выполняется перед сериализацией. Также я бы попытался создать вам сообщение msg. из прото-файла на обоих концах, так что вы на 100% уверены, что контракт одинаковый.   -  person psv    schedule 29.09.2015
comment
@petersv Только что протестировал его, и использование метода encode() дает тот же выходной массив байтов, поэтому проблема сохраняется. Что касается proto-файла, он был создан с использованием метода GetProto() protobuf-net, поэтому он довольно точен. Стоит отметить, что десериализация работает отлично, если класс Base не помечен как абстрактный, поэтому, вероятно, это не проблема сериализации.   -  person Pedro Villa Verde    schedule 29.09.2015
comment
@PedroVillaVerde в порядке, но одно можно сказать наверняка: сериализованный вывод должен быть идентичным, иначе возникнет проблема. Я бы посоветовал не отмечать это как абстрактное и посмотреть, можно ли вместо этого переделать некоторые элементы окружения.   -  person psv    schedule 30.09.2015


Ответы (1)


Короче говоря, поддержка полиморфизма protobuf-net предполагает, что подтип будет первым в сообщении (или, более конкретно: для любого объекта тип будет исправлен до того, как будут предоставлены данные). . В выводе js данные поля для BaseProperty идут первыми - возможно, вполне разумно. Но поскольку нет всеобъемлющего определения протокола того, как должно вести себя наследование, реализация protobuf-net на самом деле предназначалась только для работы с самим собой. Что касается байтов, это фактически сводится к тому, где появляется маркер поля «162, 6» (и соответствующая длина / данные, «5, 13, 0, 0, 0, 63»).

Библиотека может потенциально быть переработана, чтобы разрешить любой порядок полей для полиморфизма, но: это потребует некоторых усилий. Я знаю, что обычно ожидается обработка полей в любом порядке, но, поскольку это уже выходит за рамки спецификации, я не заострял на этом внимание. Все остальные поля данных принимаются в любом порядке - так работает только полиморфизм.

В общем случае: поскольку полиморфизм не является частью спецификации, я настоятельно рекомендую избегать полиморфизма при работе между библиотеками.

Примечание: вы, вероятно, можете заставить это работать, убедившись, что поля полиморфизма ниже (численно), чем поля данных.

person Marc Gravell    schedule 30.09.2015
comment
Спасибо за объяснение. Проблема действительно заключалась в порядке полей, а не в числовом порядке. Protobuf.js использует порядок, в котором поля были помещены в определение сообщения .proto, поэтому мне просто нужно было переместить optional Child Child = 100; в начало сообщения, чтобы он сгенерировал такое же байтовое представление, что и protobuf-net. Я обновлю вопрос, чтобы добавить это примечание. Большое спасибо! - person Pedro Villa Verde; 30.09.2015
comment
@Pedro, это интересно - не в последнюю очередь потому, что это показывает, что не только я немного небрежно / непоследовательно относился к порядку полей; p - person Marc Gravell; 30.09.2015