问题
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