[Fixes #61] Move must keep object reference

This commit is contained in:
Thierry Fleury 2017-02-07 14:20:42 -08:00 committed by Kiran Challa
parent dcfbbab005
commit 42d0d40b36
8 changed files with 385 additions and 5 deletions

View File

@ -362,10 +362,21 @@ namespace Microsoft.AspNetCore.JsonPatch.Adapters
// Get value at 'from' location and add that value to the 'path' location
if (TryGetValue(operation.from, objectToApplyTo, operation, out propertyValue))
{
Add(operation.path,
propertyValue,
objectToApplyTo,
operation);
// Create deep copy
var copyResult = ConversionResultProvider.CopyTo(propertyValue, propertyValue.GetType());
if (copyResult.CanBeConverted)
{
Add(operation.path,
copyResult.ConvertedInstance,
objectToApplyTo,
operation);
}
else
{
var error = CreateOperationFailedError(objectToApplyTo, operation.path, operation, Resources.FormatCannotCopyProperty(operation.from));
ErrorReporter(error);
return;
}
}
}

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.Reflection;
using Newtonsoft.Json;
namespace Microsoft.AspNetCore.JsonPatch.Internal
@ -10,9 +11,44 @@ namespace Microsoft.AspNetCore.JsonPatch.Internal
{
public static ConversionResult ConvertTo(object value, Type typeToConvertTo)
{
if (value == null)
{
return new ConversionResult(IsNullableType(typeToConvertTo), null);
}
else if (typeToConvertTo.IsAssignableFrom(value.GetType()))
{
// No need to convert
return new ConversionResult(true, value);
}
else
{
try
{
var deserialized = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(value), typeToConvertTo);
return new ConversionResult(true, deserialized);
}
catch
{
return new ConversionResult(canBeConverted: false, convertedInstance: null);
}
}
}
public static ConversionResult CopyTo(object value, Type typeToConvertTo)
{
var targetType = typeToConvertTo;
if (value == null)
{
return new ConversionResult(IsNullableType(typeToConvertTo), null);
}
else if (typeToConvertTo.IsAssignableFrom(value.GetType()))
{
// Keep original type
targetType = value.GetType();
}
try
{
var deserialized = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(value), typeToConvertTo);
var deserialized = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(value), targetType);
return new ConversionResult(true, deserialized);
}
catch
@ -20,5 +56,20 @@ namespace Microsoft.AspNetCore.JsonPatch.Internal
return new ConversionResult(canBeConverted: false, convertedInstance: null);
}
}
private static bool IsNullableType(Type type)
{
var typeInfo = type.GetTypeInfo();
if (typeInfo.IsValueType)
{
// value types are only nullable if they are Nullable<T>
return typeInfo.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
}
else
{
// reference types are always nullable
return true;
}
}
}
}

View File

@ -26,6 +26,22 @@ namespace Microsoft.AspNetCore.JsonPatch
return string.Format(CultureInfo.CurrentCulture, GetString("CannotDeterminePropertyType"), p0);
}
/// <summary>
/// The property at '{0}' could not be copied.
/// </summary>
internal static string CannotCannotCopyProperty
{
get { return GetString("CannotCannotCopyProperty"); }
}
/// <summary>
/// The property at '{0}' could not be copied.
/// </summary>
internal static string FormatCannotCopyProperty(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("CannotCannotCopyProperty"), p0);
}
/// <summary>
/// The '{0}' operation at path '{1}' could not be performed.
/// </summary>

View File

@ -117,6 +117,9 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="CannotCopyProperty" xml:space="preserve">
<value>The property at '{0}' could not be copied.</value>
</data>
<data name="CannotDeterminePropertyType" xml:space="preserve">
<value>The type of the property at path '{0}' could not be determined.</value>
</data>

View File

@ -0,0 +1,10 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.JsonPatch
{
public class InheritedDTO : SimpleDTO
{
public string AdditionalStringProperty { get; set; }
}
}

View File

@ -174,6 +174,27 @@ namespace Microsoft.AspNetCore.JsonPatch.Internal
Assert.Equal(new List<string>() { "James", "Mike", null }, targetObject);
}
[Fact]
public void Add_CompatibleTypeWorks()
{
// Arrange
var sDto = new SimpleDTO();
var iDto = new InheritedDTO();
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
var targetObject = (new List<SimpleDTO>() { sDto });
var listAdapter = new ListAdapter();
string message = null;
// Act
var addStatus = listAdapter.TryAdd(targetObject, "-", resolver.Object, iDto, out message);
// Assert
Assert.True(addStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Equal(2, targetObject.Count);
Assert.Equal(new List<SimpleDTO>() { sDto, iDto }, targetObject);
}
[Fact]
public void Add_NonCompatibleType_Fails()
{
@ -244,6 +265,60 @@ namespace Microsoft.AspNetCore.JsonPatch.Internal
Assert.Equal(expected, targetObject);
}
public static TheoryData<IList, object, string, IList> AddingKeepsObjectReferenceData {
get {
var sDto1 = new SimpleDTO();
var sDto2 = new SimpleDTO();
var sDto3 = new SimpleDTO();
return new TheoryData<IList, object, string, IList>()
{
{
new List<SimpleDTO>() { },
sDto1,
"-",
new List<SimpleDTO>() { sDto1 }
},
{
new List<SimpleDTO>() { sDto1, sDto2 },
sDto3,
"-",
new List<SimpleDTO>() { sDto1, sDto2, sDto3 }
},
{
new List<SimpleDTO>() { sDto1, sDto2 },
sDto3,
"0",
new List<SimpleDTO>() { sDto3, sDto1, sDto2 }
},
{
new List<SimpleDTO>() { sDto1, sDto2 },
sDto3,
"1",
new List<SimpleDTO>() { sDto1, sDto3, sDto2 }
}
};
}
}
[Theory]
[MemberData(nameof(AddingKeepsObjectReferenceData))]
public void Add_KeepsObjectReference(IList targetObject, object value, string position, IList expected)
{
// Arrange
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
var listAdapter = new ListAdapter();
string message = null;
// Act
var addStatus = listAdapter.TryAdd(targetObject, position, resolver.Object, value, out message);
// Assert
Assert.True(addStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Equal(expected.Count, targetObject.Count);
Assert.Equal(expected, targetObject);
}
[Theory]
[InlineData(new int[] { }, "0")]
[InlineData(new[] { 10, 20 }, "-1")]

View File

@ -1699,6 +1699,81 @@ namespace Microsoft.AspNetCore.JsonPatch
Assert.Equal(new List<int>() { 1, 2, 3, 5 }, doc.SimpleDTO.IntegerList);
}
[Fact]
public void Copy_DeepClonesObject()
{
// Arrange
var doc = new SimpleDTOWithNestedDTO()
{
SimpleDTO = new SimpleDTO()
{
StringProperty = "A",
AnotherStringProperty = "B"
},
InheritedDTO = new InheritedDTO()
{
StringProperty = "C",
AnotherStringProperty = "D"
}
};
// create patch
var patchDoc = new JsonPatchDocument<SimpleDTOWithNestedDTO>();
patchDoc.Copy<SimpleDTO>(o => o.InheritedDTO, o => o.SimpleDTO);
// Act
patchDoc.ApplyTo(doc);
// Assert
Assert.Equal("C", doc.SimpleDTO.StringProperty);
Assert.Equal("D", doc.SimpleDTO.AnotherStringProperty);
Assert.Equal("C", doc.InheritedDTO.StringProperty);
Assert.Equal("D", doc.InheritedDTO.AnotherStringProperty);
Assert.NotSame(doc.SimpleDTO.StringProperty, doc.InheritedDTO.StringProperty);
}
[Fact]
public void Copy_KeepsObjectType()
{
// Arrange
var doc = new SimpleDTOWithNestedDTO()
{
SimpleDTO = new SimpleDTO(),
InheritedDTO = new InheritedDTO()
};
// create patch
var patchDoc = new JsonPatchDocument<SimpleDTOWithNestedDTO>();
patchDoc.Copy<SimpleDTO>(o => o.InheritedDTO, o => o.SimpleDTO);
// Act
patchDoc.ApplyTo(doc);
// Assert
Assert.Equal(typeof(InheritedDTO), doc.SimpleDTO.GetType());
}
[Fact]
public void Copy_BreaksObjectReference()
{
// Arrange
var doc = new SimpleDTOWithNestedDTO()
{
SimpleDTO = new SimpleDTO(),
InheritedDTO = new InheritedDTO()
};
// create patch
var patchDoc = new JsonPatchDocument<SimpleDTOWithNestedDTO>();
patchDoc.Copy<SimpleDTO>(o => o.InheritedDTO, o => o.SimpleDTO);
// Act
patchDoc.ApplyTo(doc);
// Assert
Assert.NotSame(doc.SimpleDTO, doc.InheritedDTO);
}
[Fact]
public void Move()
{
@ -1752,6 +1827,77 @@ namespace Microsoft.AspNetCore.JsonPatch
Assert.Equal(null, doc.SimpleDTO.StringProperty);
}
[Fact]
public void Move_KeepsObjectReference()
{
// Arrange
var sDto = new SimpleDTO()
{
StringProperty = "A",
AnotherStringProperty = "B"
};
var iDto = new InheritedDTO()
{
StringProperty = "C",
AnotherStringProperty = "D"
};
var doc = new SimpleDTOWithNestedDTO()
{
SimpleDTO = sDto,
InheritedDTO = iDto
};
// create patch
var patchDoc = new JsonPatchDocument<SimpleDTOWithNestedDTO>();
patchDoc.Move<SimpleDTO>(o => o.InheritedDTO, o => o.SimpleDTO);
// Act
patchDoc.ApplyTo(doc);
// Assert
Assert.Equal("C", doc.SimpleDTO.StringProperty);
Assert.Equal("D", doc.SimpleDTO.AnotherStringProperty);
Assert.Same(iDto, doc.SimpleDTO);
Assert.Equal(null, doc.InheritedDTO);
}
[Fact]
public void Move_KeepsObjectReferenceWithSerialization()
{
// Arrange
var sDto = new SimpleDTO()
{
StringProperty = "A",
AnotherStringProperty = "B"
};
var iDto = new InheritedDTO()
{
StringProperty = "C",
AnotherStringProperty = "D"
};
var doc = new SimpleDTOWithNestedDTO()
{
SimpleDTO = sDto,
InheritedDTO = iDto
};
// create patch
var patchDoc = new JsonPatchDocument<SimpleDTOWithNestedDTO>();
patchDoc.Move<SimpleDTO>(o => o.InheritedDTO, o => o.SimpleDTO);
var serialized = JsonConvert.SerializeObject(patchDoc);
var deserialized = JsonConvert.DeserializeObject<JsonPatchDocument<SimpleDTOWithNestedDTO>>(serialized);
// Act
deserialized.ApplyTo(doc);
// Assert
Assert.Equal("C", doc.SimpleDTO.StringProperty);
Assert.Equal("D", doc.SimpleDTO.AnotherStringProperty);
Assert.Same(iDto, doc.SimpleDTO);
Assert.Equal(null, doc.InheritedDTO);
}
[Fact]
public void MoveInList()
{
@ -1801,6 +1947,71 @@ namespace Microsoft.AspNetCore.JsonPatch
Assert.Equal(new List<int>() { 2, 1, 3 }, doc.SimpleDTO.IntegerList);
}
[Fact]
public void Move_KeepsObjectReferenceInList()
{
// Arrange
var sDto1 = new SimpleDTO() { IntegerValue = 1 };
var sDto2 = new SimpleDTO() { IntegerValue = 2 };
var sDto3 = new SimpleDTO() { IntegerValue = 3 };
var doc = new SimpleDTOWithNestedDTO()
{
SimpleDTOList = new List<SimpleDTO>() {
sDto1,
sDto2,
sDto3
}
};
// create patch
var patchDoc = new JsonPatchDocument<SimpleDTOWithNestedDTO>();
patchDoc.Move<SimpleDTO>(o => o.SimpleDTOList, 0, o => o.SimpleDTOList, 1);
// Act
patchDoc.ApplyTo(doc);
// Assert
Assert.Equal(new List<SimpleDTO>() { sDto2, sDto1, sDto3 }, doc.SimpleDTOList);
Assert.Equal(2, doc.SimpleDTOList[0].IntegerValue);
Assert.Equal(1, doc.SimpleDTOList[1].IntegerValue);
Assert.Same(sDto2, doc.SimpleDTOList[0]);
Assert.Same(sDto1, doc.SimpleDTOList[1]);
}
[Fact]
public void Move_KeepsObjectReferenceInListWithSerialization()
{
// Arrange
var sDto1 = new SimpleDTO() { IntegerValue = 1 };
var sDto2 = new SimpleDTO() { IntegerValue = 2 };
var sDto3 = new SimpleDTO() { IntegerValue = 3 };
var doc = new SimpleDTOWithNestedDTO()
{
SimpleDTOList = new List<SimpleDTO>() {
sDto1,
sDto2,
sDto3
}
};
// create patch
var patchDoc = new JsonPatchDocument<SimpleDTOWithNestedDTO>();
patchDoc.Move<SimpleDTO>(o => o.SimpleDTOList, 0, o => o.SimpleDTOList, 1);
var serialized = JsonConvert.SerializeObject(patchDoc);
var deserialized = JsonConvert.DeserializeObject<JsonPatchDocument<SimpleDTOWithNestedDTO>>(serialized);
// Act
patchDoc.ApplyTo(doc);
// Assert
Assert.Equal(new List<SimpleDTO>() { sDto2, sDto1, sDto3 }, doc.SimpleDTOList);
Assert.Equal(2, doc.SimpleDTOList[0].IntegerValue);
Assert.Equal(1, doc.SimpleDTOList[1].IntegerValue);
Assert.Same(sDto2, doc.SimpleDTOList[0]);
Assert.Same(sDto1, doc.SimpleDTOList[1]);
}
[Fact]
public void MoveFromListToEndOfList()
{

View File

@ -13,6 +13,8 @@ namespace Microsoft.AspNetCore.JsonPatch
public SimpleDTO SimpleDTO { get; set; }
public InheritedDTO InheritedDTO { get; set; }
public List<SimpleDTO> SimpleDTOList { get; set; }
public IList<SimpleDTO> SimpleDTOIList { get; set; }
@ -21,6 +23,7 @@ namespace Microsoft.AspNetCore.JsonPatch
{
this.NestedDTO = new NestedDTO();
this.SimpleDTO = new SimpleDTO();
this.InheritedDTO = new InheritedDTO();
this.SimpleDTOList = new List<SimpleDTO>();
}
}