Как использовать protobuf-net с неизменяемыми типами значений? - PullRequest
11 голосов
/ 19 августа 2011

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

[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

Если я применю сеттеры к свойствам, отмеченным атрибутом 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> допускает только ссылочные типы, есть ли конкретная причина для этого?Это кажется немного странным, учитывая, что я могу сериализовать типы значений, предоставляемые через ссылочный тип.

Ответы [ 2 ]

10 голосов
/ 21 августа 2011

Какую версию 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).

1 голос
/ 20 июня 2019

Выбранный ответ не работает для меня, так как ссылка не работает, и я не вижу код 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)]

Если 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;

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

PS: Не обращайте внимания на IPersistableEvent.Для примера это не имеет значения, просто интерфейс маркера, который я использую где-то еще

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...