Protobuf как изменить иерархию наследования без потери обратной совместимости - PullRequest
1 голос
/ 30 мая 2019

У меня есть конкретный вариант использования для наследования с использованием protobuf-net, с которым я не знаю (если возможно), как обращаться с ним.

Допустим, у нас есть эти примеры классов (пометить это как Версия 1):

public class FooA
{
    public double A { get; set; }
    public double B { get; set; }
}

public class FooB : FooA
{
    public double C { get; set; }
}

public class FooC : FooB
{        
}

public class FooD : FooC
{
    public double D { get; set; }
}

public class FooA1 : FooA
{        
}

С приведенным ниже определением модели протобуфа:

        Model = RuntimeTypeModel.Create();

        Model.Add(typeof(FooA), false)
            .AddSubType(201, typeof(FooB))
            .AddSubType(202, typeof(FooA1))
            .Add(1, "A")
            .Add(2, "B");

        Model[typeof(FooB)]
            .AddSubType(201, typeof(FooC))
            .Add(1, "C");

        Model[typeof(FooC)]
            .AddSubType(201, typeof(FooD));                

        Model[typeof(FooD)]
            .Add(1, "D");

Я сериализирую их следующим образом

        FooA a = new FooA() {A = 10, B = 20};
        FooB b = new FooB() {A = 10, B = 20, C = 30};
        FooA1 b1 = new FooA1() {A = 100, B = 200};
        FooC c = new FooC() {A = 10, B = 20, C = 30};
        FooD d = new FooD() {A = 10, B = 20, C = 30, D = 40};

        using (var stream = File.Open(fileName, FileMode.Create, FileAccess.Write))
        {
            SerializeWithLengthPrefix(stream, a);
            SerializeWithLengthPrefix(stream, b);
            SerializeWithLengthPrefix(stream, b1);
            SerializeWithLengthPrefix(stream, c);
            SerializeWithLengthPrefix(stream, d);
        }

Вспомогательные методы

    public void SerializeWithLengthPrefix<T>(Stream stream, T obj)
    {
        var serializationContext = new ProtoBuf.SerializationContext() { Context = this };
        Model.SerializeWithLengthPrefix(stream, obj, typeof(T), PrefixStyle.Base128, 1, serializationContext);
    }

    public T DeserializeWithLengthPrefix<T>(Stream stream, out long bytesRead, out bool haveObject)
    {
        var serializationContext = new ProtoBuf.SerializationContext() { Context = this };

        return (T)Model.DeserializeWithLengthPrefix(stream, null, typeof(T), PrefixStyle.Base128, 1, null, out bytesRead, out haveObject, serializationContext);
    }

Теперь мне нужно изменить иерархию наследования следующим образом (пометить это как версию 2):

public class FooA
{
    public double A { get; set; }
    public double B { get; set; }
}

public class FooB : FooA
{
    public double C { get; set; }
}

public class FooC : FooA1/*FooB*/
{
    public double C { get; set; }
}

public class FooD : FooC
{
    public double D { get; set; }
}

public class FooA1 /*: FooA*/
{
    public double A { get; set; }
    public double B { get; set; }
}

А также определение модели в соответствии с ним, пытаясь сохранить один и тот же идентификатор для каждого класса, определенного ранее.

       Model = RuntimeTypeModel.Create();

        Model.Add(typeof(FooA), false)
            .AddSubType(201, typeof(FooB))
            //.AddSubType(202, typeof(FooA1))
            .Add(1, "A")
            .Add(2, "B");

        Model.Add(typeof(FooA1), false)
            .Add(1, "A")
            .Add(2, "B");

        Model[typeof(FooB)]
            //.AddSubType(201, typeof(FooC))
            .Add(1, "C");

        Model[typeof(FooA1)]
            .AddSubType(201, typeof(FooC));

        Model[typeof(FooC)]
            .Add(1, "C")
            .AddSubType(201, typeof(FooD));                

        Model[typeof(FooD)]
            .Add(1, "D");

Теперь я десериализирую файл, хранящийся в Версии 1, и проверяю типы, определенные в модели: они такие же, как и во время сериализации с Версией 1.

Model types definition

Но когда я проверяю значения объектов, я вижу, что FooC десериализован как FooD, а значение D всегда равно 0.

Deserialized values

Что я делаю не так? Есть ли способ справиться с этим?

UPDATE

При попытке отладки исходного кода protobuf-net при десериализации FooC в Версии 2 метод RuntimeTypeModel .GetKey () запускается из базового класса (getBaseKey = true), правильно получает FooA1 (key = 2), но в конечном итоге получает FooD объект вместо FooC . Может быть, есть способ обработать этот метод по-другому, чтобы разрешить такой сценарий?

protobuf debug

Ответы [ 2 ]

1 голос
/ 31 мая 2019

Я не могу придумать способ сделать это изменение без нарушения совместимости. Мой совет, когда мы кого-то связываем в один узел: разделите «реальные» типы и типы сериализации на отдельные модели типов, которые не должны быть отображением 1: 1 друг для друга. Затем вы можете делать все, что хотите, с «реальными» типами (модель предметной области), и вы просто проецируете эти значения в модель сериализации, которые могут иметь другие правила и быть более удобными для сериализатора. В этом случае типы сериализации могут не являться картой 1: 1 с точки зрения дерева наследования.

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


Ниже показано, как отобразить результирующий макет .proto, чтобы объяснить, почему FooC заканчивается как FooD в v2:

v1, начиная с FooA - FooC - это 201, 201 (FooD - это 201, 201, 201)

syntax = "proto2";

message FooA {
   optional double A = 1;
   optional double B = 2;
   oneof subtype {
      FooB FooB = 201;
      FooA1 FooA1 = 202;
   }
}
message FooA1 {
}
message FooB {
   optional double C = 1;
   oneof subtype {
      FooC FooC = 201;
   }
}
message FooC {
   oneof subtype {
      FooD FooD = 201;
   }
}
message FooD {
   optional double D = 1;
}

v2, начиная с FooA1 - FooD - 201, 201:

syntax = "proto2";

message FooA1 {
   optional double A = 1;
   optional double B = 2;
   oneof subtype {
      FooC FooC = 201;
   }
}
message FooC {
   optional double C = 1;
   oneof subtype {
      FooD FooD = 201;
   }
}
message FooD {
   optional double D = 1;
}
0 голосов
/ 03 июня 2019

Благодаря объяснению @Marc я нашел способ справиться с этим конкретным вариантом использования с помощью некоторых вспомогательных классов.

Я публикую здесь свое решение, может ли оно быть полезным для кого-то еще.

Версия 2 может быть определена следующим образом:

public class FooA
{
    public double A { get; set; }
    public double B { get; set; }
}

public class FooAFake
{
    public double A { get; set; }
    public double B { get; set; }
}

public class FooB : FooA
{
    public double C { get; set; }
}

public class FooBFake : FooAFake
{
    public double C { get; set; }
}

public class FooC : FooBFake
{        
}

public class FooD : FooC
{
    public double D { get; set; }
}

public class FooA1 : FooAFake
{        
}

И модель таким образом:

        Model = RuntimeTypeModel.Create();

        Model.Add(typeof(FooA), false)
            .AddSubType(201, typeof(FooB))
            //.AddSubType(202, typeof(FooA1))
            .Add(1, "A")
            .Add(2, "B");

        Model[typeof(FooB)]
            //.AddSubType(201, typeof(FooC))
            .Add(1, "C");

        Model[typeof(FooC)]
            .AddSubType(201, typeof(FooD));                

        Model[typeof(FooD)]
            .Add(1, "D");

        Model.Add(typeof(FooAFake), false)
            .AddSubType(201, typeof(FooBFake))
            .AddSubType(202, typeof(FooA1))
            .Add(1, "A")
            .Add(2, "B");

        Model[typeof(FooBFake)]
            .AddSubType(201, typeof(FooC))
            .Add(1, "C");
...