6

Stavo cercando di aggiungere un nuovo valore enum per una certa classe protobuf serializzata in una nuova versione dell'app e, durante i test, ho notato che la versione precedente genererà un'eccezione, dato questo nuovo formato di file :Protobuf-net enum retrocompatibilità

 
An unhandled exception of type 'ProtoBuf.ProtoException' occurred in protobuf-net.dll 
Additional information: No {enum-type-name} enum is mapped to the wire-value 3 

E 'abbastanza ovvio che mi sta dicendo che non v'è alcun valore enum per il valore int di 3, ma ho sempre avuto l'idea che Protocol Buffers defaulted to the zero-valued ("default") enum value (se esiste), nel caso in cui un valore di enumerazione reale non può essere mappato a

Per chiarire, questo può essere riprodotto utilizzando il seguente esempio (sto volutamente facendo il passo deserializzazione in una classe diversa per imitare vecchia applicazione che carica il nuovo formato):

// --- version 1 --- 

public enum EnumV1 
{ 
    Default = 0, 
    One = 1, 
    Two = 2 
} 

[ProtoContract] 
public class ClassV1 
{ 
    [ProtoMember(1)] 
    public EnumV1 Value { get; set; } 
} 



// --- version 2 --- 

public enum EnumV2 
{ 
    Default = 0, 
    One = 1, 
    Two = 2, 
    Three = 3 // <- newly added 
} 

[ProtoContract] 
public class ClassV2 
{ 
    [ProtoMember(1)] 
    public EnumV2 Value { get; set; } 
} 

E il seguente codice fallirà:

// serialize v2 using the new app 
var v2 = new ClassV2() { Value = EnumV2.Three }; 
var v2data = Serialize(v2); 

// try to deserialize this inside the old app to v1 
var v1roundtrip = Deserialize<ClassV1>(v2data); 

Dal V1 è alla luce del sole, c'è qualche metadati posso usare durante la serializzazione in v2 per evitare questo problema? Posso, naturalmente, tirarmi fuori da questo problema riscrivendo la v2 per usare una proprietà separata e lasciare i valori enum non modificati, ma mi piacerebbe rendere l'enumerazione compatibile all'indietro se possibile.

+0

Cosa dovrebbe accadere a '' v1roundtrip.Value' quando EnumV2.Three' è stato inviato? – Caramiriel

+1

@ Caramiriel: Secondo la mia comprensione (spiegato in [questa discussione] (http://stackoverflow.com/q/10392952/69809), avrebbe dovuto essere impostato su "EnumV1.Default" invece di generare un'eccezione. Questo è quello che mi aspetterei se volessi assicurarmi che il formato sia retrocompatibile. Ad esempio, [questo utente] (http://stackoverflow.com/a/13924171/69809) sembra aver avuto lo stesso problema e lo ha corretto aggiungendo un valore enum predefinito (zero), senza ulteriori attributi protobuf necessari. – Groo

+2

ping @marcgravell – jgauffin

risposta

1

Dato che v1 è in chiaro, sono disponibili alcuni metadati che posso utilizzare durante la serializzazione in v2 per evitare questo problema? Posso, naturalmente, tirarmi fuori da questo problema riscrivendo la v2 per usare una proprietà separata e lasciare i valori enum non modificati, ma mi piacerebbe rendere l'enumerazione compatibile all'indietro se possibile.

Quello che stiamo vivendo è una bug protobuf-net qui descritto protobuf-net - issue #422: Invalid behaviour while deserializing unknown enum value.

Sembra che non è ancora stato risolto in base a questo protobuf-net faulty enum exception (issue 422) need a good workaround (e ovviamente il tuo post).

Sfortunatamente è necessario correggere il codice sorgente protobuf-net o utilizzare le soluzioni menzionate.

UPDATE: Ho controllato il codice nel repository GitHub e confermando che il problema non è ancora risolto. Ecco il codice problematico all'interno del EnumSerializer.cs (il commento ISSUE #422 è mio):

public object Read(object value, ProtoReader source) 
{ 
    Helpers.DebugAssert(value == null); // since replaces 
    int wireValue = source.ReadInt32(); 
    if(map == null) { 
     return WireToEnum(wireValue); 
    } 
    for(int i = 0 ; i < map.Length ; i++) { 
     if(map[i].WireValue == wireValue) { 
      return map[i].TypedValue; 
     } 
    } 
    // ISSUE #422 
    source.ThrowEnumException(ExpectedType, wireValue); 
    return null; // to make compiler happy 
} 
0

Il tuo ClassV1 non ha compatibilità in avanti.

Avrei implementato il contratto Proto in modo tale che serializzi/deserializzi la rappresentazione stringa del valore enum. In questo modo puoi gestire da solo il fallback sul valore predefinito. La proprietà Value non sarebbe serializzata/deserializzata.

public enum EnumV1 
{ 
    Default = 0, 
    One = 1, 
    Two = 2 
} 

public enum EnumV2 
{ 
    Default = 0, 
    One = 1, 
    Two = 2, 
    Three = 3 // <- newly added 
} 

[ProtoContract] 
public class ClassV1 
{ 
    [ProtoMember(1)] 
    public string ValueAsString 
    { 
     get { return Value.ToString(); } 
     set 
     { 
      try 
      { 
       Value = (EnumV1) Enum.Parse(typeof (EnumV1), value); 
      } 
      catch (Exception) 
      { 
       Value = EnumV1.Default; 
      } 
     } 
    } 

    public EnumV1 Value { get; set; } 
} 

[ProtoContract] 
public class ClassV2 
{ 
    [ProtoMember(1)] 
    public string ValueAsString 
    { 
     get { return Value.ToString(); } 
     set 
     { 
      try 
      { 
       Value = (EnumV2)Enum.Parse(typeof(EnumV2), value); 
      } 
      catch (Exception) 
      { 
       Value = EnumV2.Default; 
      } 
     } 
    } 

    public EnumV2 Value { get; set; } 
} 

Ancora non risolve il problema di avere una classe non-forward idoneo in produzione.

+0

Esistono molti modi per garantire la compatibilità con le versioni precedenti e successive. Un modo più semplice (per un enum) che funzionerebbe sarebbe quello di de/serializzare il valore 'int'. Ma questo non è quello che ho fatto, perché mi sono basato su protobuf per essere in grado di gestirlo. Ho progettato il contratto v1 ritenendo che * I protocolli Buffer fossero in default sul valore enum con valore zero, nel caso in cui un valore enum effettivo non potesse essere mappato a *, e mi piacerebbe vedere 1) perché questo non funziona e 2) almeno come intercettare e gestire questo caso mentre deserializza. La tua risposta non spiega * perché * 'ClassV1' manca della compatibilità fw, né cosa fare ora. – Groo

+0

La mancanza di compatibilità diretta proviene da ClassV1 che non supporta nuovi valori enum, che possono essere considerati come tipi di valore costituiti da costanti tempo-compilazione. È come implementare un'istruzione switch/case senza il ramo predefinito e lasciare che gestisca casi non supportati. –

+0

Non penso che l'impostazione del valore predefinito sia una responsabilità di protobuf. Vorrei che la classe ClassV1 si occupasse di questa stessa inizializzando la proprietà durante l'instanciazione, invece di lasciarla unitializzata (i campi booleani sono anche inizializzati con false implicitamente). proto-buf getterà a vuoto, il che va bene perché mostra che c'è un problema di compatibilità. –

0

È possibile aggiungere l'attributo DefaultValue alla proprietà del membro proto.

[ProtoContract] 
public class ClassV1 
{ 
    [ProtoMember(1), DefaultValue(EnumV1.Default)] 
    public EnumV1 Value { get; set; } 
} 

Per rendere chiaro come la proprietà deve essere inizializzato per il caso di default.

+0

Hai provato questo? Non penso che questo abbia nulla a che fare con l'eccezione. – Groo

3

L'aggiunta di [ProtoContract(EnumPassthru=true)] all'enumerazione consentirà a protobuf-net di deserializzare valori sconosciuti.

Sfortunatamente, non c'è modo di correggere retroattivamente la tua v1. Dovrai usare una proprietà diversa.

+0

Ma non impostare il valore deserializzato su '3' (anche se non ha un enum corrispondente), invece del valore predefinito? Mi aspettavo ancora il valore predefinito da utilizzare nel caso in cui l'enum effettivo non potesse essere risolto? – Groo

+0

Sì. Se è necessario che il valore della versione precedente sia il valore predefinito e il valore nella nuova versione sia un nuovo valore enum (in questo caso 3), è necessario passare a una nuova proprietà ogni volta che si aggiunge un nuovo valore per il tuo enum. – yaakov