6

Próbowałem dodać nową wartość wyliczeniową dla pewnej klasy z serią protobuf w nowej wersji aplikacji, a podczas testowania zauważyłem, że poprzednia wersja wygeneruje wyjątek, biorąc pod uwagę ten nowy format pliku :Protobuf-net enum zgodność wsteczna

 
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 

jest dość oczywiste, że to mówi mi, że nie ma wartości wyliczenia dla wartości int z 3, ale zawsze miałem pojęcia, że ​​Protocol Buffers defaulted to the zero-valued ("default") enum value (jeśli taki istnieje), w przypadku gdy rzeczywista wartość enum nie można przypisać do.

celu wyjaśnienia, to może być powielana stosując następujący przykład (jestem celowo robi krok deserializacjia do innej klasy naśladować starą aplikację próby załadowania nowego formatu):

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

i następujący kod zawiedzie:

// 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); 

Od V1 jest na otwartej przestrzeni, jest jakiś metadanych można używać podczas szeregowania w v2, aby uniknąć tego problemu? Mogę, oczywiście, wydostać się z tego problemu, przepisując v2, aby użyć oddzielnej właściwości i pozostawić wartości wyliczeniowe niezmodyfikowane, ale chciałbym, aby w miarę możliwości były one zgodne wstecz.

+0

Co powinno się zdarzyć na '' EnumV2.Three' v1roundtrip.Value' kiedy został wysłany? – Caramiriel

+1

@Caramiriel: Zgodnie z moim zrozumieniem (wyjaśnione w [tym wątku] (http://stackoverflow.com/q/10392952/69809)), powinien on zostać ustawiony na 'EnumV1.Default' zamiast rzucać wyjątek. Tego bym się spodziewał, jeśli chcę, aby format był kompatybilny wstecz. Na przykład wydaje się, że [ten użytkownik] (http://stackoverflow.com/a/13924171/69809) miał ten sam problem i naprawił go, dodając domyślną wartość zerową, bez dodatkowych atrybutów protobuf. – Groo

+2

ping @marcgravell – jgauffin

Odpowiedz

1

Ponieważ v1 jest na otwartej przestrzeni, jest jakiś metadanych można używać podczas szeregowania w v2, aby uniknąć tego problemu? Mogę, oczywiście, wydostać się z tego problemu, przepisując v2, aby użyć oddzielnej właściwości i pozostawić wartości wyliczeniowe niezmodyfikowane, ale chciałbym, aby w miarę możliwości były one zgodne wstecz.

czego doświadczamy jest protobuf netto bug opisany tutaj protobuf-net - issue #422: Invalid behaviour while deserializing unknown enum value.

Wygląda na to, że nie jest jeszcze poprawiony, zgodnie z tym protobuf-net faulty enum exception (issue 422) need a good workaround (i oczywiście Twój post).

Niestety, musisz albo naprawić kod źródłowy protobuf-net lub użyć obejścia, o którym mowa.

UPDATE: Sprawdziłem kodu w repozytorium GitHub i upewnieniu się, że problem nie jest jeszcze ustalona. Oto problematyczny kod wewnątrz EnumSerializer.cs (komentarzu ISSUE #422 jest moje):

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

Twój ClassV1 nie ma kompatybilności do przodu.

Wdrożyłbym umowę Proto w taki sposób, że serializuje/deserializuje łańcuch reprezentujący wartość wyliczeniową. W ten sposób możesz samodzielnie zarządzać awarią do wartości domyślnej. Właściwość Value nie byłaby serializowana/deserializowana.

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

Nadal nie rozwiązuje to problemu posiadania klasy produkcyjnej, która nie może być dalej rozwijana.

+0

Istnieje wiele sposobów na zapewnienie zgodności wstecz/w przód. Prostszym sposobem (dla wyliczenia), który działałby, byłoby de/serializacja wartości 'int'. Ale to nie jest to, co zrobiłem, ponieważ polegałem na protobuf, aby móc sobie z tym poradzić. Zaprojektowałem kontrakt V1 z przekonaniem, że * Bufory protokołów są domyślnie ustawione na zerową wartość wyliczeniową, na wypadek gdyby nie można było odwzorować rzeczywistej wartości wyliczeniowej na * i chciałbym zobaczyć 1) dlaczego to nie działa i 2) przynajmniej jak przechwycić i obsłużyć ten przypadek podczas deserializacji. Twoja odpowiedź nie wyjaśnia, dlaczego * 'ClassV1' nie ma kompatybilności z FW, ani co teraz zrobić. – Groo

+0

Brak zgodności w przód pochodzi z klasy V1, która nie obsługuje nowych wartości wyliczeniowych, które można uznać za typy wartości składające się ze stałych kompilacji. To jest jak implementacja instrukcji switch/case bez domyślnej gałęzi i pozwól jej obsługiwać nieobsługiwane przypadki. –

+0

Nie sądzę, że ustawienie domyślnej wartości jest obowiązkiem protobuf. Pozwoliłbym klasie ClassV1 zająć się tym samym przez zainicjowanie właściwości podczas instanciation, zamiast pozostawić to unitialized (pola boolowskie są również inicjowane z błędem niejawnie). proto-buf rzuci się w nieskończoność, co jest w porządku, ponieważ pokazuje, że istnieje problem kompatybilności. –

0

Możesz dodać atrybut DefaultValue do właściwości prong proto.

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

Aby wyjaśnić, w jaki sposób należy zainicjować właściwość dla domyślnego przypadku.

+0

Czy próbowałeś tego? Nie sądzę, że ma to coś wspólnego z wyjątkiem. – Groo

3

Dodanie [ProtoContract(EnumPassthru=true)] do twoich wyliczeń pozwoli protobuf-net na deserializację nieznanych wartości.

Niestety, nie ma sposobu, aby z mocą wsteczną naprawić swój v1. Będziesz musiał użyć innej właściwości.

+0

Ale czy nie ustawiłoby to na przekształcenie z postaci szeregowej na "3" (nawet jeśli nie zawierało dopasowanego wyliczenia) zamiast wartości domyślnej? Nadal oczekiwałem, że wartość domyślna zostanie użyta w przypadku, gdy rzeczywiste wyliczenie nie może zostać rozwiązane? – Groo

+0

Tak. Jeśli potrzebujesz wartości na starej wersji, aby była domyślna, a wartość w nowej wersji będzie jakąś nową wartością wyliczeniową (w tym przypadku 3), musisz przełączyć się do nowej właściwości za każdym razem, gdy dodasz nowa wartość dla twojego wyliczenia. – yaakov