Protobuf-net RuntimeTypeModel с унаследованными закрытыми полями завершается неудачно, атрибут ProtoInclude на базовом классе работает - PullRequest
1 голос
/ 01 июня 2019

Если у меня есть тестовая настройка как

    [ProtoContract]
    [ProtoInclude(1, typeof(DecoratorDerived))]
    public class DecoratorBase
    {
        [ProtoMember(2)]
        private int baseValue1;

        private int baseValue2;

        public DecoratorBase()
        {
            baseValue1 = (new Random()).Next();
            baseValue2 = (new Random()).Next();
        }

        protected void ShowBaseValue()
        {
            Console.WriteLine($"DecoratorBase - baseValue1: {baseValue1}, baseValue2: {baseValue2}");
        }
    }

    [ProtoContract]
    public class DecoratorDerived : DecoratorBase
    {
        [ProtoMember(2)]
        public int derivedValue1;

        private int derivedValue2;

        public DecoratorDerived()
        {
            derivedValue1 = (new Random()).Next();
            derivedValue2 = (new Random()).Next();
        }

        public void ShowValues()
        {
            ShowBaseValue();
            Console.WriteLine($"DecoratorDerived - derivedValue1: {derivedValue1}, derivedValue2: {derivedValue2}");
        }
    }

        static void DecoratorTest()
        {
            var c1 = new DecoratorDerived();
            c1.ShowValues();

            byte[] raw;
            using (var stream = new MemoryStream())
            {
                ProtoBuf.Serializer.Serialize<DecoratorDerived>(stream, c1);
                raw = stream.ToArray();
            }

            DecoratorDerived c2;
            using (var stream = new MemoryStream(raw))
            {
                c2 = ProtoBuf.Serializer.Deserialize<DecoratorDerived>(stream);
            }
            c2.ShowValues();
        }

Все работает нормально, но проблема в том, что мои классы, полученные из базы, генерируются автоматически через T4, и их много, поэтому добавление всех строк ProtoInclude нереально. Проведение некоторых исследований показало, что все работает на ходу с RuntimeTypeModel. Таким образом, изменение теста на

    [ProtoContract]
    public class RTMBase
    {
        [ProtoMember(2)]
        private int baseValue1;

        private int baseValue2;

        public RTMBase()
        {
            baseValue1 = (new Random()).Next();
            baseValue2 = (new Random()).Next();
        }

        protected void ShowBaseValue()
        {
            Console.WriteLine($"RTMBase - baseValue1: {baseValue1}, baseValue2: {baseValue2}");
        }
    }

    [ProtoContract]
    public class RTMDerived : RTMBase
    {
        [ProtoMember(2)]
        public int derivedValue1;

        private int derivedValue2;

        public RTMDerived()
        {
            derivedValue1 = (new Random()).Next();
            derivedValue2 = (new Random()).Next();
        }

        public void ShowValues()
        {
            ShowBaseValue();
            Console.WriteLine($"RTMDerived - derivedValue1: {derivedValue1}, derivedValue2: {derivedValue2}");
        }
    }

        static void RTMTest(RuntimeTypeModel runtimeTypeModel)
        {
            var c1 = new RTMDerived();
            c1.ShowValues();

            // setup RTM, https://stackoverflow.com/questions/40608767/inheritance-in-protobuf-net-adding-a-lower-base-class-still-backward-compatible
            var myType = runtimeTypeModel[typeof(RTMDerived)];
            var baseType = runtimeTypeModel[typeof(RTMDerived).BaseType];
            if (!baseType.GetSubtypes().Any(s => s.DerivedType == myType))
            {
                foreach (var field in baseType.GetFields())
                {
                    myType.Add(field.FieldNumber + 500, field.Name);
                }
            }

            byte[] raw;
            using(var stream = new MemoryStream())
            {
                ProtoBuf.Serializer.Serialize<RTMDerived>(stream, c1);
                raw = stream.ToArray();
            }

            RTMDerived c2;
            using(var stream = new MemoryStream(raw))
            {
                c2 = ProtoBuf.Serializer.Deserialize<RTMDerived>(stream);
            }
            c2.ShowValues();
        }

Я получаю исключение

RTMBase - baseValue1: 1874947795, baseValue2: 1391655165
RTMDerived - derivedValue1: 922997568, derivedValue2: 837049520

Unhandled Exception: System.ArgumentException: Unable to determine member: baseValue1
Parameter name: memberName
   at ProtoBuf.Meta.MetaType.AddField(Int32 fieldNumber, String memberName, Type itemType, Type defaultType, Object defaultValue) in C:\code\protobuf-net\src\protobuf-net\Meta\MetaType.cs:line 1437
   at ProtoBuf.Meta.MetaType.Add(Int32 fieldNumber, String memberName) in C:\code\protobuf-net\src\protobuf-net\Meta\MetaType.cs:line 1261
   at proto_error.Program.RTMTest1(RuntimeTypeModel runtimeTypeModel) in /Users/christian/tmp/proto-error/Program.cs:line 52
   at proto_error.Program.Main(String[] args) in /Users/christian/tmp/proto-error/Program.cs:line 17

К счастью для меня, изменение RTMBase.baseValue1 на общедоступное, защищенное или внутреннее заставляет его работать, а внутреннее вполне подойдет для моего случая использования. Но мне любопытно, если это ошибка или я что-то не так делаю?

ДОПОЛНЕНИЕ

Если я изменю RMTest на

        static void RTMTest2(RuntimeTypeModel runtimeTypeModel)
        {
            var c1 = new RTMDerived();
            c1.ShowValues();

            // setup RTM, https://stackoverflow.com/questions/10400539/protobuf-net-runtimetypemodel-not-serializing-members-of-base-class
            var baseType = runtimeTypeModel[typeof(RTMDerived).BaseType];
            baseType.AddSubType(500, typeof(RTMDerived));

            byte[] raw;
            using (var stream = new MemoryStream())
            {
                ProtoBuf.Serializer.Serialize<RTMDerived>(stream, c1);
                raw = stream.ToArray();
            }

            RTMDerived c2;
            using (var stream = new MemoryStream(raw))
            {
                c2 = ProtoBuf.Serializer.Deserialize<RTMDerived>(stream);
            }
            c2.ShowValues();
        }

Работает просто отлично, но мне все еще любопытно, почему RTMRest1 терпит неудачу. Кроме того, параметр 500 должен быть различным для каждого типа или постоянным?

1 Ответ

0 голосов
/ 04 июня 2019

Предоставленный ключ используется для уникальной идентификации каждый раз как поля в родительском сообщении, поэтому да: он должен быть различным для каждого типа и надежным - то есть, если сейчас он равен 500, то он также должно быть 500 при перезагрузке этих данных через два года.

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

foreach (var field in baseType.GetFields())
{
    myType.Add(field.FieldNumber + 500, field.Name);
}
  1. отражение не дает никаких гарантий относительно того, в каком порядке оно возвращает вещи, разбивая данные
  2. если кто-то добавляет дополнительные поля, он может изменить номера полей других полей, нарушая данные

Что касается того, что он не находит участника - я думаю, он ищет его как объявленного участника. Это возможно что-то, что можно изменить, хотя это может создать двусмысленность и хрупкие сценарии. Следующий код полностью действителен и отлично компилируется:

class Foo
{
    private int a;
}
class Bar : Foo
{
    private int a;
}

Но значение "a" меняется в зависимости от того, думаем мы о Foo или Bar. В вашем случае (преобразование базовых членов в производные типы) вы думаете о Foo.a, но вы бы попросили его посмотреть, начиная с Bar, поэтому логичным решением будет Bar.a. Я думаю (по памяти), поэтому мы ограничиваемся вещами объявленных членов.

...