Search code examples
c#protobuf-net

How can I serialize a struct as a primitive type?


I have an enum that is serialized and sent over the wire. Requirements have evolved and I need to support additional values that may be defined in other assemblies, but I can't break binary compatibility for the who-knows-what that is in the wild.

My initial idea was to replace this enum with a enum-like struct wrapping an integer and use a surrogate to serialize it as an integer:

public readonly struct SomeFlag
{
    private readonly int _value;

    public SomeFlag(int value) { _value = value; }

    public static implicit operator int(SomeFlag type)
    {
        return type._value;
    }

    public static implicit operator SomeFlag(int value)
    {
        return new SomeFlag(value);
    }
}

...

TypeModel
    .Add(typeof(SomeFlag), false)
    .SetSurrogate(typeof(int));

When I try to serialize SomeFlag, I get an error: Data of this type has inbuilt behaviour, and cannot be added to a model in this way: System.Int32

Aside from replacing the structs in my models and code with plain integers, is there any way to make a non-primitive type serialize as a primitive?


Solution

  • Yes, but it requires a custom serializer in v3+ of the library;

    using ProtoBuf;
    using ProtoBuf.Serializers;
    
    var obj = new HazSomeFlag { Value = new SomeFlag(42) };
    var ms = new MemoryStream();
    Serializer.Serialize(ms, obj);
    if (!ms.TryGetBuffer(out var buffer)) buffer = ms.ToArray();
    var hex = BitConverter.ToString(buffer.Array!, buffer.Offset, buffer.Count);
    Console.WriteLine(hex); // 08-2A === field 1, varint = 42
    ms.Position = 0;
    var clone = Serializer.Deserialize<HazSomeFlag>(ms);
    Console.WriteLine(clone.Value);
    var schema = Serializer.GetProto<HazSomeFlag>();
    Console.WriteLine(schema);
    
    [ProtoContract]
    public class HazSomeFlag
    {
        [ProtoMember(1)]
        public SomeFlag Value { get; set; }
    }
    [ProtoContract(Serializer = typeof(MySerializer), Name = "int32")]
    public readonly struct SomeFlag
    {
        public override string ToString() => $"SomeFlag: {_value}";
        private readonly int _value;
    
        public SomeFlag(int value) { _value = value; }
    
        public static implicit operator int(SomeFlag type)
        {
            return type._value;
        }
    
        public static implicit operator SomeFlag(int value)
        {
            return new SomeFlag(value);
        }
    
        class MySerializer : ISerializer<SomeFlag>
        {
            SerializerFeatures ISerializer<SomeFlag>.Features
                => SerializerFeatures.CategoryScalar | SerializerFeatures.WireTypeVarint;
    
            SomeFlag ISerializer<SomeFlag>.Read(ref ProtoReader.State state, SomeFlag value)
                => new SomeFlag(state.ReadInt32());
    
            void ISerializer<SomeFlag>.Write(ref ProtoWriter.State state, SomeFlag value)
                => state.WriteInt32(value._value);
        }
    }
    

    which works (as shown) compatibly with the schema:

    syntax = "proto3";
    
    message HazSomeFlag {
       int32 Value = 1;
    }