13 votes

Comment utiliser protobuf-net avec des types de valeurs immuables ?

Supposons que j'ai un type de valeur immuable comme celui-ci :

[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);
}
}

Il fonctionne avec BinaryFormatter ou DataContractSerializer mais lorsque j'essaie de l'utiliser avec protobuf-net ( http://code.google.com/p/protobuf-net/ ) sérialiseur j'obtiens cette erreur :

Impossible d'appliquer les modifications à la propriété ConsoleApplication.Program+MyValueType.X

Si j'applique des setters aux propriétés marquées de l'attribut DataMember, cela fonctionnera mais cela rompt l'immuabilité de ce type de valeur et ce n'est pas souhaitable pour nous.

Quelqu'un sait-il ce que je dois faire pour que ça marche ? J'ai remarqué qu'il y a une surcharge de la méthode ProtoBu.Serializer.Serialize qui prend un SerializationInfo et un StreamingContext mais je ne les ai pas utilisés en dehors du contexte de l'implémentation de l'interface ISerializable, donc tout exemple de code sur la façon de les utiliser dans ce contexte sera très apprécié !

Merci,

EDIT : j'ai donc déterré un vieil article de MSDN et j'ai mieux compris où et comment SerializationInfo et StreamingContext sont utilisés, mais lorsque j'ai essayé de faire ceci :

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

il s'avère que le Serialize<T> ne permet que les types de référence, y a-t-il une raison particulière à cela ? Cela semble un peu étrange étant donné que je suis capable de sérialiser les types de valeur exposés par un type de référence.

10voto

Marc Gravell Points 482669

Quelle version de protobuf-net utilisez-vous ? Si vous utilisez la dernière version v2, cela devrait se faire automatiquement. Au cas où je n'aurais pas encore déployé ce code, je mettrai à jour les zones de téléchargement dans un moment, mais en gros, si votre type n'est pas orné (pas d'attributs), il détectera la patte commune "tuple" que vous utilisez, et décidera (à partir du constructeur) que x (paramètre du constructeur)/ X (propriété) est le champ 1, et z / Z est le champ 2.

Une autre approche consiste à marquer les champs :

[ProtoMember(1)]
private readonly int _x;

[ProtoMember(2)]
private readonly int _z;

(ou alternativement [DataMember(Order=n)] sur les champs)

ce qui devrait fonctionner, en fonction du niveau de confiance. Ce que je n'ont pas Il n'a pas encore été fait de généraliser le code du constructeur pour les scénarios attribués. Ce n'est pas difficile, mais je voulais d'abord pousser le cas de base, puis le faire évoluer.

J'ai ajouté les deux échantillons/tests suivants avec le code complet ici :

    [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);
        }
    }

Aussi :

il s'avère que la méthode Serialize ne permet que les types de référence

oui, il s'agissait d'une limitation de la conception de la v1 liée au modèle de boîte, etc.

Aussi, notez que protobuf-net n'a pas lui-même consommer ISerializable (bien qu'il puisse être utilisé pour mettre en œuvre ISerializable ).

2voto

reala valoro Points 148

La réponse sélectionnée n'a pas fonctionné pour moi, car le lien est cassé et je ne peux pas voir la réponse. MyValueTypeViaFields code.

En tout cas, j'ai eu la même exception No parameterless constructor found pour ma classe :

[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;
    }
}

lors de sa désérialisation avec le code suivant :

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;
        }
    }
}

être appelé comme var dataObject = (IPersistableEvent)_binarySerializationService.FromBytes(data, eventType);

Ma classe de messages FakeSimpleEvent a en effet un constructeur sans paramètre parce que je le veux immuable.

J'utilise protobuf-net 2.4.0 et je peux confirmer qu'il prend en charge les constructeurs complexes et les classes de messages immuables. Il suffit d'utiliser le décorateur suivant

[ProtoContract(SkipConstructor = true)]

Si c'est le cas, le constructeur du type est contourné lors de l'exécution de l'opération. désérialisation, ce qui signifie que tous les initialisateurs de champs ou autres code d'initialisation est ignoré.

UPDATE 1 : (20 juin 2019) Je n'aime pas polluer mes classes avec des attributs qui appartiennent à protobuffer parce que le modèle de domaine devrait être agnostique du point de vue technologique (autre que les types du framework dotnet bien sûr).

Ainsi, pour utiliser protobuf-net avec des classes de messages sans attributs et sans constructeur sans paramètre (c'est-à-dire immuable), vous pouvez avoir ce qui suit :

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

et ensuite configurer protobuf avec ce qui suit pour cette classe.

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

Ce serait l'équivalent de ma réponse précédente mais beaucoup plus propre.

PS : Ne faites pas attention à la IPersistableEvent . Ce n'est pas pertinent pour l'exemple, c'est juste une interface de marquage que j'utilise ailleurs.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X