1

I am building up a runtime model for protobuf-net in runtime using reflection without annotating the classes I need to serialize.

Some of the classes I need to serialize use inheritance and of course I want all the properties from the base class(es).

protobuf-net does not crawl the inheritance tree by default so you need to tell it about base classes. So I wrote a little piece of code to do this:

public class InheritanceTest
{
    public static string CreateProto()
    {
        var model = ProtoBuf.Meta.RuntimeTypeModel.Default;

        var type = typeof(SubClass);

        if (null != type.BaseType && type.BaseType != typeof(Object))
        {
            var hierarchy = new List<Type> { type };
            var baseType = type.BaseType;
            while (null != baseType)
            {
                if (baseType != typeof(Object))
                {
                    hierarchy.Add(baseType);
                }
                baseType = baseType.BaseType;
            }

            hierarchy.Reverse();
            var metaType = model.Add(hierarchy.First(), true);
            for (int i = 1; i < hierarchy.Count; i++)
            {
                model.Add(hierarchy[i], true);
                metaType = metaType.AddSubType(i, hierarchy[i]);
            }
        }
        else
        {
            model.Add(type, true);
        }

        var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance).OrderBy(p => p.Name);

        var tagNumber = 1;
        foreach (var propertyInfo in properties)
        {
            model[type].Add(tagNumber, propertyInfo.Name);
            tagNumber++;
        }

        var schema = model.GetSchema(type, ProtoSyntax.Proto3);
        return schema;
    }
}

public class BaseClass
{
    public string StringPropOnBaseClass { get; set; }
}

public class SubClass : BaseClass
{
    public string StringPropOnSubClass { get; set; }
}

That produces a .proto file like this:

syntax = "proto3";
package ProtoBufferSerializerTest;

message BaseClass {
   // the following represent sub-types; at most 1 should have a value
   optional SubClass SubClass = 1;
}
message SubClass {
   string StringPropOnBaseClass = 1;
   string StringPropOnSubClass = 2;
}

Why is the BaseClass included in the .proto file? There is no reason why this needs to bleed out into the public wire format.

Is there a way I can tell the runtime model not to include this in the .proto flie?

BR

Jay Pete
  • 4,123
  • 4
  • 35
  • 51

1 Answers1

1

because you told it to?

If we change that code to:

Console.WriteLine("Adding: " + hierarchy.First().Name);
var metaType = model.Add(hierarchy.First(), true);
for (int i = 1; i < hierarchy.Count; i++)
{
    Console.WriteLine("Adding: " + hierarchy[i].Name);
    model.Add(hierarchy[i], true);
    Console.WriteLine("Adding as sub type " + i + " to " + metaType.Type.Name);
    metaType = metaType.AddSubType(i, hierarchy[i]);
}

We get:

Adding: BaseClass
Adding: SubClass
Adding as sub type 1 to BaseClass

So you are explicitly adding BaseClass as a contract type, and by using AddSubType you are explicitly connecting them in the model. The protobuf format itself (the Google spec) does not handle inheritance, so anything protobuf-net does needs to work within that, so it models inheritance via optional sub-objects, starting at the root - because that's the only way that allows you to reliably deserialize from BaseClass and have it make sense. For a full description of what it does see this answer.

So: if you actually intended to support inheritance in your serialization, it is expected and normal to get two types in the .proto. If you didn't intend to support inheritance: *don't use AddSubType. You can just add the members you need from the base type *directly onto SubClass:

public class InheritanceTest
{
    static void Main()
    {
        Console.WriteLine(CreateProto());

        var obj = new SubClass {
            StringPropOnBaseClass = "abc",
            StringPropOnSubClass = "def" };
        var clone = Serializer.DeepClone(obj);
        Console.WriteLine(clone.StringPropOnBaseClass);
        Console.WriteLine(clone.StringPropOnSubClass);
    }
    public static string CreateProto()
    {
        var model = ProtoBuf.Meta.RuntimeTypeModel.Default;

        var metaType = model.Add(typeof(SubClass), false);
        metaType.AddField(1, "StringPropOnSubClass");
        metaType.AddField(2, "StringPropOnBaseClass");

        var schema =model.GetSchema(typeof(SubClass), ProtoSyntax.Proto3);
        return schema;
    }
}

which outputs:

syntax = "proto3";

message SubClass {
   string StringPropOnSubClass = 1;
   string StringPropOnBaseClass = 2;
}

and

abc
def

Incidentally, your approach for allocating numbers to the sub-types in the original code displays some misunderstanding about which numbers are valid or desirable. The field numbers don't need to be different between different levels in the hierarchy - so in a 5 level inheritance tree they could all use 1 for the sub-type number if they want. But each sub-type number does need to not conflict with field numbers on the same type. Again, the linked post goes into more detail on this.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • Thanks for the answer. I initially asked the runtime model to apply the default behavior when adding the type, but that meant I did not get any of the base class properties in my .proto file. The answer was to tell it not to apply the default behavior. Then I did not have to worry about the base class at all. Thanks a lot! – Jay Pete Jul 28 '17 at 09:12
  • @JayPete if your type is not decorated with attributes (protocontract, datacontract, etc), then the default behaviour doesn't exist *anyway* – Marc Gravell Jul 28 '17 at 10:58
  • Good to know! Thanks! – Jay Pete Jul 28 '17 at 11:13