Json.Net PopulateObject – 基于ID更新列表元素

可以定义用于JsonConvert.PopulateObject方法的自定义“list-merge”startegy吗?

例:

我有两个型号:

 class Parent { public Guid Uuid { get; set; } public string Name { get; set; } public List 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属性),因此调用JsonConvert.PopulateObject对象从我的初始JSON反序列化,带有更新JSON(它包含仅更改每个对象的值+ Uuids)结果仅更新由Uuid(在我的情况下更新Peter的分数)的更新JSON中包含的列表元素,并且更新JSON中未包含的元素保持不变。

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

您可以创建自己的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(); 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 { #region IEqualityComparer Members bool IEqualityComparer.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.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 。 拥有它可以更容易地找到关键参数,并且还可以确保,如果key参数具有[JsonProperty(name)]属性, [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硬编码,并利用List实现IListIList的事实。 如果您的集合不是List但仍实现IList ,则以下内容应该有效:

 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 error", ex); } } object ReadJsonGeneric(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer, JsonProperty keyProperty) { 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(); 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(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 { #region IEqualityComparer Members bool IEqualityComparer.Equals(object x, object y) { return object.Equals(x, y); } int IEqualityComparer.GetHashCode(object obj) { if (obj == null) return 0; return obj.GetHashCode(); } #endregion } } public static class TypeExtensions { public static IEnumerable 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 GetIListItemTypes(this Type type) { foreach (Type intType in type.GetInterfacesAndSelf()) { if (intType.IsGenericType && intType.GetGenericTypeDefinition() == typeof(IList<>)) { yield return intType.GetGenericArguments()[0]; } } } } 

请注意,对于数组没有实现合并,因为它们不可resize。