Json.Net PopulateObject - update list elements based on ID(Json.Net PopulateObject - 根据 ID 更新列表元素)
问题描述
是否可以定义用于 JsonConvert.PopulateObject
方法的自定义列表合并"启动?
It is possible to define a custom "list-merge" startegy used for the JsonConvert.PopulateObject
method?
示例:
我有两个模型:
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:
and my update JSON:
{
"Uuid":"cf82b1fd-1ca0-4125-9ea2-43d1d71c9bed",
"Childs":[
{
"Uuid":"fe7837e0-9960-4c45-b5ab-4e4658c08ccd",
"Score":170
}
]
}
我只需要指定一个模型属性(按属性)用于匹配列表项(在我的例子中是 Child 的 Uuid
属性),因此调用 JsonConvert.PopulateObject使用更新 JSON(它仅包含每个对象的更改值 + Uuid)从我的初始 JSON 反序列化的对象上的 code> 结果仅更新由 Uuid 生成的更新 JSON 中包含的列表元素(在我的情况下更新彼得的分数)和更新 JSON 中未包含的元素保持不变.
All I need is to specify a model property (by attribute) used for matching list items (in my case the Uuid
property of Child), so calling the JsonConvert.PopulateObject
on the object deserialized from my initial JSON with a update JSON (it contains ONLY changed values + Uuids for every object) results to update only list elements contained in the update JSON macthed by Uuid (in my case update a Peter's score) and elements not contained in the update JSON leave without change.
我正在寻找一些通用的解决方案 - 我需要将它应用于具有大量嵌套列表的大型 JSON(但每个模型都有一些独特的属性).所以我需要在匹配的列表项上递归调用 PopulateObject
.
I'm searching for some universal solution - I need to apply it on large JSONs with a lot of nested lists (but every model has some unique property). So I need to recursively call PopulateObject
on matched list item.
推荐答案
您可以创建自己的 JsonConverter
实现了所需的合并逻辑.这是可能的,因为 JsonConverter.ReadJson
是传递了一个 existingValue
参数,该参数包含正在反序列化的属性的预先存在的内容.
You could create your own JsonConverter
that implements the required merge logic. This is possible because JsonConverter.ReadJson
is passed an existingValue
parameter that contains the pre-existing contents of the property being deserialized.
因此:
[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)]
属性,则替换名称受到尊重.
Notice that the converter needs to know the IContractResolver
currently in use. Having it makes finding the key parameter easier, and also ensures that, if the key parameter has a [JsonProperty(name)]
attribute, the replacement name is respected.
然后添加属性:
class Child
{
[JsonMergeKey]
[JsonProperty("Uuid")] // Replacement name for testing
public Guid UUID { get; set; }
public string Name { get; set; }
public int Score { get; set; }
}
并按如下方式使用转换器:
And use the converter as follows:
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 中的任何条目具有在现有列表中找不到的键,它们将被附加到列表中.
The converter assumes that the key parameter is always present in the JSON. Also, if any entries in the JSON being merged have keys that are not found in the existing list, they are appended to the list.
更新
原始转换器专门为 List<T>
,并利用 List
IList
的事实列表
.如果您的集合不是 List<T>
但仍实现 IList<T>
,则以下内容应该有效:
The original converter is specifically hardcoded for List<T>
, and takes advantage of the fact that List<T>
implements both IList<T>
and IList
. If your collection is not a List<T>
but still implements IList<T>
, the following should work:
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];
}
}
}
}
请注意,数组没有实现合并,因为它们不可调整大小.
Note that merging is not implemented for arrays since they are not resizable.
这篇关于Json.Net PopulateObject - 根据 ID 更新列表元素的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持编程学习网!
本文标题为:Json.Net PopulateObject - 根据 ID 更新列表元素
基础教程推荐
- SSE 浮点算术是否可重现? 2022-01-01
- c# Math.Sqrt 实现 2022-01-01
- 如何在 IDE 中获取 Xamarin Studio C# 输出? 2022-01-01
- 如何激活MC67中的红灯 2022-01-01
- 将 XML 转换为通用列表 2022-01-01
- 将 Office 安装到 Windows 容器 (servercore:ltsc2019) 失败,错误代码为 17002 2022-01-01
- MS Visual Studio .NET 的替代品 2022-01-01
- 有没有办法忽略 2GB 文件上传的 maxRequestLength 限制? 2022-01-01
- 为什么Flurl.Http DownloadFileAsync/Http客户端GetAsync需要 2022-09-30
- rabbitmq 的 REST API 2022-01-01