diff --git a/src/Microsoft.AspNetCore.JsonPatch/Helpers/ExpressionHelpers.cs b/src/Microsoft.AspNetCore.JsonPatch/Helpers/ExpressionHelpers.cs index 92880eb0e1..d9b516cc0e 100644 --- a/src/Microsoft.AspNetCore.JsonPatch/Helpers/ExpressionHelpers.cs +++ b/src/Microsoft.AspNetCore.JsonPatch/Helpers/ExpressionHelpers.cs @@ -1,9 +1,12 @@ // 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 Newtonsoft.Json; using System; using System.Globalization; +using System.Linq; using System.Linq.Expressions; +using System.Reflection; namespace Microsoft.AspNetCore.JsonPatch.Helpers { @@ -51,11 +54,13 @@ namespace Microsoft.AspNetCore.JsonPatch.Helpers if (ContinueWithSubPath(memberExpression.Expression.NodeType, false)) { var left = GetPath(memberExpression.Expression, false); - return left + "/" + memberExpression.Member.Name; + // Get property name, respecting JsonProperty attribute + return left + "/" + GetPropertyNameFromMemberExpression(memberExpression); } else { - return memberExpression.Member.Name; + // Get property name, respecting JsonProperty attribute + return GetPropertyNameFromMemberExpression(memberExpression); } case ExpressionType.Parameter: // Fits "x => x" (the whole document which is "" as JSON pointer) @@ -65,6 +70,23 @@ namespace Microsoft.AspNetCore.JsonPatch.Helpers } } + private static string GetPropertyNameFromMemberExpression(MemberExpression memberExpression) + { + // if there's a JsonProperty attribute, we must return the PropertyName + // from the attribute rather than the member name + var jsonPropertyAttribute = + memberExpression.Member.GetCustomAttribute( + typeof(JsonPropertyAttribute), true); + + if (jsonPropertyAttribute == null) + { + return memberExpression.Member.Name; + } + // get value + var castedAttribute = (JsonPropertyAttribute)jsonPropertyAttribute; + return castedAttribute.PropertyName; + } + private static bool ContinueWithSubPath(ExpressionType expressionType, bool firstTime) { if (firstTime) diff --git a/test/Microsoft.AspNetCore.JsonPatch.Test/JsonPatchDocumentJsonPropertyAttributeTest.cs b/test/Microsoft.AspNetCore.JsonPatch.Test/JsonPatchDocumentJsonPropertyAttributeTest.cs new file mode 100644 index 0000000000..ef7d53c21a --- /dev/null +++ b/test/Microsoft.AspNetCore.JsonPatch.Test/JsonPatchDocumentJsonPropertyAttributeTest.cs @@ -0,0 +1,148 @@ +// 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 Newtonsoft.Json; +using System.Linq; +using Xunit; + +namespace Microsoft.AspNetCore.JsonPatch +{ + public class JsonPatchDocumentJsonPropertyAttributeTest + { + [Fact] + public void Add_WithExpression_RespectsJsonPropertyName_ForModelProperty() + { + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(p => p.Name, "John"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + // serialized value should have "AnotherName" as path + // deserialize to a JsonPatchDocument to check + var deserialized = + JsonConvert.DeserializeObject>(serialized); + + // get path + var pathToCheck = deserialized.Operations.First().path; + Assert.Equal(pathToCheck, "/anothername"); + } + + [Fact] + public void Add_WithExpression_RespectsJsonPropertyName_WhenApplyingToDifferentlyTypedClassWithPropertyMatchingJsonPropertyName() + { + var patchDocToSerialize = new JsonPatchDocument(); + patchDocToSerialize.Add(p => p.Name, "John"); + + // the patchdoc will deserialize to "anothername". We should thus be able to apply + // it to a class that HAS that other property name. + var doc = new JsonPropertyWithAnotherNameDTO() + { + AnotherName = "InitialValue" + }; + + var serialized = JsonConvert.SerializeObject(patchDocToSerialize); + var deserialized = + JsonConvert.DeserializeObject> + (serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(doc.AnotherName, "John"); + } + + [Fact] + public void Add_WithExpression_RespectsJsonPropertyName_WhenApplyingToSameTypedClassWithMatchingJsonPropertyName() + { + var patchDocToSerialize = new JsonPatchDocument(); + patchDocToSerialize.Add(p => p.Name, "John"); + + // the patchdoc will deserialize to "anothername". As JsonPropertyDTO has + // a JsonProperty signifying that "Name" should be deseriallized from "AnotherName", + // we should be able to apply the patchDoc. + + var doc = new JsonPropertyDTO() + { + Name = "InitialValue" + }; + + var serialized = JsonConvert.SerializeObject(patchDocToSerialize); + var deserialized = + JsonConvert.DeserializeObject> + (serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(doc.Name, "John"); + } + + [Fact] + public void Add_OnApplyFromJson_RespectsJsonPropertyNameOnJsonDocument() + { + var doc = new JsonPropertyDTO() + { + Name = "InitialValue" + }; + + // serialization should serialize to "AnotherName" + var serialized = "[{\"value\":\"Kevin\",\"path\":\"/AnotherName\",\"op\":\"add\"}]"; + var deserialized = + JsonConvert.DeserializeObject>(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal("Kevin", doc.Name); + } + + [Fact] + public void Remove_OnApplyFromJson_RespectsJsonPropertyNameOnJsonDocument() + { + var doc = new JsonPropertyDTO() + { + Name = "InitialValue" + }; + + // serialization should serialize to "AnotherName" + var serialized = "[{\"path\":\"/AnotherName\",\"op\":\"remove\"}]"; + var deserialized = + JsonConvert.DeserializeObject>(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(null, doc.Name); + } + + [Fact] + public void Add_OnApplyFromJson_RespectsInheritedJsonPropertyNameOnJsonDocument() + { + var doc = new JsonPropertyWithInheritanceDTO() + { + Name = "InitialName" + }; + + // serialization should serialize to "AnotherName" + var serialized = "[{\"value\":\"Kevin\",\"path\":\"/AnotherName\",\"op\":\"add\"}]"; + var deserialized = + JsonConvert.DeserializeObject>(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal("Kevin", doc.Name); + } + + [Fact] + public void Add_WithExpression_RespectsJsonPropertyName_ForInheritedModelProperty() + { + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(p => p.Name, "John"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + // serialized value should have "AnotherName" as path + // deserialize to a JsonPatchDocument to check + var deserialized = + JsonConvert.DeserializeObject>(serialized); + + // get path + var pathToCheck = deserialized.Operations.First().path; + Assert.Equal(pathToCheck, "/anothername"); + } + } +} diff --git a/test/Microsoft.AspNetCore.JsonPatch.Test/JsonPropertyDTO.cs b/test/Microsoft.AspNetCore.JsonPatch.Test/JsonPropertyDTO.cs new file mode 100644 index 0000000000..c926d9f5fd --- /dev/null +++ b/test/Microsoft.AspNetCore.JsonPatch.Test/JsonPropertyDTO.cs @@ -0,0 +1,13 @@ +// 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 Newtonsoft.Json; + +namespace Microsoft.AspNetCore.JsonPatch +{ + public class JsonPropertyDTO + { + [JsonProperty("AnotherName")] + public string Name { get; set; } + } +} diff --git a/test/Microsoft.AspNetCore.JsonPatch.Test/JsonPropertyWithAnotherNameDTO.cs b/test/Microsoft.AspNetCore.JsonPatch.Test/JsonPropertyWithAnotherNameDTO.cs new file mode 100644 index 0000000000..9b61e09005 --- /dev/null +++ b/test/Microsoft.AspNetCore.JsonPatch.Test/JsonPropertyWithAnotherNameDTO.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 JsonPropertyWithAnotherNameDTO + { + public string AnotherName { get; set; } + } +} diff --git a/test/Microsoft.AspNetCore.JsonPatch.Test/JsonPropertyWithInheritanceDTO.cs b/test/Microsoft.AspNetCore.JsonPatch.Test/JsonPropertyWithInheritanceDTO.cs new file mode 100644 index 0000000000..fa69425ea9 --- /dev/null +++ b/test/Microsoft.AspNetCore.JsonPatch.Test/JsonPropertyWithInheritanceDTO.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.JsonPatch +{ + public class JsonPropertyWithInheritanceDTO : JsonPropertyWithInheritanceBaseDTO + { + public override string Name { get; set; } + } + + public abstract class JsonPropertyWithInheritanceBaseDTO + { + [JsonProperty("AnotherName")] + public abstract string Name { get; set; } + } +}