Как я могу заставить Json.NET сериализовать и десериализовать объявленные свойства настраиваемых динамических типов, которые также реализуют IDictionary ‹string, object›?

У меня есть собственный тип, производный от типа DynamicObject. Этот тип имеет фиксированные свойства, объявленные в типе. Таким образом, он позволяет пользователю предоставлять некоторые требуемые свойства в дополнение к любым динамическим свойствам, которые они хотят. Когда я использую метод JsonConvert.DeserializeObject<MyType>(json) для десериализации данных для этого типа, он не устанавливает объявленные свойства, но эти свойства доступны через свойство индексатора объекта динамического объекта. Это говорит мне, что он просто рассматривает объект как словарь и не пытается вызвать объявленные установщики свойств и не использует их для вывода информации о типе свойства.

Кто-нибудь сталкивался с такой ситуацией раньше? Есть идеи, как я могу указать классу JsonConvert учитывать объявленные свойства при десериализации данных объекта?

Я пытался использовать собственный JsonConverter, но для этого мне нужно было написать сложные методы чтения и записи JSON. Я надеялся найти способ ввести информацию о контракте на собственность, переопределив JsonContractResolver или JsonConverter и т. Д.


//#define IMPLEMENT_IDICTIONARY

using System.Collections;
using System.Collections.Generic;
using System.Dynamic;
using Newtonsoft.Json;

namespace ConsoleApp1
{
    class Program
    {
        public class MyDynamicObject : DynamicObject
#if IMPLEMENT_IDICTIONARY
            , IDictionary<string, object>
#endif
        {
            private Dictionary<string, object> m_Members;

            public MyDynamicObject()
            {
                this.m_Members = new Dictionary<string, object>();
            }


#if IMPLEMENT_IDICTIONARY
            public int Count { get { return this.m_Members.Count; } }

            public ICollection<string> Keys => this.m_Members.Keys;

            public ICollection<object> Values => this.m_Members.Values;

            bool ICollection<KeyValuePair<string, object>>.IsReadOnly => false;

            /// <summary>
            /// Gets or sets the specified member value.
            /// </summary>
            /// <param name="memberName">Name of the member in question.</param>
            /// <returns>A value for the specified member.</returns>
            public object this[string memberName]
            {
                get
                {
                    object value;
                    if (this.m_Members.TryGetValue(memberName, out value))
                        return value;
                    else
                        return null;
                }
                set => this.m_Members[memberName] = value;
            }
#endif


            public override bool TryGetMember(GetMemberBinder binder, out object result)
            {
                this.m_Members.TryGetValue(binder.Name, out result);
                return true;
            }

            public override bool TrySetMember(SetMemberBinder binder, object value)
            {
                this.m_Members[binder.Name] = value;
                return true;
            }

            public override bool TryDeleteMember(DeleteMemberBinder binder)
            {
                return this.m_Members.Remove(binder.Name);
            }

            public override IEnumerable<string> GetDynamicMemberNames()
            {
                var names = base.GetDynamicMemberNames();
                return this.m_Members.Keys;
            }

#if IMPLEMENT_IDICTIONARY
            bool IDictionary<string, object>.ContainsKey(string memberName)
            {
                return this.m_Members.ContainsKey(memberName);
            }

            public void Add(string memberName, object value)
            {
                this.m_Members.Add(memberName, value);
            }

            public bool Remove(string memberName)
            {
                return this.m_Members.Remove(memberName);
            }

            public bool TryGetValue(string memberName, out object value)
            {
                return this.m_Members.TryGetValue(memberName, out value);
            }

            public void Clear()
            {
                this.m_Members.Clear();
            }

            void ICollection<KeyValuePair<string, object>>.Add(KeyValuePair<string, object> member)
            {
                ((IDictionary<string, object>)this.m_Members).Add(member);
            }

            bool ICollection<KeyValuePair<string, object>>.Contains(KeyValuePair<string, object> member)
            {
                return ((IDictionary<string, object>)this.m_Members).Contains(member);
            }

            public void CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
            {
                ((IDictionary<string, object>)this.m_Members).CopyTo(array, arrayIndex);
            }

            bool ICollection<KeyValuePair<string, object>>.Remove(KeyValuePair<string, object> member)
            {
                return ((IDictionary<string, object>)this.m_Members).Remove(member);
            }

            public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
            {
                return this.m_Members.GetEnumerator();
            }

            IEnumerator IEnumerable.GetEnumerator()
            {
                return this.m_Members.GetEnumerator();
            }
#endif
        }

        public class ProxyInfo
        {
            public string Server;
            public int Port;
        }

        public class CustomDynamicObject : MyDynamicObject
        {
            //[JsonProperty] // NOTE: Cannot do this.
            public string Name { get; set; }

            //[JsonProperty]  // NOTE: Cannot do this.
            public ProxyInfo Proxy { get; set; }
        }


        static void Main(string[] args)
        {
            dynamic obj = new CustomDynamicObject()
            {
                Name = "Test1",
                Proxy = new ProxyInfo() { Server = "http://test.com/",  Port = 10102 }
            };
            obj.Prop1 = "P1";
            obj.Prop2 = 320;

            string json = JsonConvert.SerializeObject(obj);  // Returns: { "Prop1":"P1", "Prop2":320 }

            // ISSUE #1: It did not serialize the declared properties. Only the dynamically added properties are serialized.
            //           Following JSON was expected. It produces correct JSON if I mark the declared properties with
            //           JsonProperty attribute, which I cannot do in all cases.
            string expectedJson = "{ \"Prop1\":\"P1\", \"Prop2\":320, \"Name\":\"Test1\", \"Proxy\":{ \"Server\":\"http://test.com/\", \"Port\":10102 } }";


            CustomDynamicObject deserializedObj = JsonConvert.DeserializeObject<CustomDynamicObject>(expectedJson);

            // ISSUE #2: Deserialization worked in this case, but does not work once I re-introduce the IDictionary interface on my base class.
            //           In that case, it does not populate the declared properties, but simply added all 4 properties to the underlying dictionary.
            //           Neither does it infer the ProxyInfo type when deserializing the Proxy property value and simply bound the JObject token to
            //           the dynamic object.
        }
    }
}

Я ожидал, что он будет использовать отражение для определения свойства и информации о его типе, как это делается для обычных типов. Но кажется, что он просто рассматривает объект как обычный словарь.

Обратите внимание, что:

  • Я не могу удалить интерфейс IDictionary<string, object>, поскольку некоторые варианты использования моего API полагаются на то, что объект является словарем, а не динамическим.

  • Добавление [JsonProperty] ко всем объявленным свойствам для сериализации нецелесообразно, поскольку его производные типы создаются другими разработчиками, и им не нужно явно заботиться о механизме сохранения.

Любые предложения о том, как я могу заставить его работать правильно?


person B Singh    schedule 01.04.2019    source источник
comment
Можете ли вы поделиться минимальным воспроизводимым примером, который включает вашу реализацию MyDynamicObject? Это должно работать, пока MyDynamicObject реализовано правильно. См., Например, Сериализовать экземпляр класса, производного от класса DynamicObject, где проблема заключалась в невозможности переопределить GetDynamicMemberNames(). Возможно, вам потребуется пометить сериализуемые свойства с помощью [JsonProperty], см. C # Как сериализовать (JSON, XML) обычные свойства в классе, который наследуется от DynamicObject.   -  person dbc    schedule 02.04.2019
comment
Спасибо за быстрый ответ. У меня было две проблемы с моим настраиваемым типом: 1) он не реализовал метод GetDynamicMemberNames() и 2) он реализовал интерфейс IDictionary<string, object>. Когда я удалил интерфейс IDictionary и реализовал метод GetDynamicMemberNames, сериализация сработала.   -  person B Singh    schedule 02.04.2019
comment
Однако для объявленных свойств требуется атрибут [JsonProperty], что я не могу сделать во всех случаях, поскольку эти производные типы создаются другими разработчиками, и им не нужно явно заботиться о механизме сохраняемости. Я могу попросить их изменить свои библиотеки, чтобы включить атрибут, но я хотел бы посмотреть, можно ли это обработать автоматически в коде моей базовой библиотеки. Есть предложения по этому поводу?   -  person B Singh    schedule 02.04.2019
comment
Мне действительно нужно, чтобы мой настраиваемый тип реализовывал интерфейс IDictionary, поскольку некоторые из вариантов использования в моем ответе API на объект должны быть словарем, а не динамическим. Любые предложения о том, как сохранить интерфейс IDictionary и при этом правильно работать сериализацию / десериализацию?   -  person B Singh    schedule 02.04.2019
comment
Спасибо. Обновил приведенный выше пример кода на рабочий.   -  person B Singh    schedule 02.04.2019


Ответы (2)


Здесь у вас есть несколько проблем:

  1. Вам необходимо правильно переопределить DynamicObject.GetDynamicMemberNames(), как описано в этом ответе на Сериализовать экземпляр класса, производного от класса DynamicObject, AlbertK, чтобы Json.NET мог сериализовать ваши динамические свойства.

    (Это уже было исправлено в отредактированной версии вашего вопроса.)

  2. Объявленные свойства не отображаются, если вы явно не пометите их с помощью [JsonProperty] (как описано в этом ответе на C # Как сериализовать (JSON, XML) обычные свойства в классе, который наследуется от DynamicObject), но определения ваших типов читаются - только и не может быть изменен.

    Проблема здесь, похоже, в том, что JsonSerializerInternalWriter.SerializeDynamic() сериализует только объявленные свойства, для которых JsonProperty.HasMemberAttribute == true. (Я не знаю, почему эта проверка сделана там, кажется, имеет смысл установить CanRead или Ignored внутри преобразователя контрактов.)

  3. Вы хотели бы, чтобы ваш класс реализовал IDictionary<string, object>, но если вы это сделаете, это нарушит десериализацию; объявленные свойства больше не заполняются, а вместо этого добавляются в словарь.

    Проблема здесь, похоже, в том, что DefaultContractResolver.CreateContract() возвращает JsonDictionaryContract, а не JsonDynamicContract, когда входящий тип реализует IDictionary<TKey, TValue> для любых TKey и TValue.

Предполагая, что вы устранили проблему №1, проблемы №2 и №3 можно решить с помощью настраиваемый преобразователь контрактов, например:

public class MyContractResolver : DefaultContractResolver
{
    protected override JsonContract CreateContract(Type objectType)
    {
        // Prefer JsonDynamicContract for MyDynamicObject
        if (typeof(MyDynamicObject).IsAssignableFrom(objectType))
        {
            return CreateDynamicContract(objectType);
        }
        return base.CreateContract(objectType);
    }

    protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
    {
        var properties = base.CreateProperties(type, memberSerialization);
        // If object type is a subclass of MyDynamicObject and the property is declared
        // in a subclass of MyDynamicObject, assume it is marked with JsonProperty 
        // (unless it is explicitly ignored).  By checking IsSubclassOf we ensure that 
        // "bookkeeping" properties like Count, Keys and Values are not serialized.
        if (type.IsSubclassOf(typeof(MyDynamicObject)) && memberSerialization == MemberSerialization.OptOut)
        {
            foreach (var property in properties)
            {
                if (!property.Ignored && property.DeclaringType.IsSubclassOf(typeof(MyDynamicObject)))
                {
                    property.HasMemberAttribute = true;
                }
            }
        }
        return properties;
    }
}

Затем, чтобы использовать преобразователь контрактов, кешируйте его где-нибудь для повышения производительности:

static IContractResolver resolver = new MyContractResolver();

А затем сделайте:

var settings = new JsonSerializerSettings
{
    ContractResolver = resolver,
};
string json = JsonConvert.SerializeObject(obj, settings);

Образец скрипта здесь.

person dbc    schedule 02.04.2019
comment
С расширенным преобразователем контрактов, как описано выше, теперь у меня другая проблема! Если мой файл JSON содержит комментарий, десериализовать файл не удастся. Бросает Unexpected token when deserializing object: Comment. Path 'appBaseUrl', line 4, position 125.. Это свойство в файле JSON выглядит как "appBaseUrl": "http://wwww.WillBeReplacedInCode.com", // NOTE: Placeholder for the runtime location to be used by the test.. У меня не возникает этой проблемы, если я не устанавливаю распознаватель контрактов в объекте настроек. Есть идеи, почему это не удалось? - person B Singh; 03.04.2019
comment
@BSingh - мне нужно увидеть минимально воспроизводимый пример. Обратите внимание, что комментарии не являются частью стандарта JSON, они являются расширением, которое поддерживает Json.NET. И в их поддержке иногда есть ошибки, см., Например, github.com/JamesNK/Newtonsoft.Json/issues/1545. - person dbc; 03.04.2019
comment
comment
да. Спасибо. Кажется, есть еще одна ошибка в преобразователе контрактов динамических объектов. Он не заполняет значения объявленных свойств списка типов или словаря, если свойство не включает установщик. Для обычных типов он отлично работает без средства задания свойств для таких свойств. - person B Singh; 05.04.2019

Я не могу сказать, что находится внутри класса ProxyInfo. Однако при использовании строки для свойства Name и Proxy десериализация работает правильно. Пожалуйста, проверьте следующий рабочий образец:

    class Program
    {
        static void Main(string[] args)
        {
            // NOTE: This is how I load the JSON data into the new type.
            var obj = JsonConvert.DeserializeObject<MyCustomDynamicObject>("{name:'name1', proxy:'string'}");
            var proxy = obj.Proxy;
            var name = obj.Name;
        }
    }

    public class MyDynamicObject : DynamicObject
    {
        // Implements the functionality to store dynamic properties in 
        // dictionary.
        // NOTE: This base class does not have any declared properties.
    }

    // NOTE: This is the actual concrete type that has declared properties
    public class MyCustomDynamicObject : MyDynamicObject
    {
        public string Name { get; set; }
        public string Proxy { get; set; }
    }
person Gru b.    schedule 02.04.2019