2016-01-06 2 views
3

Можно определить пользовательский метод «list-merge» startegy, используемый для метода JsonConvert.PopulateObject?Json.Net PopulateObject - элементы списка обновления на основе ID

Пример:

У меня есть две модели:

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

Все, что мне нужно, это указать свойство модели (по атрибуту), используемое для сопоставления элементов списка (в моем случае это свойство Uuid Child), поэтому вызываем JsonConvert.PopulateObject на объект, десериализованный из моего исходного JSON, с обновлением JSON (он содержит ТОЛЬКО измененные значения + Uuids для каждого объекта) приводит к обновлению только элементов списка, содержащихся в обновлении JSON с учетом Uuid (в моем случае обновляет оценку Peter), а элементы, не содержащиеся в обновлении JSON, остаются без изменений.

Я ищу какое-то универсальное решение - мне нужно применить его на больших JSON с большим количеством вложенных списков (но каждая модель имеет уникальное свойство). Поэтому мне нужно рекурсивно называть PopulateObject на элемент совпадающего списка.

ответ

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 имеют ключи, которые не найдены в существующем списке, они добавляются в список.

Update

оригинальный преобразователь специально для зашиты 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]; 
      } 
     } 
    } 
} 

Обратите внимание, что слияние не реализована для массивов, так как они не являются изменяемыми.

+0

Я надеялся, что это было решение аналогичной проблемы, с которой я столкнулся: http://stackoverflow.com/questions/34839939/deserializing-objects-stored-in-json-using-a-custom-idefaultcontractresolver-res, но даже используя это и задавая имя в качестве ключа слияния, приводит к заселению данных, – Wobbles

Смежные вопросы