【问题标题】:How to use protobuf-net with immutable value types?如何使用具有不可变值类型的 protobuf-net?
【发布时间】:2011-10-30 13:19:33
【问题描述】:

假设我有一个像这样的不可变值类型:

[Serializable]
[DataContract]
public struct MyValueType : ISerializable
{
private readonly int _x;
private readonly int _z;

public MyValueType(int x, int z)
    : this()
{
    _x = x;
    _z = z;
}

// this constructor is used for deserialization
public MyValueType(SerializationInfo info, StreamingContext text)
    : this()
{
    _x = info.GetInt32("X");
    _z = info.GetInt32("Z");
}

[DataMember(Order = 1)]
public int X
{
    get { return _x; }
}

[DataMember(Order = 2)]
public int Z
{
    get { return _z; }
}

public static bool operator ==(MyValueType a, MyValueType b)
{
    return a.Equals(b);
}

public static bool operator !=(MyValueType a, MyValueType b)
{
    return !(a == b);
}

public override bool Equals(object other)
{
    if (!(other is MyValueType))
    {
        return false;
    }

    return Equals((MyValueType)other);
}

public bool Equals(MyValueType other)
{
    return X == other.X && Z == other.Z;
}

public override int GetHashCode()
{
    unchecked
    {
        return (X * 397) ^ Z;
    }
}

// this method is called during serialization
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
    info.AddValue("X", X);
    info.AddValue("Z", Z);
}

public override string ToString()
{
    return string.Format("[{0}, {1}]", X, Z);
}
}

它适用于 BinaryFormatter 或 DataContractSerializer,但当我尝试将它与 protobuf-net (http://code.google.com/p/protobuf-net/) 序列化程序一起使用时,我收到此错误:

无法将更改应用于属性 ConsoleApplication.Program+MyValueType.X

如果我将 setter 应用于标有 DataMember 属性的属性,它会起作用,但它会破坏此值类型的不变性,这对我们来说是不可取的。

有人知道我需要做什么才能让它工作吗?我注意到有一个 ProtoBu.Serializer.Serialize 方法的重载,它接受一个 SerializationInfo 和一个 StreamingContext 但我没有在实现 ISerializable 接口的上下文之外使用它们,所以任何关于如何使用它们的代码示例非常感谢这个上下文!

谢谢,

编辑:所以我翻出了一些旧的 MSDN 文章,对 SerializationInfo 和 StreamingContext 的使用位置和方式有了更好的理解,但是当我尝试这样做时:

var serializationInfo = new SerializationInfo(
    typeof(MyValueType), new FormatterConverter());
ProtoBuf.Serializer.Serialize(serializationInfo, valueType);

原来Serialize<T> 方法只允许引用类型,这有什么特别的原因吗?考虑到我能够序列化通过引用类型公开的值类型,这似乎有点奇怪。

【问题讨论】:

  • 抱歉耽搁了 - 忙碌的周末

标签: c# serialization protocol-buffers protobuf-net


【解决方案1】:

您使用的是哪个版本的 protobuf-net?如果您是最新的 v2 版本,它应该会自动处理这个问题。如果我还没有部署此代码,我将立即更新下载区域,但基本上如果您的类型是未经修饰的(没有属性),它将检测您正在使用的常见“元组”模式,并决定(来自构造函数)x(构造函数参数)/X(属性)是字段 1,z/Z 是字段 2。

另一种方法是标记字段:

[ProtoMember(1)]
private readonly int _x;

[ProtoMember(2)]
private readonly int _z;

(或者在字段上[DataMember(Order=n)]

这应该有效,具体取决于信任级别。我还没有将构造函数代码推广到属性场景。这并不难,但我想先推动基本案例,然后再发展它。

我添加了以下两个样本/测试with full code here

    [Test]
    public void RoundTripImmutableTypeAsTuple()
    {
        using(var ms = new MemoryStream())
        {
            var val = new MyValueTypeAsTuple(123, 456);
            Serializer.Serialize(ms, val);
            ms.Position = 0;
            var clone = Serializer.Deserialize<MyValueTypeAsTuple>(ms);
            Assert.AreEqual(123, clone.X);
            Assert.AreEqual(456, clone.Z);
        }
    }
    [Test]
    public void RoundTripImmutableTypeViaFields()
    {
        using (var ms = new MemoryStream())
        {
            var val = new MyValueTypeViaFields(123, 456);
            Serializer.Serialize(ms, val);
            ms.Position = 0;
            var clone = Serializer.Deserialize<MyValueTypeViaFields>(ms);
            Assert.AreEqual(123, clone.X);
            Assert.AreEqual(456, clone.Z);
        }
    }

还有:

原来Serialize方法只允许引用类型

是的,这是 v1 的设计限制,与拳击模型等有关;这不再适用于 v2。

另外,请注意,protobuf-net 自身不消耗ISerializable(尽管它可以用于实现 ISerializable)。

【讨论】:

  • 嗨,Marc,刚刚下载了您发布的新二进制文件,它工作正常,我还使用 RuntimeTypeModel 实例检查了序列化/反序列化,它们也工作。
  • 但是我很好奇,为什么 RuntiemTypeModel.Deserialize 方法在返回修改后的对象时需要一个实例来修改。在不可变值类型的情况下,这意味着您最终会执行以下操作: var clone = (MyValueType)FormatterServices.GetUninitializedObject(typeof(MyValueType)); clone = (MyValueType)runtimeTypeModel.Deserialize(memStream, clone, typeof(MyValueType));感觉不太对劲
  • @theburningmonk 在这种情况下你可以传入null;这里的重点是 API 旨在支持“合并”以将值应用于 现有 对象。如果(通常)您希望序列化程序创建对象,只需传递 null
  • @theburningmonk 确实,如果使用 protobuf-net 来实现 ISerializable,这非常重要
  • 链接失效。这个MyValueTypeViaFields 类不在答案中。我有同样的异常No parameterless constructor found 并使用带有不可变模型的 protobuf-2.4.0(确实没有无参数构造函数),这似乎不受支持
【解决方案2】:

所选答案对我不起作用,因为链接已损坏,我看不到 MyValueTypeViaFields 代码。

无论如何,我的班级都有同样的例外No parameterless constructor found

[ProtoContract]
public class FakeSimpleEvent
    : IPersistableEvent
{
    [ProtoMember(1)]
    public Guid AggregateId { get; }
    [ProtoMember(2)]
    public string Value { get; }
    public FakeSimpleEvent(Guid aggregateId, string value)
    {
        AggregateId = aggregateId;
        Value = value;
    }
}

使用以下代码反序列化时:

public class BinarySerializationService
    : IBinarySerializationService
{
    public byte[] ToBytes(object obj)
    {
        if (obj == null) throw new ArgumentNullException(nameof(obj));
        using (var memoryStream = new MemoryStream())
        {
            Serializer.Serialize(memoryStream, obj);
            var bytes = memoryStream.ToArray();
            return bytes;
        }
    }

    public TType FromBytes<TType>(byte[] bytes)
        where TType : class
    {
        if (bytes == null) throw new ArgumentNullException(nameof(bytes));
        var type = typeof(TType);
        var result = FromBytes(bytes, type);
        return (TType)result;
    }

    public object FromBytes(byte[] bytes, Type type)
    {
        if (bytes == null) throw new ArgumentNullException(nameof(bytes));
        int length = bytes.Length;
        using (var memoryStream = new MemoryStream())
        {
            memoryStream.Write(bytes, 0, length);
            memoryStream.Seek(0, SeekOrigin.Begin);
            var obj = Serializer.Deserialize(type, memoryStream);
            return obj;
        }
    }
}

被称为var dataObject = (IPersistableEvent)_binarySerializationService.FromBytes(data, eventType);

我的消息类FakeSimpleEvent 确实有无参数构造函数,因为我希望它不可变。

我正在使用protobuf-net 2.4.0,我可以确认它支持复杂的构造函数和不可变消息类。只需使用以下装饰器

[ProtoContract(SkipConstructor = true)]

如果为真,则该类型的构造函数在运行期间被绕过 反序列化,意味着任何字段初始化器或其他 初始化代码被跳过。

更新 1:(2019 年 6 月 20 日) 我不喜欢用属于 protobuffer 的属性污染我的类,因为域模型应该与技术无关(当然除了 dotnet 框架的类型)

因此,对于没有属性且没有无参数构造函数(即:不可变)的消息类使用 protobuf-net,您可以拥有以下内容:

public class FakeSimpleEvent
    : IPersistableEvent
{
    public Guid AggregateId { get; }
    public string Value { get; }
    public FakeSimpleEvent(Guid aggregateId, string value)
    {
        AggregateId = aggregateId;
        Value = value;
    }
}

然后为这个类配置以下内容。

var fakeSimpleEvent = RuntimeTypeModel.Default.Add(typeof(FakeSimpleEvent), false);
fakeSimpleEvent.Add(1, nameof(FakeSimpleEvent.AggregateId));
fakeSimpleEvent.Add(2, nameof(FakeSimpleEvent.Value));
fakeSimpleEvent.UseConstructor = false;

这相当于我之前的答案,但更简洁。

PS:不要介意IPersistableEvent。与示例无关,只是我在其他地方使用的标记界面

【讨论】:

  • 确实,protobuf-net 支持类似元组的类型(尽管在 2011 年我最初回答时可能还没有这样做);但我要提醒一句——如果有人认为FakeSimpleEvent 实际上需要int EventId {get;},并认为将构造函数更改为FakeSimpleEvent(int eventId, Guid aggregateId, string value) 非常重要……猜猜接下来会发生什么?我的建议:如果您依赖它,请在代码中添加注释,说明“不要更改参数顺序或在右侧以外的任何位置插入参数 - 序列化所需”
  • 这是一件我没有意识到的非常重要和危险的事情。谢谢@MarcGravell你知道当前的protobuf-net是否支持在proto文件模式或类似文件中添加消息配置?我正在探索如何使用 protobuf 进行事件溯源和版本控制以及进行 PoC。谢谢!
  • 您的意思是您拥有 .proto,但您想将其映射到您自己的代码优先类型?有趣的想法,尤其是现在我们有一个完全托管的 .proto 解析器......如果你想在 GitHub 上记录它,我很想听听一个具体的例子?
  • 类似的东西。我的想法是探索在发送方和接收方之间仅使用共享合约/模式(.proto 文件)的可能性。但是每个组件都有自己的实现。老实说,我仍然不知道事件源系统有什么可能性:)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-11-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-07-30
  • 2013-06-15
相关资源
最近更新 更多