Как использовать protobuf-net с неизменяемыми типами значений?

Предположим, что у меня есть неизменяемый тип значения, например:

[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/) serializer, я получаю эту ошибку:

Невозможно применить изменения к свойству ConsoleApplication.Program + MyValueType.X

Если я применяю сеттеры к свойствам, отмеченным атрибутом DataMember, он будет работать, но тогда он нарушит неизменность этого типа значений и что нам не желательно.

Кто-нибудь знает, что мне нужно сделать, чтобы заставить его работать? Я заметил, что существует перегрузка метода ProtoBu.Serializer.Serialize, который принимает в SerializationInfo и StreamingContext, но я не использую их вне контекста реализации интерфейса ISerializable, поэтому любые примеры кода о том, как их использовать в этот контекст будет высоко оценен!

Спасибо,

EDIT: поэтому я выкопал какую-то старую статью MSDN и лучше понял, где и как используются SerializationInfo и StreamingContext, но когда я пытался это сделать:

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

получается, что метод Serialize<T> допускает только ссылочные типы, есть ли для этого конкретная причина? Кажется немного странным, учитывая, что я могу сериализовать типы значений, открытые через ссылочный тип.

Ответ 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)] в полях)

который должен работать, в зависимости от уровня доверия. То, что я еще не сделал, - это обобщение кода конструктора на атрибуты сценария. Это не сложно, но я хотел сначала подталкивать основной случай, а затем развивать его.

Я добавил следующие два образца/тесты с полным кодом здесь:

    [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).

Ответ 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); как var dataObject = (IPersistableEvent)_binarySerializationService.FromBytes(data, eventType);

Мой класс сообщений FakeSimpleEvent действительно имеет конструктор без параметров, потому что я хочу, чтобы он был неизменным.

Я использую protobuf-net 2.4.0 и могу подтвердить, что он поддерживает сложные конструкторы и неизменяемые классы сообщений. Просто используйте следующий декоратор

[ProtoContract(SkipConstructor = true)]

Если true, конструктор для типа обходит во время десериализации, что означает, что любые инициализаторы полей или другой код инициализации пропускаются.

ОБНОВЛЕНИЕ 1: (20 июня 2019 г.) Мне не нравится загрязнять мои классы атрибутами, которые принадлежат протобуферу, потому что модель предметной области должна быть технологически независимой (конечно, кроме типов фреймворка 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;
    }
}

а затем настройте protobuf со следующим для этого класса.

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

Это было бы эквивалентно моему предыдущему ответу.