Iterate over unknown repeated protobuf field with FieldDescriptor

Viewed 1430

I am trying to write a generalized method to iterate through a protobuf message and basically create an object tree.

So far I have

IMessage message = new Message() as IMessage;
foreach (FieldDescriptor field in message.Descriptor.Fields.InFieldNumberOrder())
            {
                if (field.FieldType == Google.Protobuf.Reflection.FieldType.Message)
                {
                    if (field.IsRepeated)
                    {
                        // How to do this?
                    }
                    else
                    {
                        Children.Add(new MessageNode(field, (IMessage)field.Accessor.GetValue(message)));
                    }
                }
                else
                {
                    Children.Add(new FieldNode(field, message));
                }
           }

Works fine except for repeated fields (and probably the same problem will crop up for Map fields).

If I do field.Accessor.GetValue(message) for the repeated field, I get an object[RepeatedField<T>] with T being some nested message. However, I cannot iterate over something of type object. I tried casting object to RepeatedField<IMessage> but this just returns 0.

Is there any way to iterate over a repeated field without knowing the type inside? I would just need an IMessage pointer to the objects inside, from there I can work with the Descriptor in IMessage.

2 Answers

As you noted in your answer, as per the protobuf documentation field.Accessor.GetValue(protobufMessage) will return you an IList for repeated fields and an IDictionary for map fields.

Here is some code I wrote that iterates recursively through a protobuf message using the field descriptors. It may be a useful starting point for doing other operations instead of printing.

    public static void PrintProtobufMessage(IMessage protobufMessage)
    {
        if (protobufMessage == null)
        {
            return;
        }
        foreach (var field in protobufMessage.Descriptor.Fields.InFieldNumberOrder())
        {
            if (field.IsMap)
            {
                // for map fields, the field type is always a MapField, inside of which are contained a keyField and valueField
                //var keyField = field.MessageType.Fields.InFieldNumberOrder()[0];
                var valueField = field.MessageType.Fields.InFieldNumberOrder()[1];
                IDictionary mapFields = field.Accessor.GetValue(protobufMessage) as IDictionary;
                if (mapFields != null)
                {
                    foreach (DictionaryEntry mapField in mapFields)
                    {
                        Console.WriteLine("Iterating Map, Field {0} ({1}): Key {2}:", field.FieldNumber, field.Name, mapField.Key);
                        if (valueField.FieldType == Google.Protobuf.Reflection.FieldType.Message)
                        {
                            PrintProtobufMessage(mapField.Value as IMessage);
                        }
                        else
                        {
                            PrintProtobufField(valueField, mapField.Value);
                        }
                    }
                }
            }
            else if (field.IsRepeated)
            {
                IList repeatedFieldValues = field.Accessor.GetValue(protobufMessage) as IList;
                if (repeatedFieldValues != null)
                {
                    foreach (var repeatedFieldValue in repeatedFieldValues)
                    {
                        // for repeated fields, the field type represents each element in the list, handle them accordingly
                        if (field.FieldType == Google.Protobuf.Reflection.FieldType.Message)
                        {
                            PrintProtobufMessage(repeatedFieldValue as IMessage);
                        }
                        else
                        {
                            PrintProtobufField(field, repeatedFieldValue);
                        }
                    }
                }
            }
            else
            {
                if (field.FieldType == Google.Protobuf.Reflection.FieldType.Message)
                {
                    var fieldValue = field.Accessor.GetValue(protobufMessage);
                    PrintProtobufMessage(fieldValue as IMessage);
                }
                else
                {
                    PrintProtobufField(field, protobufMessage);
                }
            }
        }
    }

    public static void PrintProtobufField(FieldDescriptor field, IMessage protobufMessage)
    {                   
        var fieldValue = field.Accessor.GetValue(protobufMessage);
        PrintProtobufField(field, fieldValue);
    }

    public static void PrintProtobufField(FieldDescriptor field, object fieldValue)
    {
        if (fieldValue != null)
        {
            Console.WriteLine(
                "Field {0} ({1}): {2}",
                field.FieldNumber,
                field.Name,
                fieldValue
            );
        }
    }

I don't know how blind I have been in the last two days trying to solve this...

Right in the documentation for GetValue of the Accessor it says

For repeated values, this will be an IList implementation. For map values, this will be an IDictionary implementation.

So yeah, IList d = field.Accessor.GetValue(message) as IList; does the trick and provides an iterable collection of the repeated field.

Related