Json.Net PopulateObject - ID-based update list items

Can I define a custom "list-merge" startegy used for a method JsonConvert.PopulateObject?

Example:

I have two models:

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

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

and my JSON update:

{  
   "Uuid":"cf82b1fd-1ca0-4125-9ea2-43d1d71c9bed",
   "Childs":[  
      {  
         "Uuid":"fe7837e0-9960-4c45-b5ab-4e4658c08ccd",
         "Score":170
      }
   ]
}

All I need to do is specify the model property (by attribute) used to map the list items (in my case, this is the UuidChild property ), so calling JsonConvert.PopulateObjectthe deserialized object from my original JSON using update JSON (it contains ONLY changed values ​​+ Uuids for each object) leads to updating only the list items contained in the JSON update taking into account Uuid (in my case, updates the Peter score), and the elements not contained in the JSON update remain unchanged.

- - JSON ( ). PopulateObject .

+4
1

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

, , .

+3

Source: https://habr.com/ru/post/1623044/


All Articles