Search code examples
c#deserializationprotocol-buffersprotobuf-net

Protobuf-Net Cannot Deserialize Record Without Parameterless Constructor


Consider the following code example; I want to be able to serialize (which works fine) and deserialize (which doesn't work) the Account record using protobuf-net:

public abstract record State
{
    public abstract ISet<Identity> Identities { get; }

    public SerializedState Serialize()
    {
        using MemoryStream stream = new();
        Serializer.Serialize(stream, this);
        return new SerializedState(stream.ToArray(), GetType());
    }
}

public sealed record Account(Identity Owner, string Identifier, decimal Balance) : State
{
    public override ISet<Identity> Identities => new HashSet<Identity> {Owner};
}

The ProtoBuf contract configuration is effectively:

RuntimeTypeModel
    .Default
    .Add<Account>()
    .Add(nameof(Account.Owner))
    .Add(nameof(Account.Identifier))
    .Add(nameof(Account.Balance));

But I get the following exception:

ProtoBuf.ProtoException: No parameterless constructor found for Example.Account

Is there a way to configure deserialisation (without using attributes) to allow records without parameterless constructors?


Solution

  • The properties of a record are, by default, init-only, and as such can actually be set by reflection by . Thus your Account record can be deserialized by by bypassing the constructor as explained in this answer by Marc Gravell to Does protobuf-net support C# 9 positional record types?.

    Since you are initializing the contract for Account in runtime, modify your initialization code as to set MetaType.UseConstructor = false as follows:

    var accountMeta = RuntimeTypeModel
        .Default
        .Add<Account>()
        .Add(nameof(Account.Owner))
        .Add(nameof(Account.Identifier))
        .Add(nameof(Account.Balance));          
    accountMeta.UseConstructor = false;
    

    And now you can do:

    var account = new Account(identity, "Foo", 1.1m);       
    var state = account.Serialize();
    var account2 = (Account)Serializer.NonGeneric.Deserialize(state.Type, new MemoryStream(state.Data));
    

    Where I assume that SerializedState looks like:

    public class SerializedState
    {
        public SerializedState(byte [] data, Type type) => (Data, Type) = (data, type);
        
        public byte [] Data { get; set; }
        public System.Type Type { get; set; }
    }
    

    Demo fiddle here.