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

时间:2016-01-06 08:50:19

标签: c# json json.net

可以定义自定义" list-merge" startegy用于JsonConvert.PopulateObject方法?

示例:

我有两种模式:

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:

{  
   "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

1 个答案:

答案 0 :(得分:3)

您可以创建自己的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<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)]属性,则会更改替换名称。

然后添加属性:

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<T>进行了硬编码,并利用List<T>同时实现IList<T>IList这一事实。如果您的收藏集不是List<T>但仍然实施IList<T>,则以下内容应该有效:

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

请注意,数组没有实现合并,因为它们不可调整大小。