1

Considering these class definitions:

[ProtoContract, ProtoInclude(2, typeof(Class2))]
class Class1
{
    [ProtoMember(1)]
    public string Field1 { get; set; }
}

[ProtoContract]
class Class2 : Class1
{
    [ProtoMember(1)]
    public string Field2 { get; set; }
}

I am trying to achieve the following:

using (var ms = new MemoryStream())
{
    var c1 = new Class1 { Field1 = "hello" };
    Serializer.Serialize<Class1>(ms, c1);

    ms.Position = 0;

    var c2 = Serializer.Deserialize<Class2>(ms);
}

But I get the following exception: Unable to cast object of type 'ProtoBufTest.Class1' to type 'ProtoBufTest.Class2'

I don't really understand the issue; my understanding is that when deserializing, Protobuf should just consider the incoming stream as a collection of bytes, so why does it apparently deserialize to a Class1 object first, and then try to fit it in a Class2?

ThomasWeiss
  • 1,292
  • 16
  • 30

2 Answers2

2

By adding [ProtoInclude(...)], you told protobuf-net to treat Class1 and Class2 in a way that allows inheritance to work. No matter whether you specify <Class1> or <Class2>, protobuf-net is going to start at the base-type and build upwards; essentially you model has become (in protobuf terms):

message Class1 {
   optional string Field1 = 1;
   // the following represent sub-types; at most 1 should have a value
   optional Class2 Class2 = 2;
}
message Class2 {
   optional string Field2 = 1;
}

If a .Class2 instance is present, it will deserialize as a Class2; otherwise it will deserialize as a Class1. This is intentionally so that if you serialize a Class1 you get back a Class1, and so that if you serialize a Class2 you get back a Class2.

If you want to consider the two types separately, don't add [ProtoInclude]. In fact, in that case you can even use Serializer.ChangeType to do a serialize/deserialize round-trip:

var c1 = new Class1 { Field1 = "hello" };
var c2 = Serializer.ChangeType<Class1, Class2>(c1);

Note: in that scenario, I would wonder why there was an inheritance relationship in the first place. From what you are doing, it feels like you actually just want:

[ProtoContract]
class Class1
{
    [ProtoMember(1)]
    public string Field1 { get; set; }
}

[ProtoContract]
class Class2
{
    [ProtoMember(1)]
    public string Field2 { get; set; }
}

(although I have no idea why)

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • Thanks for this thorough explanation. Actually I would not really require any ProtoMember in Class2, as my use-case is to deserialize Class1 and augment it with additional fields before sending it to a client (with the fields "flat" in the object for JSON serialization). I've tried Serializer.ChangeType but both Field1 and Field2 were null in the result. – ThomasWeiss Jul 23 '14 at 13:37
  • @ThomasWeiss worked fine here... would need to see an example of it not working – Marc Gravell Jul 23 '14 at 13:53
  • By using the class definitions from my post without the ProtoInclude, and calling c2 = Serializer.ChangeType(c1), I get c2.Field1 = null and c2.Field2 = "hello". – ThomasWeiss Jul 24 '14 at 02:13
1

Because you are sending instance of Class1, not Class2. If you call method with instance of Class1, you can not magicaly transform it into Class2.

You can create new instance of Class2 and fill by members from Class1 instance. But it looks like bad design.

TcKs
  • 25,849
  • 11
  • 66
  • 104
  • When calling Serializer.Deserialize(ms), the only thing the Serializer receives is a stream that doesn't make any reference to Class1 (type names are not serialized AFAIK), so why does 'Class1' pop up here? – ThomasWeiss Jul 23 '14 at 09:30
  • According to http://stackoverflow.com/questions/947666/what-does-the-protoinclude-attribute-mean-in-protobuf-net the meta-info for types is included when you use ProtoInclude attribute. – TcKs Jul 23 '14 at 11:21
  • What I understand from this link is that ProtoInclude inserts a "placeholder" for the content of the sub-class, but the actual type information is not embedded. So it seems that when deserializing the stream from Class1, Protobuf finds this placeholder empty so it guesses that it should be deserialized as Class1, not Class2. I was expecting to get Class2 with its particular fields left null/default... – ThomasWeiss Jul 23 '14 at 12:00