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
请注意,转换器需要知道当前使用的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
实现IList
和IList
的事实。 如果您的集合不是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。