diff --git a/src/Features/JsonPatch/src/Internal/ConversionResultProvider.cs b/src/Features/JsonPatch/src/Internal/ConversionResultProvider.cs index c2e14262306d..992d2bb8aa6a 100644 --- a/src/Features/JsonPatch/src/Internal/ConversionResultProvider.cs +++ b/src/Features/JsonPatch/src/Internal/ConversionResultProvider.cs @@ -4,6 +4,7 @@ using System; using System.Reflection; using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; namespace Microsoft.AspNetCore.JsonPatch.Internal { @@ -14,6 +15,11 @@ namespace Microsoft.AspNetCore.JsonPatch.Internal public static class ConversionResultProvider { public static ConversionResult ConvertTo(object value, Type typeToConvertTo) + { + return ConvertTo(value, typeToConvertTo, null); + } + + internal static ConversionResult ConvertTo(object value, Type typeToConvertTo, IContractResolver contractResolver) { if (value == null) { @@ -28,8 +34,20 @@ public static ConversionResult ConvertTo(object value, Type typeToConvertTo) { try { - var deserialized = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(value), typeToConvertTo); - return new ConversionResult(true, deserialized); + if (contractResolver == null) + { + var deserialized = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(value), typeToConvertTo); + return new ConversionResult(true, deserialized); + } + else + { + var serializerSettings = new JsonSerializerSettings() + { + ContractResolver = contractResolver + }; + var deserialized = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(value), typeToConvertTo, serializerSettings); + return new ConversionResult(true, deserialized); + } } catch { diff --git a/src/Features/JsonPatch/src/Internal/ListAdapter.cs b/src/Features/JsonPatch/src/Internal/ListAdapter.cs index a23dd7a4ad14..58c95580c8e1 100644 --- a/src/Features/JsonPatch/src/Internal/ListAdapter.cs +++ b/src/Features/JsonPatch/src/Internal/ListAdapter.cs @@ -36,7 +36,7 @@ public virtual bool TryAdd( return false; } - if (!TryConvertValue(value, typeArgument, segment, out var convertedValue, out errorMessage)) + if (!TryConvertValue(value, typeArgument, segment, contractResolver, out var convertedValue, out errorMessage)) { return false; } @@ -138,7 +138,7 @@ public virtual bool TryReplace( return false; } - if (!TryConvertValue(value, typeArgument, segment, out var convertedValue, out errorMessage)) + if (!TryConvertValue(value, typeArgument, segment, contractResolver, out var convertedValue, out errorMessage)) { return false; } @@ -175,7 +175,7 @@ public virtual bool TryTest( return false; } - if (!TryConvertValue(value, typeArgument, segment, out var convertedValue, out errorMessage)) + if (!TryConvertValue(value, typeArgument, segment, contractResolver, out var convertedValue, out errorMessage)) { return false; } @@ -235,7 +235,24 @@ protected virtual bool TryConvertValue( out object convertedValue, out string errorMessage) { - var conversionResult = ConversionResultProvider.ConvertTo(originalValue, listTypeArgument); + return TryConvertValue( + originalValue, + listTypeArgument, + segment, + null, + out convertedValue, + out errorMessage); + } + + protected virtual bool TryConvertValue( + object originalValue, + Type listTypeArgument, + string segment, + IContractResolver contractResolver, + out object convertedValue, + out string errorMessage) + { + var conversionResult = ConversionResultProvider.ConvertTo(originalValue, listTypeArgument, contractResolver); if (!conversionResult.CanBeConverted) { convertedValue = null; diff --git a/src/Features/JsonPatch/src/PublicAPI.Unshipped.txt b/src/Features/JsonPatch/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..0d62a86ecfa1 100644 --- a/src/Features/JsonPatch/src/PublicAPI.Unshipped.txt +++ b/src/Features/JsonPatch/src/PublicAPI.Unshipped.txt @@ -1 +1,2 @@ #nullable enable +~virtual Microsoft.AspNetCore.JsonPatch.Internal.ListAdapter.TryConvertValue(object originalValue, System.Type listTypeArgument, string segment, Newtonsoft.Json.Serialization.IContractResolver contractResolver, out object convertedValue, out string errorMessage) -> bool diff --git a/src/Features/JsonPatch/test/IntegrationTests/HeterogenousCollectionTests.cs b/src/Features/JsonPatch/test/IntegrationTests/HeterogenousCollectionTests.cs new file mode 100644 index 000000000000..786a76d74ad3 --- /dev/null +++ b/src/Features/JsonPatch/test/IntegrationTests/HeterogenousCollectionTests.cs @@ -0,0 +1,103 @@ +// 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. + +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; +using Xunit; + +namespace Microsoft.AspNetCore.JsonPatch.IntegrationTests +{ + public class HeterogenousCollectionTests + { + [Fact] + public void AddItemToList() + { + // Arrange + var targetObject = new Canvas() + { + Items = new List() + }; + + var circleJObject = JObject.Parse(@"{ + Type: 'Circle', + ShapeProperty: 'Shape property', + CircleProperty: 'Circle property' + }"); + + var patchDocument = new JsonPatchDocument + { + ContractResolver = new CanvasContractResolver() + }; + + patchDocument.Add("/Items/-", circleJObject); + + // Act + patchDocument.ApplyTo(targetObject); + + // Assert + var circle = targetObject.Items[0] as Circle; + Assert.NotNull(circle); + Assert.Equal("Shape property", circle.ShapeProperty); + Assert.Equal("Circle property", circle.CircleProperty); + } + } + + public class CanvasContractResolver : DefaultContractResolver + { + protected override JsonConverter ResolveContractConverter(Type objectType) + { + if (objectType == typeof(Shape)) + { + return new ShapeJsonConverter(); + } + + return base.ResolveContractConverter(objectType); + } + } + + public class ShapeJsonConverter : CustomCreationConverter + { + private const string TypeProperty = "Type"; + + public override bool CanRead => true; + + public override Shape Create(Type objectType) + { + throw new NotImplementedException(); + } + + public override object ReadJson( + JsonReader reader, + Type objectType, + object existingValue, + JsonSerializer serializer) + { + var jObject = JObject.Load(reader); + + var target = CreateShape(jObject); + serializer.Populate(jObject.CreateReader(), target); + + return target; + } + + private Shape CreateShape(JObject jObject) + { + var typeProperty = jObject.GetValue(TypeProperty).ToString(); + + switch (typeProperty) + { + case "Circle": + return new Circle(); + + case "Rectangle": + return new Rectangle(); + } + + throw new NotSupportedException(); + } + } +} diff --git a/src/Features/JsonPatch/test/Internal/ListAdapterTest.cs b/src/Features/JsonPatch/test/Internal/ListAdapterTest.cs index 65c87bab1830..822acd13f45c 100644 --- a/src/Features/JsonPatch/test/Internal/ListAdapterTest.cs +++ b/src/Features/JsonPatch/test/Internal/ListAdapterTest.cs @@ -4,7 +4,6 @@ using System.Collections; using System.Collections.Generic; using System.Globalization; -using Moq; using Newtonsoft.Json.Serialization; using Xunit; @@ -16,12 +15,12 @@ public class ListAdapterTest public void Patch_OnArrayObject_Fails() { // Arrange - var resolver = new Mock(MockBehavior.Strict); + var resolver = new DefaultContractResolver(); var targetObject = new[] { 20, 30 }; var listAdapter = new ListAdapter(); // Act - var addStatus = listAdapter.TryAdd(targetObject, "0", resolver.Object, "40", out var message); + var addStatus = listAdapter.TryAdd(targetObject, "0", resolver, "40", out var message); // Assert Assert.False(addStatus); @@ -32,14 +31,14 @@ public void Patch_OnArrayObject_Fails() public void Patch_OnNonGenericListObject_Fails() { // Arrange - var resolver = new Mock(MockBehavior.Strict); + var resolver = new DefaultContractResolver(); var targetObject = new ArrayList(); targetObject.Add(20); targetObject.Add(30); var listAdapter = new ListAdapter(); // Act - var addStatus = listAdapter.TryAdd(targetObject, "-", resolver.Object, "40", out var message); + var addStatus = listAdapter.TryAdd(targetObject, "-", resolver, "40", out var message); // Assert Assert.False(addStatus); @@ -50,13 +49,13 @@ public void Patch_OnNonGenericListObject_Fails() public void Add_WithIndexSameAsNumberOfElements_Works() { // Arrange - var resolver = new Mock(MockBehavior.Strict); + var resolver = new DefaultContractResolver(); var targetObject = new List() { "James", "Mike" }; var listAdapter = new ListAdapter(); var position = targetObject.Count.ToString(CultureInfo.InvariantCulture); // Act - var addStatus = listAdapter.TryAdd(targetObject, position, resolver.Object, "Rob", out var message); + var addStatus = listAdapter.TryAdd(targetObject, position, resolver, "Rob", out var message); // Assert Assert.Null(message); @@ -72,12 +71,12 @@ public void Add_WithIndexSameAsNumberOfElements_Works() public void Add_WithOutOfBoundsIndex_Fails(string position) { // Arrange - var resolver = new Mock(MockBehavior.Strict); + var resolver = new DefaultContractResolver(); var targetObject = new List() { "James", "Mike" }; var listAdapter = new ListAdapter(); // Act - var addStatus = listAdapter.TryAdd(targetObject, position, resolver.Object, "40", out var message); + var addStatus = listAdapter.TryAdd(targetObject, position, resolver, "40", out var message); // Assert Assert.False(addStatus); @@ -90,12 +89,12 @@ public void Add_WithOutOfBoundsIndex_Fails(string position) public void Patch_WithInvalidPositionFormat_Fails(string position) { // Arrange - var resolver = new Mock(MockBehavior.Strict); + var resolver = new DefaultContractResolver(); var targetObject = new List() { "James", "Mike" }; var listAdapter = new ListAdapter(); // Act - var addStatus = listAdapter.TryAdd(targetObject, position, resolver.Object, "40", out var message); + var addStatus = listAdapter.TryAdd(targetObject, position, resolver, "40", out var message); // Assert Assert.False(addStatus); @@ -125,11 +124,11 @@ public static TheoryData, List> AppendAtEndOfListData public void Add_Appends_AtTheEnd(List targetObject, List expected) { // Arrange - var resolver = new Mock(MockBehavior.Strict); + var resolver = new DefaultContractResolver(); var listAdapter = new ListAdapter(); // Act - var addStatus = listAdapter.TryAdd(targetObject, "-", resolver.Object, "20", out var message); + var addStatus = listAdapter.TryAdd(targetObject, "-", resolver, "20", out var message); // Assert Assert.True(addStatus); @@ -142,12 +141,12 @@ public void Add_Appends_AtTheEnd(List targetObject, List expected) public void Add_NullObject_ToReferenceTypeListWorks() { // Arrange - var resolver = new Mock(MockBehavior.Strict); + var resolver = new DefaultContractResolver(); var listAdapter = new ListAdapter(); var targetObject = new List() { "James", "Mike" }; // Act - var addStatus = listAdapter.TryAdd(targetObject, "-", resolver.Object, value: null, errorMessage: out var message); + var addStatus = listAdapter.TryAdd(targetObject, "-", resolver, value: null, errorMessage: out var message); // Assert Assert.True(addStatus); @@ -162,12 +161,12 @@ public void Add_CompatibleTypeWorks() // Arrange var sDto = new SimpleObject(); var iDto = new InheritedObject(); - var resolver = new Mock(MockBehavior.Strict); + var resolver = new DefaultContractResolver(); var targetObject = new List() { sDto }; var listAdapter = new ListAdapter(); // Act - var addStatus = listAdapter.TryAdd(targetObject, "-", resolver.Object, iDto, out var message); + var addStatus = listAdapter.TryAdd(targetObject, "-", resolver, iDto, out var message); // Assert Assert.True(addStatus); @@ -180,12 +179,12 @@ public void Add_CompatibleTypeWorks() public void Add_NonCompatibleType_Fails() { // Arrange - var resolver = new Mock(MockBehavior.Strict); + var resolver = new DefaultContractResolver(); var targetObject = new List() { 10, 20 }; var listAdapter = new ListAdapter(); // Act - var addStatus = listAdapter.TryAdd(targetObject, "-", resolver.Object, "James", out var message); + var addStatus = listAdapter.TryAdd(targetObject, "-", resolver, "James", out var message); // Assert Assert.False(addStatus); @@ -231,11 +230,11 @@ public static TheoryData AddingDifferentComplexTyp public void Add_DifferentComplexTypeWorks(IList targetObject, object value, string position, IList expected) { // Arrange - var resolver = new Mock(MockBehavior.Strict); + var resolver = new DefaultContractResolver(); var listAdapter = new ListAdapter(); // Act - var addStatus = listAdapter.TryAdd(targetObject, position, resolver.Object, value, out var message); + var addStatus = listAdapter.TryAdd(targetObject, position, resolver, value, out var message); // Assert Assert.True(addStatus); @@ -286,11 +285,11 @@ public static TheoryData AddingKeepsObjectReferenc public void Add_KeepsObjectReference(IList targetObject, object value, string position, IList expected) { // Arrange - var resolver = new Mock(MockBehavior.Strict); + var resolver = new DefaultContractResolver(); var listAdapter = new ListAdapter(); // Act - var addStatus = listAdapter.TryAdd(targetObject, position, resolver.Object, value, out var message); + var addStatus = listAdapter.TryAdd(targetObject, position, resolver, value, out var message); // Assert Assert.True(addStatus); @@ -306,12 +305,12 @@ public void Add_KeepsObjectReference(IList targetObject, object value, string po public void Get_IndexOutOfBounds(int[] input, string position) { // Arrange - var resolver = new Mock(MockBehavior.Strict); + var resolver = new DefaultContractResolver(); var targetObject = new List(input); var listAdapter = new ListAdapter(); // Act - var getStatus = listAdapter.TryGet(targetObject, position, resolver.Object, out var value, out var message); + var getStatus = listAdapter.TryGet(targetObject, position, resolver, out var value, out var message); // Assert Assert.False(getStatus); @@ -325,12 +324,12 @@ public void Get_IndexOutOfBounds(int[] input, string position) public void Get(int[] input, string position, object expected) { // Arrange - var resolver = new Mock(MockBehavior.Strict); + var resolver = new DefaultContractResolver(); var targetObject = new List(input); var listAdapter = new ListAdapter(); // Act - var getStatus = listAdapter.TryGet(targetObject, position, resolver.Object, out var value, out var message); + var getStatus = listAdapter.TryGet(targetObject, position, resolver, out var value, out var message); // Assert Assert.True(getStatus); @@ -345,12 +344,12 @@ public void Get(int[] input, string position, object expected) public void Remove_IndexOutOfBounds(int[] input, string position) { // Arrange - var resolver = new Mock(MockBehavior.Strict); + var resolver = new DefaultContractResolver(); var targetObject = new List(input); var listAdapter = new ListAdapter(); // Act - var removeStatus = listAdapter.TryRemove(targetObject, position, resolver.Object, out var message); + var removeStatus = listAdapter.TryRemove(targetObject, position, resolver, out var message); // Assert Assert.False(removeStatus); @@ -364,12 +363,12 @@ public void Remove_IndexOutOfBounds(int[] input, string position) public void Remove(int[] input, string position, int[] expected) { // Arrange - var resolver = new Mock(MockBehavior.Strict); + var resolver = new DefaultContractResolver(); var targetObject = new List(input); var listAdapter = new ListAdapter(); // Act - var removeStatus = listAdapter.TryRemove(targetObject, position, resolver.Object, out var message); + var removeStatus = listAdapter.TryRemove(targetObject, position, resolver, out var message); // Assert Assert.True(removeStatus); @@ -380,12 +379,12 @@ public void Remove(int[] input, string position, int[] expected) public void Replace_NonCompatibleType_Fails() { // Arrange - var resolver = new Mock(MockBehavior.Strict); + var resolver = new DefaultContractResolver(); var targetObject = new List() { 10, 20 }; var listAdapter = new ListAdapter(); // Act - var replaceStatus = listAdapter.TryReplace(targetObject, "-", resolver.Object, "James", out var message); + var replaceStatus = listAdapter.TryReplace(targetObject, "-", resolver, "James", out var message); // Assert Assert.False(replaceStatus); @@ -396,12 +395,12 @@ public void Replace_NonCompatibleType_Fails() public void Replace_ReplacesValue_AtTheEnd() { // Arrange - var resolver = new Mock(MockBehavior.Strict); + var resolver = new DefaultContractResolver(); var targetObject = new List() { 10, 20 }; var listAdapter = new ListAdapter(); // Act - var replaceStatus = listAdapter.TryReplace(targetObject, "-", resolver.Object, "30", out var message); + var replaceStatus = listAdapter.TryReplace(targetObject, "-", resolver, "30", out var message); // Assert Assert.True(replaceStatus); @@ -432,12 +431,12 @@ public static TheoryData> ReplacesValuesAtPositionData public void Replace_ReplacesValue_AtGivenPosition(string position, List expected) { // Arrange - var resolver = new Mock(MockBehavior.Strict); + var resolver = new DefaultContractResolver(); var targetObject = new List() { 10, 20 }; var listAdapter = new ListAdapter(); // Act - var replaceStatus = listAdapter.TryReplace(targetObject, position, resolver.Object, "30", out var message); + var replaceStatus = listAdapter.TryReplace(targetObject, position, resolver, "30", out var message); // Assert Assert.True(replaceStatus); @@ -449,12 +448,12 @@ public void Replace_ReplacesValue_AtGivenPosition(string position, List exp public void Test_DoesNotThrowException_IfTestIsSuccessful() { // Arrange - var resolver = new Mock(MockBehavior.Strict); + var resolver = new DefaultContractResolver(); var targetObject = new List() { 10, 20 }; var listAdapter = new ListAdapter(); // Act - var testStatus = listAdapter.TryTest(targetObject, "0", resolver.Object, "10", out var message); + var testStatus = listAdapter.TryTest(targetObject, "0", resolver, "10", out var message); //Assert Assert.True(testStatus); @@ -465,13 +464,13 @@ public void Test_DoesNotThrowException_IfTestIsSuccessful() public void Test_ThrowsJsonPatchException_IfTestFails() { // Arrange - var resolver = new Mock(MockBehavior.Strict); + var resolver = new DefaultContractResolver(); var targetObject = new List() { 10, 20 }; var listAdapter = new ListAdapter(); var expectedErrorMessage = "The current value '20' at position '1' is not equal to the test value '10'."; // Act - var testStatus = listAdapter.TryTest(targetObject, "1", resolver.Object, "10", out var errorMessage); + var testStatus = listAdapter.TryTest(targetObject, "1", resolver, "10", out var errorMessage); //Assert Assert.False(testStatus); @@ -482,13 +481,13 @@ public void Test_ThrowsJsonPatchException_IfTestFails() public void Test_ThrowsJsonPatchException_IfListPositionOutOfBounds() { // Arrange - var resolver = new Mock(MockBehavior.Strict); + var resolver = new DefaultContractResolver(); var targetObject = new List() { 10, 20 }; var listAdapter = new ListAdapter(); var expectedErrorMessage = "The index value provided by path segment '2' is out of bounds of the array size."; // Act - var testStatus = listAdapter.TryTest(targetObject, "2", resolver.Object, "10", out var errorMessage); + var testStatus = listAdapter.TryTest(targetObject, "2", resolver, "10", out var errorMessage); //Assert Assert.False(testStatus); diff --git a/src/Features/JsonPatch/test/TestObjectModels/HeterogenousCollection.cs b/src/Features/JsonPatch/test/TestObjectModels/HeterogenousCollection.cs new file mode 100644 index 000000000000..3348f217ed8e --- /dev/null +++ b/src/Features/JsonPatch/test/TestObjectModels/HeterogenousCollection.cs @@ -0,0 +1,27 @@ +// 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. + +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.JsonPatch +{ + public abstract class Shape + { + public string ShapeProperty { get; set; } + } + + public class Circle : Shape + { + public string CircleProperty { get; set; } + } + + public class Rectangle : Shape + { + public string RectangleProperty { get; set; } + } + + public class Canvas + { + public IList Items { get; set; } + } +}