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

查看:208
本文介绍了Json.Net PopulateObject - 根据ID更新列表元素的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

它可以定义用于 JsonConvert.PopulateObject 办法自定义列表合并startegy?

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:

My initial 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
      }
   ]
}

所有我需要的是指定一个模型属性(按属性)中使用匹配列表项(在我的情况儿童 UUID 属性),所以调用 JsonConvert.PopulateObject 的对象从我最初的JSON序列化与更新JSON(只包含更改的值+的UUID的每个对象)的结果来更新不包含在包含在UUID(在我的情况更新一个彼得的分数)macthed更新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.

我在寻找一些通用的解决方案 - 我需要用大量的嵌套列表中它适用于大JSONs(但每一个模型有一定的独特的属性)。所以,我需要递归调用 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(名称)] 属性,更换名字得到尊重。

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; }
}

和使用转换器如下:

        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.

更新

最初的转换器是专门硬编码了的 列表< T> ,并采取一个事实,即列表与LT优势; T> 同时实现了的IList< T> 的IList 。如果您的收藏不是列表< 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天全站免登陆