如何使用 protobuf-net 反序列化需要 ctor 调用的自定义只读结构?

How to deserialize a custom readonly struct requiring ctor invocation using protobuf-net?

我创建了一个自定义(只读)结构来封装小数。我到处都在使用该结构,包括面向 public 的 API 被各种编程语言使用,因此我想避免公开十进制数据类型。

这显示了结构的相关部分:

[ProtoContract(SkipConstructor = false, ImplicitFields=ImplicitFields.None)]
public readonly struct Amount
{
    [ProtoIgnore]
    public const decimal Scale = 100000m;

    [ProtoIgnore]
    public decimal Value { get; }

    [ProtoMember(1)]
    public long ScaledValue { get; }

    public Amount(decimal value)
    {
        Value = value;
        ScaledValue = checked((long)(value * Scale).Round(0));
    }

    public Amount(long scaledValue)
    {
        Value = scaledValue / Scale;
        ScaledValue = scaledValue;
    }        

    public static Amount CreateFrom(long scaledValue) => new Amount(scaledValue);
}

我遇到的问题是,尽管 ProtoContract 上的 SkipConstructor=false,但在反序列化期间未调用 ctor,导致仅 ScaledValue 属性 被正确初始化。

我不能使用 ProtoAfterDeserialization 方法来设置值 属性 因为该结构是只读的。

我已尝试为 protobuf-net 配置自定义工厂方法以在创建对象时使用,方法是:

var createFrom = typeof(Amount).GetMethod("CreateFrom", BindingFlags.Public | BindingFlags.Static, null, new[] { typeof(long) }, null);
RuntimeTypeModel.Default[typeof(Amount)].SetFactory(createFrom);

但这总是会导致“InvalidOperationException:由于对象的当前状态,操作无效。”。我已验证找到了 CreateFrom 方法(因此我传递了一个有效的 MethodInfo 对象)。

关于如何使这项工作有任何想法吗?

struct,尤其是 readonly struct,是我计划在 v3 中解决的问题,v3 有新序列化程序 API 的计划。在此期间,这不是它处理得很好的场景,但你最好的选择可能是 "surrogates" - 这意味着序列化程序在很大程度上忽略 Amount,使用 其他东西 取而代之的是对序列化更友好。这也意味着您可以从 Amount:

中删除任何序列化程序属性或 API
using ProtoBuf;
using ProtoBuf.Meta;

static class P
{
    static void Main()
    {
        // only need to do this once, *before*
        // serializing/deserialing anything
        RuntimeTypeModel.Default.Add(typeof(Amount), false)
            .SetSurrogate(typeof(AmountSurrogate));

        // test it works
        var obj = new Foo { Amount = new Amount(123.45M) };
        var clone = Serializer.DeepClone(obj);
        System.Console.WriteLine(clone.Amount.Value);
    }
}
[ProtoContract]
public class Foo
{
    [ProtoMember(1)]
    public Amount Amount { get; set; }
}

[ProtoContract]
struct AmountSurrogate
{ // a nice simple type for serialization
    [ProtoMember(1)]
    public long Value { get; set; }

    // operators define how to get between the two types
    public static implicit operator Amount(AmountSurrogate value)
        => Amount.CreateFrom(value.Value);
    public static implicit operator AmountSurrogate(Amount value)
        => new AmountSurrogate { Value = value.ScaledValue };
}