【问题标题】:Json.Net PopulateObject - update list elements based on IDJson.Net PopulateObject - 根据 ID 更新列表元素
【发布时间】:2016-01-06 08:50:18
【问题描述】:

是否可以定义用于JsonConvert.PopulateObject 方法的自定义“列表合并”启动?

示例:

我有两个模型:

class Parent
{
    public Guid Uuid { get; set; }

    public string Name { get; set; }

    public List<Child> Childs { get; set; }
}

class Child 
{
    public Guid Uuid { get; set; }

    public string Name { get; set; }

    public int Score { get; set; }
}

我的初始 JSON:

{  
   "Uuid":"cf82b1fd-1ca0-4125-9ea2-43d1d71c9bed",
   "Name":"John",
   "Childs":[  
      {  
         "Uuid":"96b93f95-9ce9-441d-bfb0-f44b65f7fe0d",
         "Name":"Philip",
         "Score":100
      },
      {  
         "Uuid":"fe7837e0-9960-4c45-b5ab-4e4658c08ccd",
         "Name":"Peter",
         "Score":150
      },
      {  
         "Uuid":"1d2cdba4-9efb-44fc-a2f3-6b86a5291954",
         "Name":"Steve",
         "Score":80
      }
   ]
}

还有我的更新 JSON:

{  
   "Uuid":"cf82b1fd-1ca0-4125-9ea2-43d1d71c9bed",
   "Childs":[  
      {  
         "Uuid":"fe7837e0-9960-4c45-b5ab-4e4658c08ccd",
         "Score":170
      }
   ]
}

我只需要指定一个模型属性(按属性)用于匹配列表项(在我的例子中是 Child 的 Uuid 属性),因此在从我的初始 JSON 反序列化的对象上调用 JsonConvert.PopulateObject更新 JSON(它仅包含每个对象的更改值 + Uuid)结果仅更新由 Uuid 生成的更新 JSON 中包含的列表元素(在我的情况下更新彼得的分数),并且更新 JSON 中不包含的元素保持不变。

我正在寻找一些通用的解决方案 - 我需要将它应用于具有大量嵌套列表的大型 JSON(但每个模型都有一些独特的属性)。所以我需要在匹配的列表项上递归调用PopulateObject。

【问题讨论】:

    标签: c# json json.net


    【解决方案1】:

    您可以创建自己的JsonConverter 来实现所需的合并逻辑。这是可能的,因为 JsonConverter.ReadJson 传递了一个 existingValue 参数,其中包含正在反序列化的属性的预先存在的内容。

    因此:

    [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
    public class JsonMergeKeyAttribute : System.Attribute
    {
    }
    
    public class KeyedListMergeConverter : JsonConverter
    {
        readonly IContractResolver contractResolver;
    
        public KeyedListMergeConverter(IContractResolver contractResolver)
        {
            if (contractResolver == null)
                throw new ArgumentNullException("contractResolver");
            this.contractResolver = contractResolver;
        }
    
        static bool CanConvert(IContractResolver contractResolver, Type objectType, out Type elementType, out JsonProperty keyProperty)
        {
            elementType = objectType.GetListType();
            if (elementType == null)
            {
                keyProperty = null;
                return false;
            }
            var contract = contractResolver.ResolveContract(elementType) as JsonObjectContract;
            if (contract == null)
            {
                keyProperty = null;
                return false;
            }
            keyProperty = contract.Properties.Where(p => p.AttributeProvider.GetAttributes(typeof(JsonMergeKeyAttribute), true).Count > 0).SingleOrDefault();
            return keyProperty != null;
        }
    
        public override bool CanConvert(Type objectType)
        {
            Type elementType;
            JsonProperty keyProperty;
            return CanConvert(contractResolver, objectType, out elementType, out keyProperty);
        }
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            if (contractResolver != serializer.ContractResolver)
                throw new InvalidOperationException("Inconsistent contract resolvers");
            Type elementType;
            JsonProperty keyProperty;
            if (!CanConvert(contractResolver, objectType, out elementType, out keyProperty))
                throw new JsonSerializationException(string.Format("Invalid input type {0}", objectType));
    
            if (reader.TokenType == JsonToken.Null)
                return existingValue;
    
            var list = existingValue as IList;
            if (list == null || list.Count == 0)
            {
                list = list ?? (IList)contractResolver.ResolveContract(objectType).DefaultCreator();
                serializer.Populate(reader, list);
            }
            else
            {
                var jArray = JArray.Load(reader);
                var comparer = new KeyedListMergeComparer();
                var lookup = jArray.ToLookup(i => i[keyProperty.PropertyName].ToObject(keyProperty.PropertyType, serializer), comparer);
                var done = new HashSet<JToken>();
                foreach (var item in list)
                {
                    var key = keyProperty.ValueProvider.GetValue(item);
                    var replacement = lookup[key].Where(v => !done.Contains(v)).FirstOrDefault();
                    if (replacement != null)
                    {
                        using (var subReader = replacement.CreateReader())
                            serializer.Populate(subReader, item);
                        done.Add(replacement);
                    }
                }
                // Populate the NEW items into the list.
                if (done.Count < jArray.Count)
                    foreach (var item in jArray.Where(i => !done.Contains(i)))
                    {
                        list.Add(item.ToObject(elementType, serializer));
                    }
            }
            return list;
        }
    
        public override bool CanWrite { get { return false; } }
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }
    
        class KeyedListMergeComparer : IEqualityComparer<object>
        {
            #region IEqualityComparer<object> Members
    
            bool IEqualityComparer<object>.Equals(object x, object y)
            {
                if (object.ReferenceEquals(x, y))
                    return true;
                else if (x == null || y == null)
                    return false;
                return x.Equals(y);
            }
    
            int IEqualityComparer<object>.GetHashCode(object obj)
            {
                if (obj == null)
                    return 0;
                return obj.GetHashCode();
            }
    
            #endregion
        }
    }
    
    public static class TypeExtensions
    {
        public static Type GetListType(this Type type)
        {
            while (type != null)
            {
                if (type.IsGenericType)
                {
                    var genType = type.GetGenericTypeDefinition();
                    if (genType == typeof(List<>))
                        return type.GetGenericArguments()[0];
                }
                type = type.BaseType;
            }
            return null;
        }
    }
    

    请注意,转换器需要知道当前使用的IContractResolver。拥有它可以更轻松地找到关键参数,并且还可以确保,如果关键参数具有 [JsonProperty(name)] 属性,则替换名称受到尊重。

    然后添加属性:

    class Child
    {
        [JsonMergeKey]
        [JsonProperty("Uuid")] // Replacement name for testing
        public Guid UUID { get; set; }
    
        public string Name { get; set; }
    
        public int Score { get; set; }
    }
    

    并按如下方式使用转换器:

            var serializer = JsonSerializer.CreateDefault();
            var converter = new KeyedListMergeConverter(serializer.ContractResolver);
            serializer.Converters.Add(converter);
    
            using (var reader = new StringReader(updateJson))
            {
                serializer.Populate(reader, parent);
            }
    

    转换器假定关键参数始终存在于 JSON 中。此外,如果要合并的 JSON 中的任何条目具有在现有列表中找不到的键,它们将被附加到列表中。

    更新

    原始转换器专门为List&lt;T&gt; 硬编码,并利用List&lt;T&gt; 实现IList&lt;T&gt; 和IList 的事实。如果您的集合不是 List&lt;T&gt; 但仍实现 IList&lt;T&gt;,则以下内容应该有效:

    public class KeyedIListMergeConverter : JsonConverter
    {
        readonly IContractResolver contractResolver;
    
        public KeyedIListMergeConverter(IContractResolver contractResolver)
        {
            if (contractResolver == null)
                throw new ArgumentNullException("contractResolver");
            this.contractResolver = contractResolver;
        }
    
        static bool CanConvert(IContractResolver contractResolver, Type objectType, out Type elementType, out JsonProperty keyProperty)
        {
            if (objectType.IsArray)
            {
                // Not implemented for arrays, since they cannot be resized.
                elementType = null;
                keyProperty = null;
                return false;
            }
            var elementTypes = objectType.GetIListItemTypes().ToList();
            if (elementTypes.Count != 1)
            {
                elementType = null;
                keyProperty = null;
                return false;
            }
            elementType = elementTypes[0];
            var contract = contractResolver.ResolveContract(elementType) as JsonObjectContract;
            if (contract == null)
            {
                keyProperty = null;
                return false;
            }
            keyProperty = contract.Properties.Where(p => p.AttributeProvider.GetAttributes(typeof(JsonMergeKeyAttribute), true).Count > 0).SingleOrDefault();
            return keyProperty != null;
        }
    
        public override bool CanConvert(Type objectType)
        {
            Type elementType;
            JsonProperty keyProperty;
            return CanConvert(contractResolver, objectType, out elementType, out keyProperty);
        }
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            if (contractResolver != serializer.ContractResolver)
                throw new InvalidOperationException("Inconsistent contract resolvers");
            Type elementType;
            JsonProperty keyProperty;
            if (!CanConvert(contractResolver, objectType, out elementType, out keyProperty))
                throw new JsonSerializationException(string.Format("Invalid input type {0}", objectType));
    
            if (reader.TokenType == JsonToken.Null)
                return existingValue;
    
            var method = GetType().GetMethod("ReadJsonGeneric", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);
            var genericMethod = method.MakeGenericMethod(new[] { elementType });
            try
            {
                return genericMethod.Invoke(this, new object[] { reader, objectType, existingValue, serializer, keyProperty });
            }
            catch (TargetInvocationException ex)
            {
                // Wrap the TargetInvocationException in a JsonSerializationException
                throw new JsonSerializationException("ReadJsonGeneric<T> error", ex);
            }
        }
    
        object ReadJsonGeneric<T>(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer, JsonProperty keyProperty)
        {
            var list = existingValue as IList<T>;
            if (list == null || list.Count == 0)
            {
                list = list ?? (IList<T>)contractResolver.ResolveContract(objectType).DefaultCreator();
                serializer.Populate(reader, list);
            }
            else
            {
                var jArray = JArray.Load(reader);
                var comparer = new KeyedListMergeComparer();
                var lookup = jArray.ToLookup(i => i[keyProperty.PropertyName].ToObject(keyProperty.PropertyType, serializer), comparer);
                var done = new HashSet<JToken>();
                foreach (var item in list)
                {
                    var key = keyProperty.ValueProvider.GetValue(item);
                    var replacement = lookup[key].Where(v => !done.Contains(v)).FirstOrDefault();
                    if (replacement != null)
                    {
                        using (var subReader = replacement.CreateReader())
                            serializer.Populate(subReader, item);
                        done.Add(replacement);
                    }
                }
                // Populate the NEW items into the list.
                if (done.Count < jArray.Count)
                    foreach (var item in jArray.Where(i => !done.Contains(i)))
                    {
                        list.Add(item.ToObject<T>(serializer));
                    }
            }
            return list;
        }
    
        public override bool CanWrite { get { return false; } }
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }
    
        class KeyedListMergeComparer : IEqualityComparer<object>
        {
            #region IEqualityComparer<object> Members
    
            bool IEqualityComparer<object>.Equals(object x, object y)
            {
                return object.Equals(x, y);
            }
    
            int IEqualityComparer<object>.GetHashCode(object obj)
            {
                if (obj == null)
                    return 0;
                return obj.GetHashCode();
            }
    
            #endregion
        }
    }
    
    public static class TypeExtensions
    {
        public static IEnumerable<Type> GetInterfacesAndSelf(this Type type)
        {
            if (type == null)
                throw new ArgumentNullException();
            if (type.IsInterface)
                return new[] { type }.Concat(type.GetInterfaces());
            else
                return type.GetInterfaces();
        }
    
        public static IEnumerable<Type> GetIListItemTypes(this Type type)
        {
            foreach (Type intType in type.GetInterfacesAndSelf())
            {
                if (intType.IsGenericType
                    && intType.GetGenericTypeDefinition() == typeof(IList<>))
                {
                    yield return intType.GetGenericArguments()[0];
                }
            }
        }
    }
    

    请注意,数组没有实现合并,因为它们不可调整大小。

    【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-05-22
    • 1970-01-01
    • 2017-04-18
    • 1970-01-01
    • 1970-01-01
    • 2021-04-26
    • 2014-10-05
    相关资源
    最近更新 更多