-1

First let me show a simple test case of the problem and how to trigger it. Here is the class:

    class ProtoRecurseTest
    {
        private int nextPayload = 1;
        public int Payload { get; private set; } = 0;
        public ProtoRecurseTest Back { get; private set; } = null;
        public List<ProtoRecurseTest> Children { get; set; } = new List<ProtoRecurseTest>();

        public ProtoRecurseTest Add()
        {
            ProtoRecurseTest result = new ProtoRecurseTest(this, nextPayload++);
            Children.Add(result);
            return result;
        }

        public ProtoRecurseTest()
        {
        }

        private ProtoRecurseTest(ProtoRecurseTest parent, int payload)
        {
            Back = parent;
            this.Payload = payload;
            nextPayload = payload + 1;
        }

        private static void ToStringHelper(ProtoRecurseTest proto, StringBuilder sb)
        {
            sb.Append(proto.Payload + " -> ");

            // another little hassle of protobuf due to empty list -> null deserialization
            if (proto.Children != null)
            {
                foreach (var child in proto.Children)
                    ToStringHelper(child, sb);
            }
        }

        public override string ToString()
        {
            StringBuilder sb = new StringBuilder();
            ToStringHelper(this, sb);
            return sb.ToString();
        }
    }

There are no protobuf annotations as that is being taken care of programmatically. I have manually ensured that the class along with Back and Children are all added to the schema with .AsReferenceDefault = true.

The recursion triggering occurs when an instance is populated to a depth of at least 8 bizarrely enough. 7 is fine. Population code is straight forward:

        ProtoRecurseTest recurseTest = new ProtoRecurseTest();
        ProtoRecurseTest recurseItem = recurseTest;
        for (int i = 0; i < 8; i++)
            recurseItem = recurseItem.Add();

And then serialize recurseTest. This behavior only occurs when the children are in a list but in a list it occurs even with only 1 child per list as you end up with from the sample populating code. When I replaced the children with a single reference everything serialized fine.

This is using ProtoBuf.NET 2.1.0.0.

1 Answers1

0

Here is a solution, but one I'm not particularly fond of. It was based on @marc-gravell 's answer to this question.

    class ProtoRecurseTest : ISerializationManagementCallbacks
    {
        private int nextPayload = 1;
        public int Payload { get; private set; } = 0;
        public ProtoRecurseTest Back { get; private set; } = null;
        public List<ProtoRecurseTest> Children { get; set; } = new List<ProtoRecurseTest>();

        public ProtoRecurseTest Add()
        {
            ProtoRecurseTest result = new ProtoRecurseTest(this, nextPayload++);
            Children.Add(result);
            return result;
        }

        public ProtoRecurseTest()
        {
        }

        private ProtoRecurseTest(ProtoRecurseTest parent, int payload)
        {
            Back = parent;
            this.Payload = payload;
            nextPayload = payload + 1;
        }

        private static void ToStringHelper(ProtoRecurseTest proto, StringBuilder sb)
        {
            sb.Append(proto.Payload + " -> ");

            // another little hassle of protobuf due to empty list -> null deserialization
            if (proto.Children != null)
            {
                foreach (var child in proto.Children)
                    ToStringHelper(child, sb);
            }
        }

        public override string ToString()
        {
            StringBuilder sb = new StringBuilder();
            ToStringHelper(this, sb);
            return sb.ToString();
        }

        static void PreSerializationHelper(ProtoRecurseTest instance)
        {
            instance.Back = null;
            foreach (var child in instance.Children)
                PreSerializationHelper(child);
        }

        public void BeforeSerialization()
        {
            PreSerializationHelper(this);
        }

        static void PostDeserializationHelper(ProtoRecurseTest instance, ProtoRecurseTest parent)
        {
            if (instance.Children == null)
                instance.Children = new List<ProtoRecurseTest>();
            instance.Back = parent;
            foreach (var child in instance.Children)
                PostDeserializationHelper(child, instance);
        }

        public void AfterDeserialization()
        {
            PostDeserializationHelper(this, null);
        }
    }

The calls to serialize/deserialize now simply check if the type can be casted to the ISerializationManagementCallbacks (which was just provide a BeforeSerialization and AfterDeserialization method) before doing their business and then calling the appropriate methods if so. And it works fine.

However, I'm not really fond of mixing ever more serialization issues into my codebase - that's actually why the schema is generated programmatically. Ideally I'd like to separate the serialization entirely but the empty list -> null issue (which not to say it's an "issue" per se but just an undesirale part of the protobuf standard) already makes that impossible, but this would be another even more esoteric issue to work around and I do think this one might indeed be an issue.

Community
  • 1
  • 1