具有代理和继承层次结构的 protobuf-net v3

protobuf-net v3 with surrogate and inheritance hierarchies

关于使用的现有类型层次结构,我正在努力从 protobuf-net v2.4.6 迁移到 v3.0.100(或任何 3.0.x)作为 ProtoContracts 其中一个子类型需要代理,因为它的 属性 类型之一是 object.

使用之前的配置,我在创建运行时模型时抛出以下异常: System.InvalidOperationException: 'Types with surrogates cannot be used in inheritance hierarchies'

因此,我的问题是如何使用 protobuf-net 3 正确处理这种情况。0.x?

这是我对该问题的(过度)简化重现:

class Program
{
    static void Main(string[] args)
    {
        var model = RuntimeTypeModel.Create();
        _ = model[typeof(Base)]; // <-- InvalidOperationException thrown here

        Base value = new Complex();
        var copy = model.DeepClone(value);
    }
}

[ProtoContract]
[ProtoInclude(1, typeof(Simple))]
[ProtoInclude(2, typeof(Complex))]
public abstract class Base
{
}

[ProtoContract]
public class Simple : Base
{
}

[ProtoContract(Surrogate = typeof(ComplexSurrogate))]
public class Complex : Base
{
}

[ProtoContract(Name = nameof(Complex))]
public class ComplexSurrogate
{
    [ProtoConverter]
    public static ComplexSurrogate Convert(Complex source) => new ComplexSurrogate();

    [ProtoConverter]
    public static Complex Convert(ComplexSurrogate source) => new Complex();
}

附带说明:当从源代码编译 protobuf-net 并抑制上述异常时,我能够为 Base class 这似乎是一种解决方法。

目前不支持该方案。在修改 v3 的代码时,发现了一些模棱两可的 outcomes/intents,需要深入研究并找出每种情况下的正确结果,设计如何实现、实施和测试。那个时间还没有找到,所以现在防止可能导致下游大问题的配置比只是耸耸肩并假设发生的任何事情都是正确的更安全。它在我要做的事情清单上,但是:归根结底,这是一个完全来自我业余时间的项目——它不是赞助的,也不是我有偿工作的一部分,所以:当它到达那里时,它就会到达那里.

我在 protobuf v3 中遇到了同样的错误,我用自定义序列化程序解决了这个问题。

我的基地class是

    [ProtoContract]
    [ProtoInclude(500, typeof(XXXRequest))]
    [ProtoInclude(501, typeof(XXXResponse))]
    // ...
    public class MessageBase
    {
        [ProtoMember(1)]
        long ID { get; internal set; }

        
        [ProtoMember(3)]
        int ExecutionMilliseconds { get; set; }
    }

它的等效原型是

message Message {
  int64 ID = 1;
  int32 ExecutionMilliseconds = 3;
  oneof body {
      PredictBonusRequest XXXRequest = 500;
      PredictBonusResponse XXXResponse = 501;
      // ...
  }
}

我想替换某些类型(例如 XXXResponse)以使用 contract-first class。这将使我们能够顺利地从 code-first 迁移到 contract-first。

为了 sub-types 应该被代理,我们创建自定义序列化程序如下。


using ProtoBuf;
using ProtoBuf.Serializers;

using UnderlyingMessage = GeneratedProto.Contract.Message;
using UnderlyingResponse = GeneratedProto.Contract.XXXResponse;

[DataContract]
[Serializable]
[ProtoContract(Serializer = typeof(XXXResponseSerializer))]
public class XXXResponse : MessageBase
{
    class XXXResponseSerializer : ISerializer<XXXResponse>
    {
        public SerializerFeatures Features => SerializerFeatures.CategoryMessage | SerializerFeatures.WireTypeString;

        public XXXResponse Read(ref ProtoReader.State state, XXXResponse value)
        {
            ISerializer<UnderlyingMessage> serializer = state.GetSerializer<UnderlyingMessage>();
            return serializer.Read(ref state, value);
        }

        public void Write(ref ProtoWriter.State state, XXXResponse value)
        {
            ISerializer<UnderlyingMessage> serializer = state.GetSerializer<UnderlyingMessage>();
            serializer.Write(ref state, value);
        }
    }

    private readonly UnderlyingResponse _resp;


    public XXXResponse() : this(new UnderlyingResponse() { })
    {
    }

    private XXXResponse(UnderlyingResponse msg)
    {
        _resp = msg;
    }

    public static implicit operator XXXResponse(UnderlyingMessage value)
    {
        if( value != null)
        {
            return new XXXResponse(value.XXXResponse)
            {
                ID = value.ID,
                ExecutionMilliseconds = value.ExecutionMilliseconds,
            };
        }
        return null;
    }

    public static implicit operator UnderlyingMessage(XXXResponse value)
    {
        if(value != null)
        {
            return new UnderlyingMessage()
            {
                ID = value.ID,
                ExecutionMilliseconds = value.ExecutionMilliseconds,
                XXXResponse = value._resp,
            };
        }
        return null;
    }


    public Transaction[] Transactions 
    { 
        get { return _resp.Transactions?.Select(t => (Transaction)t)?.ToArray(); } 
        set { _resp.Transactions = value?.Select(t => (BE.Data.Contract.Transaction)t)?.ToList(); } 
    }


    public long DomainID { get { return _resp.DomainID; } set { _resp.DomainID = value; } }


    public string UniversalID { get { return _resp.UniversalID; } set { _resp.UniversalID = value; } }


    public string ExtraData { get { return _resp.ExtraData; } set { _resp.ExtraData = value; } }


    // other proxied fields ...
}

关键是,当ISerializer.ReadISerializer.Write被触发时,wire-format来自entrie消息的范围,包括基础class的所有字段,当前 sub-type 位于编号由 ProtoInclude.

标识的字段中

在我们的例子中这是有效的。对于我们目前不想要代理的其他sub-types,它仍然像以前一样工作。