extremeandy
extremeandy

Reputation: 535

How to serialize struct which implements IEnumerable using protobuf-net and surrogate

I'm using the protobuf-net package version 3.0.101.

The following code generates a runtime exception when executing typeModel.Serialize(ms, value). The exception is:

GenericArguments[0], 'UserQuery+Option`1[System.Int32]', on 'ProtoBuf.Serializers.RepeatedSerializer`2[TCollection,T] CreateEnumerable[TCollection,T]()' violates the constraint of type 'TCollection'.
void Main()
{
    var typeModel = RuntimeTypeModel.Create();
    typeModel.SetSurrogate<Option<int>, OptionSurrogate<int>>();
    //typeModel[typeof(Option<int>)].IgnoreListHandling = true; // This doesn't help.
    
    var value = new Option<int>(true, 5);
    
    var ms = new MemoryStream();
    typeModel.Serialize(ms, value);
}

struct Option<T> : IEnumerable<T>
{
    public Option(bool hasValue, T value)
    {
        HasValue = hasValue;
        Value = value;
    }

    public readonly bool HasValue;

    public readonly T Value;

    public IEnumerator<T> GetEnumerator()
    {
        if (HasValue) {
            yield return Value;
        }
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

[ProtoContract]
struct OptionSurrogate<T>
{
    public OptionSurrogate(Option<T> thing) {
        HasValue = thing.HasValue;
        Value = thing.Value;
    }
    
    [ProtoMember(1)]
    public readonly bool HasValue;
    
    [ProtoMember(2)]
    public readonly T Value;

    public static implicit operator Option<T>(OptionSurrogate<T> surrogate) => new Option<T>(surrogate.HasValue, surrogate.Value);
    public static implicit operator OptionSurrogate<T>(Option<T> value) => new OptionSurrogate<T>(value);
}

There are two things that will fix this:

  1. Changing struct Option<T> to class Option<T>
  2. Removing IEnumerable<T> from struct Option<T>

However, neither of these are possible because the struct I want to serialize is in a 3rd party library.

Is this a bug in protobuf-net or is there a workaround?

Upvotes: 1

Views: 265

Answers (1)

DekuDesu
DekuDesu

Reputation: 2292

One possible workaround that might work is wrapping the object in your own class, where you will be able to control which properties get serialized and how.

So I pulled the protobuf repo and stepped through their code for surrogate handling for IEnumerable<T>(the Option<T>) and their implementation makes heavy assumptions that any IEnumerable<T> is an ICollection by default leading to this error. Despite the fact that you explicitly give the runtime the information that Option<T> is not serializable using SetSurrogate, their implementation does not check surrogates until after the item it packed into a more manageable Enumerable type. The problem here is that they're trying to pack:

public IEnumerator<T> GetEnumerator()
{
    if (HasValue) {
        yield return Value;
    }
}

and that's not possible (in-fact they don't even 'see'/'look' for this kind of implementation of IEnumerable.

All that being said, I recommend just explictly casting the objects before and after deserialization, I provided an example that I got working below.

static void Main()
{
    var typeModel = RuntimeTypeModel.Create();

    var value = new Option<int>(true, 5);

    using var writer = new FileInfo("test.txt").OpenWrite();

    typeModel.Serialize(writer , (OptionSurrogate<int>)value, typeModel);

    writer .Dispose();

    using var reader = new FileInfo("test.txt").OpenRead();

    Option<int> deserializedValue = (OptionSurrogate<int>)typeModel.Deserialize(reader, null, typeof(OptionSurrogate<int>));

}

Edit
Added simple implementation of ICollection<T> to possibly prevent constraint error

Edit
Added Working work-around this time.

Editors Note
I am not an expert at protobuf and my opinion of how and why this bug is happening is based on some very simple debugging and stepping through their github source. This should not be taken as a fact as I neither wrote protobuf or have worked with the source code of protobuf long enough to properly opine on the cause of this error.

Upvotes: 1

Related Questions