protobuf-net AddField ignores IgnoreListHandling

不羁的心 提交于 2021-02-10 07:36:15

问题


I have this data structure declaration:

[ProtoContract]
public class NotACollectionHolder
{
    public NotACollection some_objects;
}

[ProtoContract(IgnoreListHandling = true, ImplicitFields = ImplicitFields.AllPublic)]
public class NotACollection : IEnumerable<int>
{
    public int some_data;

    // something looks like a collection API
    public void Add(int a) { }
    public IEnumerator<int> GetEnumerator() { throw new NotImplementedException(); }
    IEnumerator IEnumerable.GetEnumerator() { throw new NotImplementedException(); }
}

I am manually registering field to MetaType by the following code:

MetaType meta = RuntimeTypeModel.Default.Add(typeof(NotACollectionHolder), false);
ValueMember member = meta.AddField(1, "some_objects", itemType: null, defaultType: null);
string proto = Serializer.GetProto<NotACollectionHolder>();

I mark NotACollection with IgnoreListHandling. I try to force AddField to ignore the fact that NotACollection looks like collection by providing itemType: null, defaultType: null.

Nevertheless, I have member.ItemType is not null, and member.DefaultType is not null either. And some_objects became a repeated field in generated proto:

message NotACollectionHolder {
    repeated int32 some_objects = 1;
}

I expect proto to look like this:

message NotACollection {
   optional int32 some_data = 1 [default = 0];
}
message NotACollectionHolder {
   optional NotACollection some_objects = 1;
}

How can I achieve that? What am I doing wrong? How can I force protobuf-net to treat this field like a non collection field?

Thanks in advance.


回答1:


I think this might be a bug or limitation with the RuntimeTypeModel API.

The method that determines whether a ValueMember is a collection is RuntimeTypeModel.ResolveListTypes(). It tries to infer the collection item type when the incoming itemType is null. When building a contract for NotACollectionHolder using only static attributes, for instance by doing:

var model = TypeModel.Create();
var schema = model.GetSchema(typeof(NotACollectionHolder));

Then ProtoBuf.Meta.MetaType.ApplyDefaultBehaviour(bool isEnum, ProtoMemberAttribute normalizedAttribute) is called to create an initialize the ValueMember. It has the following logic:

        // check for list types
        ResolveListTypes(model, effectiveType, ref itemType, ref defaultType);
        // but take it back if it is explicitly excluded
        if(itemType != null)
        { // looks like a list, but double check for IgnoreListHandling
            int idx = model.FindOrAddAuto(effectiveType, false, true, false);
            if(idx >= 0 && model[effectiveType].IgnoreListHandling)
            {
                itemType = null;
                defaultType = null;
            }
        }

Notice the explicit check for IgnoreListHandling? This correctly prevents some_objects from being serialized as a collection.

Conversely, if adds the ValueMember programmatically as follows:

var model = TypeModel.Create();
var meta = model.Add(typeof(NotACollectionHolder), false);
var member = meta.AddField(1, "some_objects", null, null);
var schema = model.GetSchema(typeof(NotACollectionHolder));

Then MetaType.AddField(int fieldNumber, string memberName, Type itemType, Type defaultType, object defaultValue) is called, which simply does:

        ResolveListTypes(model, miType, ref itemType, ref defaultType);

Notice there is no check for IgnoreListHandling? This is the cause of your problem.

Unfortunately, ValueType.itemType is read-only and MetaType[int fieldNumber] is get-only so there doesn't seem to be a simple API to call to fix this. You might consider reporting an issue.

The only workaround I could find is to introduce a surrogate for the NotACollectionHolder type like so:

[ProtoContract]
internal class NotACollectionHolderSurrogate
{
    [ProtoMember(1)]
    internal NotACollectionSurrogate some_objects;

    public static implicit operator NotACollectionHolder(NotACollectionHolderSurrogate input)
    {
        if (input == null)
            return null;
        return new NotACollectionHolder { some_objects = input.some_objects };
    }

    public static implicit operator NotACollectionHolderSurrogate(NotACollectionHolder input)
    {
        if (input == null)
            return null;
        return new NotACollectionHolderSurrogate { some_objects = input.some_objects };
    }
}

[ProtoContract]
internal class NotACollectionSurrogate
{
    [ProtoMember(1)]
    public int some_data;

    public static implicit operator NotACollection(NotACollectionSurrogate input)
    {
        if (input == null)
            return null;
        return new NotACollection { some_data = input.some_data };
    }

    public static implicit operator NotACollectionSurrogate(NotACollection input)
    {
        if (input == null)
            return null;
        return new NotACollectionSurrogate { some_data = input.some_data };
    }
}

And then do:

var model = TypeModel.Create();
model.Add(typeof(NotACollectionHolder), false).SetSurrogate(typeof(NotACollectionHolderSurrogate));

var schema = model.GetSchema(typeof(NotACollectionHolder));

The contract generated is as required:

message NotACollectionHolderSurrogate {
   optional NotACollectionSurrogate some_objects = 1;
}
message NotACollectionSurrogate {
   optional int32 some_data = 1 [default = 0];
}



回答2:


I found out that this is indeed a bug in protobuf-net. First way to fix it is to make changes to the source code of protobuf-net: in file MetaType.cs in function

private ValueMember AddField(int fieldNumber, string memberName, Type itemType, Type defaultType, object defaultValue)

replace line

ResolveListTypes(model, miType, ref itemType, ref defaultType);

with

if (model.FindWithoutAdd(miType)?.IgnoreListHandling == true)
{
    itemType = null;
    defaultType = null;
}
else
    ResolveListTypes(model, miType, ref itemType, ref defaultType);

Another way is to use reflection to set private fields itemType and defaultType of the object ValueMember to null after adding field:

ValueMember m = meta.AddField(++last_field_number, f.Name, f.ItemType, f.DefaultType);
m.GetType().GetField("itemType", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(m, null);
m.GetType().GetField("defaultType", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(m, null);

Hope this will help someone.



来源:https://stackoverflow.com/questions/42600663/protobuf-net-addfield-ignores-ignorelisthandling

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!