How to use protobuf-net with immutable value types?
Which version of protobuf-net are you using? If you are the latest v2 build, it should cope with this automatically. In case I haven't deployed this code yet, I'll update the download areas in a moment, but essentially if your type is unadorned (no attributes), it will detect the common "tuple" patten you are using, and decide (from the constructor) that x
(constructor parameter)/X
(property) is field 1, and z
/Z
is field 2.
Another approach is to mark the fields:
[ProtoMember(1)]
private readonly int _x;
[ProtoMember(2)]
private readonly int _z;
(or alternatively [DataMember(Order=n)]
on the fields)
which should work, depending on the trust level. What I haven't done yet is generalise the constructor code to attributed scenarios. That isn't hard, but I wanted to push the basic case first, then evolve it.
I've added the following two samples/tests 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);
}
}
Also:
it turns out that the Serialize method only allows reference types
yes, that was a design limitation of v1 that related to the boxing model etc; this no longer applies with v2.
Also, note that protobuf-net doesn't itself consume ISerializable
(although it can be used to implement ISerializable
).
The selected answer didn't work for me since the link is broken and I cannot see the MyValueTypeViaFields
code.
In any case I have had the same exception No parameterless constructor found
for my class:
[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;
}
}
when deserializing it with the following code:
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;
}
}
}
being called like var dataObject = (IPersistableEvent)_binarySerializationService.FromBytes(data, eventType);
My message class FakeSimpleEvent
has indeed parameterless constructor because I want it immutable.
I am using protobuf-net 2.4.0
and I can confirm that it supports complex constructors and immutable message classes. Simply use the following decorator
[ProtoContract(SkipConstructor = true)]
If true, the constructor for the type is bypassed during deserialization, meaning any field initializers or other initialization code is skipped.
UPDATE 1: (20 June 2019) I don't like polluting my classes with attributes that belong to protobuffer because the domain model should be technology-agnostic (other than dotnet framework's types of course)
So for using protobuf-net with message classes without attributes and without parameterless constructor (i.e: immutable) you can have the following:
public class FakeSimpleEvent
: IPersistableEvent
{
public Guid AggregateId { get; }
public string Value { get; }
public FakeSimpleEvent(Guid aggregateId, string value)
{
AggregateId = aggregateId;
Value = value;
}
}
and then configure protobuf with the following for this class.
var fakeSimpleEvent = RuntimeTypeModel.Default.Add(typeof(FakeSimpleEvent), false);
fakeSimpleEvent.Add(1, nameof(FakeSimpleEvent.AggregateId));
fakeSimpleEvent.Add(2, nameof(FakeSimpleEvent.Value));
fakeSimpleEvent.UseConstructor = false;
This would be the equivalent to my previous answer but much cleaner.
PS: Don't mind the IPersistableEvent
. It's irrelevant for the example, just a marker interface I use somewhere else