diff --git a/src/Microsoft.AspNetCore.JsonPatch/Adapters/ObjectAdapter.cs b/src/Microsoft.AspNetCore.JsonPatch/Adapters/ObjectAdapter.cs index 0ac2ebbe56..b1a487f088 100644 --- a/src/Microsoft.AspNetCore.JsonPatch/Adapters/ObjectAdapter.cs +++ b/src/Microsoft.AspNetCore.JsonPatch/Adapters/ObjectAdapter.cs @@ -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; + } } } diff --git a/src/Microsoft.AspNetCore.JsonPatch/Internal/ConversionResultProvider.cs b/src/Microsoft.AspNetCore.JsonPatch/Internal/ConversionResultProvider.cs index bdf4122394..71af0d27fc 100644 --- a/src/Microsoft.AspNetCore.JsonPatch/Internal/ConversionResultProvider.cs +++ b/src/Microsoft.AspNetCore.JsonPatch/Internal/ConversionResultProvider.cs @@ -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 + return typeInfo.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); + } + else + { + // reference types are always nullable + return true; + } + } } } diff --git a/src/Microsoft.AspNetCore.JsonPatch/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.JsonPatch/Properties/Resources.Designer.cs index ed567cf003..97c98c2b05 100644 --- a/src/Microsoft.AspNetCore.JsonPatch/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.JsonPatch/Properties/Resources.Designer.cs @@ -26,6 +26,22 @@ namespace Microsoft.AspNetCore.JsonPatch return string.Format(CultureInfo.CurrentCulture, GetString("CannotDeterminePropertyType"), p0); } + /// + /// The property at '{0}' could not be copied. + /// + internal static string CannotCannotCopyProperty + { + get { return GetString("CannotCannotCopyProperty"); } + } + + /// + /// The property at '{0}' could not be copied. + /// + internal static string FormatCannotCopyProperty(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("CannotCannotCopyProperty"), p0); + } + /// /// The '{0}' operation at path '{1}' could not be performed. /// diff --git a/src/Microsoft.AspNetCore.JsonPatch/Resources.resx b/src/Microsoft.AspNetCore.JsonPatch/Resources.resx index 7381ad02fc..87ed5c0a17 100644 --- a/src/Microsoft.AspNetCore.JsonPatch/Resources.resx +++ b/src/Microsoft.AspNetCore.JsonPatch/Resources.resx @@ -117,6 +117,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + The property at '{0}' could not be copied. + The type of the property at path '{0}' could not be determined. diff --git a/test/Microsoft.AspNetCore.JsonPatch.Test/InheritedDTO.cs b/test/Microsoft.AspNetCore.JsonPatch.Test/InheritedDTO.cs new file mode 100644 index 0000000000..a784a6304b --- /dev/null +++ b/test/Microsoft.AspNetCore.JsonPatch.Test/InheritedDTO.cs @@ -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; } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.JsonPatch.Test/ListAdapterTest.cs b/test/Microsoft.AspNetCore.JsonPatch.Test/ListAdapterTest.cs index 799f6e9e89..077a179a40 100644 --- a/test/Microsoft.AspNetCore.JsonPatch.Test/ListAdapterTest.cs +++ b/test/Microsoft.AspNetCore.JsonPatch.Test/ListAdapterTest.cs @@ -174,6 +174,27 @@ namespace Microsoft.AspNetCore.JsonPatch.Internal Assert.Equal(new List() { "James", "Mike", null }, targetObject); } + [Fact] + public void Add_CompatibleTypeWorks() + { + // Arrange + var sDto = new SimpleDTO(); + var iDto = new InheritedDTO(); + var resolver = new Mock(MockBehavior.Strict); + var targetObject = (new List() { 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() { 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 AddingKeepsObjectReferenceData { + get { + var sDto1 = new SimpleDTO(); + var sDto2 = new SimpleDTO(); + var sDto3 = new SimpleDTO(); + return new TheoryData() + { + { + new List() { }, + sDto1, + "-", + new List() { sDto1 } + }, + { + new List() { sDto1, sDto2 }, + sDto3, + "-", + new List() { sDto1, sDto2, sDto3 } + }, + { + new List() { sDto1, sDto2 }, + sDto3, + "0", + new List() { sDto3, sDto1, sDto2 } + }, + { + new List() { sDto1, sDto2 }, + sDto3, + "1", + new List() { sDto1, sDto3, sDto2 } + } + }; + } + } + + [Theory] + [MemberData(nameof(AddingKeepsObjectReferenceData))] + public void Add_KeepsObjectReference(IList targetObject, object value, string position, IList expected) + { + // Arrange + var resolver = new Mock(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")] diff --git a/test/Microsoft.AspNetCore.JsonPatch.Test/NestedObjectTests.cs b/test/Microsoft.AspNetCore.JsonPatch.Test/NestedObjectTests.cs index cc2e990997..ece1a6e3d6 100644 --- a/test/Microsoft.AspNetCore.JsonPatch.Test/NestedObjectTests.cs +++ b/test/Microsoft.AspNetCore.JsonPatch.Test/NestedObjectTests.cs @@ -1699,6 +1699,81 @@ namespace Microsoft.AspNetCore.JsonPatch Assert.Equal(new List() { 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(); + patchDoc.Copy(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(); + patchDoc.Copy(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(); + patchDoc.Copy(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(); + patchDoc.Move(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(); + patchDoc.Move(o => o.InheritedDTO, o => o.SimpleDTO); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(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() { 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() { + sDto1, + sDto2, + sDto3 + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Move(o => o.SimpleDTOList, 0, o => o.SimpleDTOList, 1); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 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() { + sDto1, + sDto2, + sDto3 + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Move(o => o.SimpleDTOList, 0, o => o.SimpleDTOList, 1); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 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() { diff --git a/test/Microsoft.AspNetCore.JsonPatch.Test/SimpleDTOWithNestedDTO.cs b/test/Microsoft.AspNetCore.JsonPatch.Test/SimpleDTOWithNestedDTO.cs index 879d820277..de089eb25f 100644 --- a/test/Microsoft.AspNetCore.JsonPatch.Test/SimpleDTOWithNestedDTO.cs +++ b/test/Microsoft.AspNetCore.JsonPatch.Test/SimpleDTOWithNestedDTO.cs @@ -13,6 +13,8 @@ namespace Microsoft.AspNetCore.JsonPatch public SimpleDTO SimpleDTO { get; set; } + public InheritedDTO InheritedDTO { get; set; } + public List SimpleDTOList { get; set; } public IList 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(); } }