I am using Protobuf to communicate between my web client and server (C #) using WebSocket. On the client, de-serialization is done through Protobuf.js , and on the server, the protobuf-net protocol.
The problem is that when using aggregation with abstract classes, protobuf-net cannot deserialize the data sent by Protobuf.js.
This is the stack trace:
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()
This is the contract:
[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; } }
And this is the code for reproducing the error. When serialization works, I only provide the result as a byte array. If this helps, I can provide the steps that I have taken to get serialized values.
// 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. }
If I add SkipConstructor = true to the definition of the base class, the error will change to "MemberAccessException: cannot create an abstract class" with the following stack trace. If I remove the abstract from the base class definition, it works as expected.
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()
I'm not sure why the binary representation generated via protobuf-net and Protobuf.js is different, but both of them seem valid as they work if the base class is not abstract.
Any ideas on why this is happening, or a way to do without removing the abstract from the base class?
Thanks in advance!
UPDATE
This is the code I used to generate byte serialization via 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>
DECISION
As Mark explained, protobuf-net does not support polymorphism when the field order is inverted. As a workaround specific to Protobuf.js, you can rearrange the fields in the .proto file to serialize it in the correct order.
In my case, changing the .proto file to the following solution to the problem:
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; }
(Note optional Child Child = 100; BaseProperty optional Child Child = 100; before BaseProperty )