Fix for #2280 - Cannot do an "add" patch to IList<Anything>

Added unit tests
This commit is contained in:
Kirthi Krishnamraju 2015-04-09 15:42:25 -07:00
parent e48565dcd8
commit bf712263fc
6 changed files with 296 additions and 149 deletions

View File

@ -3,10 +3,13 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using Microsoft.AspNet.JsonPatch.Exceptions;
using Microsoft.AspNet.JsonPatch.Helpers;
using Microsoft.AspNet.JsonPatch.Operations;
using Microsoft.Framework.Internal;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace Microsoft.AspNet.JsonPatch.Adapters
@ -124,17 +127,16 @@ namespace Microsoft.AspNet.JsonPatch.Adapters
}
else
{
positionAsInteger = PropertyHelpers.GetNumericEnd(path);
positionAsInteger = GetNumericEnd(path);
if (positionAsInteger > -1)
{
actualPathToProperty = path.Substring(0,
path.IndexOf('/' + positionAsInteger.ToString()));
path.LastIndexOf('/' + positionAsInteger.ToString()));
}
}
var patchProperty = PropertyHelpers
.FindPropertyAndParent(objectToApplyTo, actualPathToProperty, ContractResolver);
var patchProperty = FindPropertyAndParent(objectToApplyTo, actualPathToProperty);
// does property at path exist?
if (!CheckIfPropertyExists(patchProperty, objectToApplyTo, operationToReport, path))
@ -148,13 +150,12 @@ namespace Microsoft.AspNet.JsonPatch.Adapters
if (appendList || positionAsInteger > -1)
{
// what if it's an array but there's no position??
if (IsNonStringArray(patchProperty))
if (IsNonStringArray(patchProperty.Property.PropertyType))
{
// now, get the generic type of the enumerable
var genericTypeOfArray = PropertyHelpers.GetEnumerableType(
patchProperty.Property.PropertyType);
// now, get the generic type of the IList<> from Property type.
var genericTypeOfArray = GetIListType(patchProperty.Property.PropertyType);
var conversionResult = PropertyHelpers.ConvertToActualType(genericTypeOfArray, value);
var conversionResult = ConvertToActualType(genericTypeOfArray, value);
if (!CheckIfPropertyCanBeSet(conversionResult, objectToApplyTo, operationToReport, path))
{
@ -198,7 +199,7 @@ namespace Microsoft.AspNet.JsonPatch.Adapters
}
else
{
var conversionResultTuple = PropertyHelpers.ConvertToActualType(
var conversionResultTuple = ConvertToActualType(
patchProperty.Property.PropertyType,
value);
@ -244,7 +245,7 @@ namespace Microsoft.AspNet.JsonPatch.Adapters
var positionAsInteger = -1;
var actualFromProperty = operation.from;
positionAsInteger = PropertyHelpers.GetNumericEnd(operation.from);
positionAsInteger = GetNumericEnd(operation.from);
if (positionAsInteger > -1)
{
@ -252,8 +253,7 @@ namespace Microsoft.AspNet.JsonPatch.Adapters
operation.from.IndexOf('/' + positionAsInteger.ToString()));
}
var patchProperty = PropertyHelpers
.FindPropertyAndParent(objectToApplyTo, actualFromProperty, ContractResolver);
var patchProperty = FindPropertyAndParent(objectToApplyTo, actualFromProperty);
// does property at from exist?
if (!CheckIfPropertyExists(patchProperty, objectToApplyTo, operation, operation.from))
@ -265,10 +265,10 @@ namespace Microsoft.AspNet.JsonPatch.Adapters
// the path must end with "/position" or "/-", which we already determined before.
if (positionAsInteger > -1)
{
if (IsNonStringArray(patchProperty))
if (IsNonStringArray(patchProperty.Property.PropertyType))
{
// now, get the generic type of the enumerable
var genericTypeOfArray = PropertyHelpers.GetEnumerableType(patchProperty.Property.PropertyType);
// now, get the generic type of the IList<> from Property type.
var genericTypeOfArray = GetIListType(patchProperty.Property.PropertyType);
// get value (it can be cast, we just checked that)
var array = (IList)patchProperty.Property.ValueProvider.GetValue(patchProperty.Parent);
@ -345,7 +345,7 @@ namespace Microsoft.AspNet.JsonPatch.Adapters
}
else
{
positionAsInteger = PropertyHelpers.GetNumericEnd(path);
positionAsInteger = GetNumericEnd(path);
if (positionAsInteger > -1)
{
@ -354,8 +354,7 @@ namespace Microsoft.AspNet.JsonPatch.Adapters
}
}
var patchProperty = PropertyHelpers
.FindPropertyAndParent(objectToApplyTo, actualPathToProperty, ContractResolver);
var patchProperty = FindPropertyAndParent(objectToApplyTo, actualPathToProperty);
// does the target location exist?
if (!CheckIfPropertyExists(patchProperty, objectToApplyTo, operationToReport, path))
@ -369,10 +368,10 @@ namespace Microsoft.AspNet.JsonPatch.Adapters
if (removeFromList || positionAsInteger > -1)
{
// what if it's an array but there's no position??
if (IsNonStringArray(patchProperty))
if (IsNonStringArray(patchProperty.Property.PropertyType))
{
// now, get the generic type of the enumerable
var genericTypeOfArray = PropertyHelpers.GetEnumerableType(patchProperty.Property.PropertyType);
// now, get the generic type of the IList<> from Property type.
var genericTypeOfArray = GetIListType(patchProperty.Property.PropertyType);
// get value (it can be cast, we just checked that)
var array = (IList)patchProperty.Property.ValueProvider.GetValue(patchProperty.Parent);
@ -480,7 +479,7 @@ namespace Microsoft.AspNet.JsonPatch.Adapters
var positionInPathAsInteger = -1;
var actualPathProperty = operation.path;
positionInPathAsInteger = PropertyHelpers.GetNumericEnd(operation.path);
positionInPathAsInteger = GetNumericEnd(operation.path);
if (positionInPathAsInteger > -1)
{
@ -488,8 +487,7 @@ namespace Microsoft.AspNet.JsonPatch.Adapters
operation.path.IndexOf('/' + positionInPathAsInteger.ToString()));
}
var patchProperty = PropertyHelpers
.FindPropertyAndParent(objectToApplyTo, actualPathProperty, ContractResolver);
var patchProperty = FindPropertyAndParent(objectToApplyTo, actualPathProperty);
// does property at path exist?
if (!CheckIfPropertyExists(patchProperty, objectToApplyTo, operation, operation.path))
@ -504,11 +502,10 @@ namespace Microsoft.AspNet.JsonPatch.Adapters
// the path must end with "/position" or "/-", which we already determined before.
if (positionInPathAsInteger > -1)
{
if (IsNonStringArray(patchProperty))
if (IsNonStringArray(patchProperty.Property.PropertyType))
{
// now, get the generic type of the enumerable
typeOfFinalPropertyAtPathLocation = PropertyHelpers
.GetEnumerableType(patchProperty.Property.PropertyType);
// now, get the generic type of the IList<> from Property type.
typeOfFinalPropertyAtPathLocation = GetIListType(patchProperty.Property.PropertyType);
// get value (it can be cast, we just checked that)
var array = (IList)patchProperty.Property.ValueProvider.GetValue(patchProperty.Parent);
@ -542,9 +539,7 @@ namespace Microsoft.AspNet.JsonPatch.Adapters
typeOfFinalPropertyAtPathLocation = patchProperty.Property.PropertyType;
}
var conversionResultTuple = PropertyHelpers.ConvertToActualType(
typeOfFinalPropertyAtPathLocation,
operation.value);
var conversionResultTuple = ConvertToActualType(typeOfFinalPropertyAtPathLocation, operation.value);
// Is conversion successful
if (!CheckIfPropertyCanBeSet(conversionResultTuple, objectToApplyTo, operation, operation.path))
@ -610,7 +605,7 @@ namespace Microsoft.AspNet.JsonPatch.Adapters
var positionAsInteger = -1;
var actualFromProperty = operation.from;
positionAsInteger = PropertyHelpers.GetNumericEnd(operation.from);
positionAsInteger = GetNumericEnd(operation.from);
if (positionAsInteger > -1)
{
@ -618,8 +613,7 @@ namespace Microsoft.AspNet.JsonPatch.Adapters
operation.from.IndexOf('/' + positionAsInteger.ToString()));
}
var patchProperty = PropertyHelpers
.FindPropertyAndParent(objectToApplyTo, actualFromProperty, ContractResolver);
var patchProperty = FindPropertyAndParent(objectToApplyTo, actualFromProperty);
// does property at from exist?
if (!CheckIfPropertyExists(patchProperty, objectToApplyTo, operation, operation.from))
@ -632,10 +626,10 @@ namespace Microsoft.AspNet.JsonPatch.Adapters
// the path must end with "/position" or "/-", which we already determined before.
if (positionAsInteger > -1)
{
if (IsNonStringArray(patchProperty))
if (IsNonStringArray(patchProperty.Property.PropertyType))
{
// now, get the generic type of the enumerable
var genericTypeOfArray = PropertyHelpers.GetEnumerableType(patchProperty.Property.PropertyType);
// now, get the generic type of the IList<> from Property type.
var genericTypeOfArray = GetIListType(patchProperty.Property.PropertyType);
// get value (it can be cast, we just checked that)
var array = (IList)patchProperty.Property.ValueProvider.GetValue(patchProperty.Parent);
@ -702,11 +696,14 @@ namespace Microsoft.AspNet.JsonPatch.Adapters
return true;
}
private bool IsNonStringArray(JsonPatchProperty patchProperty)
private bool IsNonStringArray(Type type)
{
return !(patchProperty.Property.PropertyType == typeof(string))
&& typeof(IList).GetTypeInfo().IsAssignableFrom(
patchProperty.Property.PropertyType.GetTypeInfo());
if (GetIListType(type) != null)
{
return true;
}
return (!(type == typeof(string)) && typeof(IList).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo()));
}
private bool CheckIfPropertyCanBeSet(
@ -739,5 +736,111 @@ namespace Microsoft.AspNet.JsonPatch.Adapters
throw new JsonPatchException<T>(jsonPatchError);
}
}
private JsonPatchProperty FindPropertyAndParent(
object targetObject,
string propertyPath)
{
try
{
var splitPath = propertyPath.Split('/');
// skip the first one if it's empty
var startIndex = (string.IsNullOrWhiteSpace(splitPath[0]) ? 1 : 0);
for (int i = startIndex; i < splitPath.Length; i++)
{
var jsonContract = (JsonObjectContract)ContractResolver.ResolveContract(targetObject.GetType());
foreach (var property in jsonContract.Properties)
{
if (string.Equals(property.PropertyName, splitPath[i], StringComparison.OrdinalIgnoreCase))
{
if (i == (splitPath.Length - 1))
{
return new JsonPatchProperty(property, targetObject);
}
else
{
targetObject = property.ValueProvider.GetValue(targetObject);
// if property is of IList type then get the array index from splitPath and get the
// object at the indexed position from the list.
if (GetIListType(property.PropertyType) != null)
{
var index = int.Parse(splitPath[++i]);
targetObject = ((IList)targetObject)[index];
}
}
break;
}
}
}
return null;
}
catch (Exception)
{
// will result in JsonPatchException in calling class, as expected
return null;
}
}
private ConversionResult ConvertToActualType(Type propertyType, object value)
{
try
{
var o = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(value), propertyType);
return new ConversionResult(true, o);
}
catch (Exception)
{
return new ConversionResult(false, null);
}
}
private Type GetIListType([NotNull] Type type)
{
if (IsGenericListType(type))
{
return type.GetGenericArguments()[0];
}
foreach (Type interfaceType in type.GetTypeInfo().ImplementedInterfaces)
{
if (IsGenericListType(interfaceType))
{
return interfaceType.GetGenericArguments()[0];
}
}
return null;
}
private bool IsGenericListType([NotNull] Type type)
{
if (type.GetTypeInfo().IsGenericType &&
type.GetGenericTypeDefinition() == typeof(IList<>))
{
return true;
}
return false;
}
private int GetNumericEnd(string path)
{
var possibleIndex = path.Substring(path.LastIndexOf("/") + 1);
var castedIndex = -1;
if (int.TryParse(possibleIndex, out castedIndex))
{
return castedIndex;
}
return -1;
}
}
}

View File

@ -1,107 +0,0 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace Microsoft.AspNet.JsonPatch.Helpers
{
internal static class PropertyHelpers
{
public static JsonPatchProperty FindPropertyAndParent(
object targetObject,
string propertyPath,
IContractResolver contractResolver)
{
try
{
var splitPath = propertyPath.Split('/');
// skip the first one if it's empty
var startIndex = (string.IsNullOrWhiteSpace(splitPath[0]) ? 1 : 0);
for (int i = startIndex; i < splitPath.Length; i++)
{
var jsonContract = (JsonObjectContract)contractResolver.ResolveContract(targetObject.GetType());
foreach (var property in jsonContract.Properties)
{
if (string.Equals(property.PropertyName, splitPath[i], StringComparison.OrdinalIgnoreCase))
{
if (i == (splitPath.Length - 1))
{
return new JsonPatchProperty(property, targetObject);
}
else
{
targetObject = property.ValueProvider.GetValue(targetObject);
// if property is of IList type then get the array index from splitPath and get the
// object at the indexed position from the list.
if (typeof(IList).GetTypeInfo().IsAssignableFrom(property.PropertyType.GetTypeInfo()))
{
var index = int.Parse(splitPath[++i]);
targetObject = ((IList)targetObject)[index];
}
}
break;
}
}
}
return null;
}
catch (Exception)
{
// will result in JsonPatchException in calling class, as expected
return null;
}
}
internal static ConversionResult ConvertToActualType(Type propertyType, object value)
{
try
{
var o = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(value), propertyType);
return new ConversionResult(true, o);
}
catch (Exception)
{
return new ConversionResult(false, null);
}
}
internal static Type GetEnumerableType(Type type)
{
if (type == null) throw new ArgumentNullException();
foreach (Type interfaceType in type.GetInterfaces())
{
if (interfaceType.GetTypeInfo().IsGenericType &&
interfaceType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{
return interfaceType.GetGenericArguments()[0];
}
}
return null;
}
internal static int GetNumericEnd(string path)
{
var possibleIndex = path.Substring(path.LastIndexOf("/") + 1);
var castedIndex = -1;
if (int.TryParse(possibleIndex, out castedIndex))
{
return castedIndex;
}
return -1;
}
}
}

View File

@ -201,6 +201,110 @@ namespace Microsoft.AspNet.JsonPatch.Test
Assert.Equal(new List<int>() { 4, 1, 2, 3 }, doc.SimpleDTO.IntegerList);
}
[Fact]
public void AddToIntegerIList()
{
// Arrange
var doc = new SimpleDTOWithNestedDTO()
{
SimpleDTO = new SimpleDTO()
{
IntegerIList = new List<int>() { 1, 2, 3 }
}
};
// create patch
var patchDoc = new JsonPatchDocument<SimpleDTOWithNestedDTO>();
patchDoc.Add<int>(o => (List<int>)o.SimpleDTO.IntegerIList, 4, 0);
// Act
patchDoc.ApplyTo(doc);
// Assert
Assert.Equal(new List<int>() { 4, 1, 2, 3 }, doc.SimpleDTO.IntegerIList);
}
[Fact]
public void AddToIntegerIListWithSerialization()
{
// Arrange
var doc = new SimpleDTOWithNestedDTO()
{
SimpleDTO = new SimpleDTO()
{
IntegerIList = new List<int>() { 1, 2, 3 }
}
};
// create patch
var patchDoc = new JsonPatchDocument<SimpleDTOWithNestedDTO>();
patchDoc.Add<int>(o => (List<int>)o.SimpleDTO.IntegerIList, 4, 0);
var serialized = JsonConvert.SerializeObject(patchDoc);
var deserialized = JsonConvert.DeserializeObject<JsonPatchDocument<SimpleDTOWithNestedDTO>>(serialized);
// Act
deserialized.ApplyTo(doc);
// Assert
Assert.Equal(new List<int>() { 4, 1, 2, 3 }, doc.SimpleDTO.IntegerIList);
}
[Fact]
public void AddToNestedIntegerIList()
{
// Arrange
var doc = new SimpleDTOWithNestedDTO()
{
SimpleDTOIList = new List<SimpleDTO>
{
new SimpleDTO
{
IntegerIList = new List<int>() { 1, 2, 3 }
}
}
};
// create patch
var patchDoc = new JsonPatchDocument<SimpleDTOWithNestedDTO>();
patchDoc.Add<int>(o => (List<int>)o.SimpleDTOIList[0].IntegerIList, 4, 0);
// Act
patchDoc.ApplyTo(doc);
// Assert
Assert.Equal(new List<int>() { 4, 1, 2, 3 }, doc.SimpleDTOIList[0].IntegerIList);
}
[Fact]
public void AddToNestedIntegerIListWithSerialization()
{
// Arrange
var doc = new SimpleDTOWithNestedDTO()
{
SimpleDTOIList = new List<SimpleDTO>
{
new SimpleDTO
{
IntegerIList = new List<int>() { 1, 2, 3 }
}
}
};
// create patch
var patchDoc = new JsonPatchDocument<SimpleDTOWithNestedDTO>();
patchDoc.Add<int>(o => (List<int>)o.SimpleDTOIList[0].IntegerIList, 4, 0);
var serialized = JsonConvert.SerializeObject(patchDoc);
var deserialized = JsonConvert.DeserializeObject<JsonPatchDocument<SimpleDTOWithNestedDTO>>(serialized);
// Act
deserialized.ApplyTo(doc);
// Assert
Assert.Equal(new List<int>() { 4, 1, 2, 3 }, doc.SimpleDTOIList[0].IntegerIList);
}
[Fact]
public void AddToComplextTypeListSpecifyIndex()
{

View File

@ -98,6 +98,49 @@ namespace Microsoft.AspNet.JsonPatch.Test
Assert.Equal(new List<int>() { 4, 1, 2, 3 }, doc.IntegerList);
}
[Fact]
public void AddToIntegerIList()
{
// Arrange
var doc = new SimpleDTO()
{
IntegerIList = new List<int>() { 1, 2, 3 }
};
// create patch
var patchDoc = new JsonPatchDocument<SimpleDTO>();
patchDoc.Add<int>(o => o.IntegerIList, 4, 0);
// Act
patchDoc.ApplyTo(doc);
// Assert
Assert.Equal(new List<int>() { 4, 1, 2, 3 }, doc.IntegerIList);
}
[Fact]
public void AddToIntegerIListWithSerialization()
{
// Arrange
var doc = new SimpleDTO()
{
IntegerIList = new List<int>() { 1, 2, 3 }
};
// create patch
var patchDoc = new JsonPatchDocument<SimpleDTO>();
patchDoc.Add<int>(o => o.IntegerIList, 4, 0);
var serialized = JsonConvert.SerializeObject(patchDoc);
var deserialized = JsonConvert.DeserializeObject<JsonPatchDocument<SimpleDTO>>(serialized);
// Act
deserialized.ApplyTo(doc);
// Assert
Assert.Equal(new List<int>() { 4, 1, 2, 3 }, doc.IntegerIList);
}
[Fact]
public void AddToListInvalidPositionTooLarge()
{

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections;
using System.Collections.Generic;
namespace Microsoft.AspNet.JsonPatch.Test
@ -9,6 +10,7 @@ namespace Microsoft.AspNet.JsonPatch.Test
public class SimpleDTO
{
public List<int> IntegerList { get; set; }
public IList<int> IntegerIList { get; set; }
public int IntegerValue { get; set; }
public string StringProperty { get; set; }
public string AnotherStringProperty { get; set; }

View File

@ -15,6 +15,8 @@ namespace Microsoft.AspNet.JsonPatch.Test
public List<SimpleDTO> SimpleDTOList { get; set; }
public IList<SimpleDTO> SimpleDTOIList { get; set; }
public SimpleDTOWithNestedDTO()
{
this.NestedDTO = new NestedDTO();