Json.Net PopulateObject - 根据 ID 更新列表元素 [英] Json.Net PopulateObject - update list elements based on ID

查看:19
本文介绍了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 结果仅更新由 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 更新列表元素的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆