From 1c8ab0933e269dc1a5685a1acdb7b76ec2b194f8 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 7 Oct 2019 21:37:43 +0200 Subject: [PATCH] Add JObjectAdapter to support JSON Patch for JObject properties (#12908) * Add JObjectAdapter to support JSON Patch for JObject properties * Add missing import * Update ref * Ignore Rider .idea folder * Add JsonPatch.sln * Add test project to solution * Add tests for JObject support * Remove unrelated test --- .gitignore | 1 + src/Features/JsonPatch/JsonPatch.sln | 48 ++++++ ...oft.AspNetCore.JsonPatch.netstandard2.0.cs | 10 ++ .../JsonPatch/src/Adapters/AdapterFactory.cs | 5 + .../JsonPatch/src/Internal/JObjectAdapter.cs | 140 +++++++++++++++++ .../test/JsonPatchDocumentJObjectTest.cs | 144 ++++++++++++++++++ .../TestObjectModels/ObjectWithJObject.cs | 9 ++ 7 files changed, 357 insertions(+) create mode 100644 src/Features/JsonPatch/JsonPatch.sln create mode 100644 src/Features/JsonPatch/src/Internal/JObjectAdapter.cs create mode 100644 src/Features/JsonPatch/test/JsonPatchDocumentJObjectTest.cs create mode 100644 src/Features/JsonPatch/test/TestObjectModels/ObjectWithJObject.cs diff --git a/.gitignore b/.gitignore index 8a2385174b..fddfe14e16 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,4 @@ launchSettings.json msbuild.ProjectImports.zip StyleCop.Cache UpgradeLog.htm +.idea \ No newline at end of file diff --git a/src/Features/JsonPatch/JsonPatch.sln b/src/Features/JsonPatch/JsonPatch.sln new file mode 100644 index 0000000000..efa5fbdf86 --- /dev/null +++ b/src/Features/JsonPatch/JsonPatch.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.JsonPatch", "src\Microsoft.AspNetCore.JsonPatch.csproj", "{B2094419-9ED4-4733-B15D-60314118B61C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.JsonPatch.Tests", "test\Microsoft.AspNetCore.JsonPatch.Tests.csproj", "{4F34177F-6E1E-4880-A2CA-0511EFEDB395}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B2094419-9ED4-4733-B15D-60314118B61C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2094419-9ED4-4733-B15D-60314118B61C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2094419-9ED4-4733-B15D-60314118B61C}.Debug|x64.ActiveCfg = Debug|Any CPU + {B2094419-9ED4-4733-B15D-60314118B61C}.Debug|x64.Build.0 = Debug|Any CPU + {B2094419-9ED4-4733-B15D-60314118B61C}.Debug|x86.ActiveCfg = Debug|Any CPU + {B2094419-9ED4-4733-B15D-60314118B61C}.Debug|x86.Build.0 = Debug|Any CPU + {B2094419-9ED4-4733-B15D-60314118B61C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2094419-9ED4-4733-B15D-60314118B61C}.Release|Any CPU.Build.0 = Release|Any CPU + {B2094419-9ED4-4733-B15D-60314118B61C}.Release|x64.ActiveCfg = Release|Any CPU + {B2094419-9ED4-4733-B15D-60314118B61C}.Release|x64.Build.0 = Release|Any CPU + {B2094419-9ED4-4733-B15D-60314118B61C}.Release|x86.ActiveCfg = Release|Any CPU + {B2094419-9ED4-4733-B15D-60314118B61C}.Release|x86.Build.0 = Release|Any CPU + {4F34177F-6E1E-4880-A2CA-0511EFEDB395}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F34177F-6E1E-4880-A2CA-0511EFEDB395}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F34177F-6E1E-4880-A2CA-0511EFEDB395}.Debug|x64.ActiveCfg = Debug|Any CPU + {4F34177F-6E1E-4880-A2CA-0511EFEDB395}.Debug|x64.Build.0 = Debug|Any CPU + {4F34177F-6E1E-4880-A2CA-0511EFEDB395}.Debug|x86.ActiveCfg = Debug|Any CPU + {4F34177F-6E1E-4880-A2CA-0511EFEDB395}.Debug|x86.Build.0 = Debug|Any CPU + {4F34177F-6E1E-4880-A2CA-0511EFEDB395}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F34177F-6E1E-4880-A2CA-0511EFEDB395}.Release|Any CPU.Build.0 = Release|Any CPU + {4F34177F-6E1E-4880-A2CA-0511EFEDB395}.Release|x64.ActiveCfg = Release|Any CPU + {4F34177F-6E1E-4880-A2CA-0511EFEDB395}.Release|x64.Build.0 = Release|Any CPU + {4F34177F-6E1E-4880-A2CA-0511EFEDB395}.Release|x86.ActiveCfg = Release|Any CPU + {4F34177F-6E1E-4880-A2CA-0511EFEDB395}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/src/Features/JsonPatch/ref/Microsoft.AspNetCore.JsonPatch.netstandard2.0.cs b/src/Features/JsonPatch/ref/Microsoft.AspNetCore.JsonPatch.netstandard2.0.cs index f8ba24f51f..270bbcb56c 100644 --- a/src/Features/JsonPatch/ref/Microsoft.AspNetCore.JsonPatch.netstandard2.0.cs +++ b/src/Features/JsonPatch/ref/Microsoft.AspNetCore.JsonPatch.netstandard2.0.cs @@ -201,6 +201,16 @@ namespace Microsoft.AspNetCore.JsonPatch.Internal bool TryTest(object target, string segment, Newtonsoft.Json.Serialization.IContractResolver contractResolver, object value, out string errorMessage); bool TryTraverse(object target, string segment, Newtonsoft.Json.Serialization.IContractResolver contractResolver, out object nextTarget, out string errorMessage); } + public partial class JObjectAdapter : Microsoft.AspNetCore.JsonPatch.Internal.IAdapter + { + public JObjectAdapter() { } + public virtual bool TryAdd(object target, string segment, Newtonsoft.Json.Serialization.IContractResolver contractResolver, object value, out string errorMessage) { throw null; } + public virtual bool TryGet(object target, string segment, Newtonsoft.Json.Serialization.IContractResolver contractResolver, out object value, out string errorMessage) { throw null; } + public virtual bool TryRemove(object target, string segment, Newtonsoft.Json.Serialization.IContractResolver contractResolver, out string errorMessage) { throw null; } + public virtual bool TryReplace(object target, string segment, Newtonsoft.Json.Serialization.IContractResolver contractResolver, object value, out string errorMessage) { throw null; } + public virtual bool TryTest(object target, string segment, Newtonsoft.Json.Serialization.IContractResolver contractResolver, object value, out string errorMessage) { throw null; } + public virtual bool TryTraverse(object target, string segment, Newtonsoft.Json.Serialization.IContractResolver contractResolver, out object nextTarget, out string errorMessage) { throw null; } + } public partial class ListAdapter : Microsoft.AspNetCore.JsonPatch.Internal.IAdapter { public ListAdapter() { } diff --git a/src/Features/JsonPatch/src/Adapters/AdapterFactory.cs b/src/Features/JsonPatch/src/Adapters/AdapterFactory.cs index 078f83e175..ff91018a88 100644 --- a/src/Features/JsonPatch/src/Adapters/AdapterFactory.cs +++ b/src/Features/JsonPatch/src/Adapters/AdapterFactory.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.JsonPatch.Internal; +using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; using System; using System.Collections; @@ -29,6 +30,10 @@ namespace Microsoft.AspNetCore.JsonPatch.Adapters var jsonContract = contractResolver.ResolveContract(target.GetType()); + if (target is JObject) + { + return new JObjectAdapter(); + } if (target is IList) { return new ListAdapter(); diff --git a/src/Features/JsonPatch/src/Internal/JObjectAdapter.cs b/src/Features/JsonPatch/src/Internal/JObjectAdapter.cs new file mode 100644 index 0000000000..8354d2043b --- /dev/null +++ b/src/Features/JsonPatch/src/Internal/JObjectAdapter.cs @@ -0,0 +1,140 @@ +using Microsoft.AspNetCore.JsonPatch.Internal; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.AspNetCore.JsonPatch.Internal +{ + public class JObjectAdapter : IAdapter + { + public virtual bool TryAdd( + object target, + string segment, + IContractResolver contractResolver, + object value, + out string errorMessage) + { + var obj = (JObject) target; + + obj[segment] = JToken.FromObject(value); + + errorMessage = null; + return true; + } + + public virtual bool TryGet( + object target, + string segment, + IContractResolver contractResolver, + out object value, + out string errorMessage) + { + var obj = (JObject) target; + + if (!obj.ContainsKey(segment)) + { + value = null; + errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment); + return false; + } + + value = obj[segment]; + errorMessage = null; + return true; + } + + public virtual bool TryRemove( + object target, + string segment, + IContractResolver contractResolver, + out string errorMessage) + { + var obj = (JObject) target; + + if (!obj.ContainsKey(segment)) + { + errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment); + return false; + } + + obj.Remove(segment); + errorMessage = null; + return true; + } + + public virtual bool TryReplace( + object target, + string segment, + IContractResolver contractResolver, + object value, + out string errorMessage) + { + var obj = (JObject) target; + + if (!obj.ContainsKey(segment)) + { + errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment); + return false; + } + + obj[segment] = JToken.FromObject(value); + + errorMessage = null; + return true; + } + + public virtual bool TryTest( + object target, + string segment, + IContractResolver contractResolver, + object value, + out string errorMessage) + { + var obj = (JObject) target; + + if (!obj.ContainsKey(segment)) + { + errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment); + return false; + } + + var currentValue = obj[segment]; + + if (currentValue == null || string.IsNullOrEmpty(currentValue.ToString())) + { + errorMessage = Resources.FormatValueForTargetSegmentCannotBeNullOrEmpty(segment); + return false; + } + + if (!JToken.DeepEquals(JsonConvert.SerializeObject(currentValue), JsonConvert.SerializeObject(value))) + { + errorMessage = Resources.FormatValueNotEqualToTestValue(currentValue, value, segment); + return false; + } + + errorMessage = null; + return true; + } + + public virtual bool TryTraverse( + object target, + string segment, + IContractResolver contractResolver, + out object nextTarget, + out string errorMessage) + { + var obj = (JObject) target; + + if (!obj.ContainsKey(segment)) + { + nextTarget = null; + errorMessage = null; + return false; + } + + nextTarget = obj[segment]; + errorMessage = null; + return true; + } + } +} \ No newline at end of file diff --git a/src/Features/JsonPatch/test/JsonPatchDocumentJObjectTest.cs b/src/Features/JsonPatch/test/JsonPatchDocumentJObjectTest.cs new file mode 100644 index 0000000000..271f100871 --- /dev/null +++ b/src/Features/JsonPatch/test/JsonPatchDocumentJObjectTest.cs @@ -0,0 +1,144 @@ +// 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; +using System.Dynamic; +using Microsoft.AspNetCore.JsonPatch.Exceptions; +using Microsoft.AspNetCore.JsonPatch.Operations; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace Microsoft.AspNetCore.JsonPatch +{ + public class JsonPatchDocumentJObjectTest + { + [Fact] + public void ApplyTo_Array_Add() + { + // Arrange + var model = new ObjectWithJObject{ CustomData = JObject.FromObject(new { Emails = new[] { "foo@bar.com" } })}; + var patch = new JsonPatchDocument(); + + patch.Operations.Add(new Operation("add", "/CustomData/Emails/-", null, "foo@baz.com")); + + // Act + patch.ApplyTo(model); + + // Assert + Assert.Equal("foo@baz.com", model.CustomData["Emails"][1].Value()); + } + + [Fact] + public void ApplyTo_Model_Test1() + { + // Arrange + var model = new ObjectWithJObject{ CustomData = JObject.FromObject(new { Email = "foo@bar.com", Name = "Bar" })}; + var patch = new JsonPatchDocument(); + + patch.Operations.Add(new Operation("test", "/CustomData/Email", null, "foo@baz.com")); + patch.Operations.Add(new Operation("add", "/CustomData/Name", null, "Bar Baz")); + + // Act & Assert + Assert.Throws(() => patch.ApplyTo(model)); + } + + [Fact] + public void ApplyTo_Model_Test2() + { + // Arrange + var model = new ObjectWithJObject{ CustomData = JObject.FromObject(new { Email = "foo@bar.com", Name = "Bar" })}; + var patch = new JsonPatchDocument(); + + patch.Operations.Add(new Operation("test", "/CustomData/Email", null, "foo@bar.com")); + patch.Operations.Add(new Operation("add", "/CustomData/Name", null, "Bar Baz")); + + // Act + patch.ApplyTo(model); + + // Assert + Assert.Equal("Bar Baz", model.CustomData["Name"].Value()); + } + + [Fact] + public void ApplyTo_Model_Copy() + { + // Arrange + var model = new ObjectWithJObject{ CustomData = JObject.FromObject(new { Email = "foo@bar.com" })}; + var patch = new JsonPatchDocument(); + + patch.Operations.Add(new Operation("copy", "/CustomData/UserName", "/CustomData/Email")); + + // Act + patch.ApplyTo(model); + + // Assert + Assert.Equal("foo@bar.com", model.CustomData["UserName"].Value()); + } + + [Fact] + public void ApplyTo_Model_Remove() + { + // Arrange + var model = new ObjectWithJObject{ CustomData = JObject.FromObject(new { FirstName = "Foo", LastName = "Bar" })}; + var patch = new JsonPatchDocument(); + + patch.Operations.Add(new Operation("remove", "/CustomData/LastName", null)); + + // Act + patch.ApplyTo(model); + + // Assert + Assert.False(model.CustomData.ContainsKey("LastName")); + } + + [Fact] + public void ApplyTo_Model_Move() + { + // Arrange + var model = new ObjectWithJObject{ CustomData = JObject.FromObject(new { FirstName = "Bar" })}; + var patch = new JsonPatchDocument(); + + patch.Operations.Add(new Operation("move", "/CustomData/LastName", "/CustomData/FirstName")); + + // Act + patch.ApplyTo(model); + + // Assert + Assert.False(model.CustomData.ContainsKey("FirstName")); + Assert.Equal("Bar", model.CustomData["LastName"].Value()); + } + + [Fact] + public void ApplyTo_Model_Add() + { + // Arrange + var model = new ObjectWithJObject(); + var patch = new JsonPatchDocument(); + + patch.Operations.Add(new Operation("add", "/CustomData/Name", null, "Foo")); + + // Act + patch.ApplyTo(model); + + // Assert + Assert.Equal("Foo", model.CustomData["Name"].Value()); + } + + [Fact] + public void ApplyTo_Model_Replace() + { + // Arrange + var model = new ObjectWithJObject{ CustomData = JObject.FromObject(new { Email = "foo@bar.com", Name = "Bar" })}; + var patch = new JsonPatchDocument(); + + patch.Operations.Add(new Operation("replace", "/CustomData/Email", null, "foo@baz.com")); + + // Act + patch.ApplyTo(model); + + // Assert + Assert.Equal("foo@baz.com", model.CustomData["Email"].Value()); + } + } +} diff --git a/src/Features/JsonPatch/test/TestObjectModels/ObjectWithJObject.cs b/src/Features/JsonPatch/test/TestObjectModels/ObjectWithJObject.cs new file mode 100644 index 0000000000..188ca034d2 --- /dev/null +++ b/src/Features/JsonPatch/test/TestObjectModels/ObjectWithJObject.cs @@ -0,0 +1,9 @@ +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.JsonPatch +{ + public class ObjectWithJObject + { + public JObject CustomData { get; set; } = new JObject(); + } +}