diff --git a/src/Microsoft.AspNetCore.JsonPatch/Internal/DynamicObjectAdapter.cs b/src/Microsoft.AspNetCore.JsonPatch/Internal/DynamicObjectAdapter.cs
index dc3c48266f..5b3d7d8bdd 100644
--- a/src/Microsoft.AspNetCore.JsonPatch/Internal/DynamicObjectAdapter.cs
+++ b/src/Microsoft.AspNetCore.JsonPatch/Internal/DynamicObjectAdapter.cs
@@ -96,6 +96,11 @@ namespace Microsoft.AspNetCore.JsonPatch.Internal
return false;
}
+ if (!TryRemove(target, segment, contractResolver, out errorMessage))
+ {
+ return false;
+ }
+
if (!TrySetDynamicObjectProperty(target, contractResolver, segment, convertedValue, out errorMessage))
{
return false;
diff --git a/test/Microsoft.AspNetCore.JsonPatch.Test/Internal/DynamicObjectAdapterTest.cs b/test/Microsoft.AspNetCore.JsonPatch.Test/Internal/DynamicObjectAdapterTest.cs
index 96b1aee935..9d5eb2595c 100644
--- a/test/Microsoft.AspNetCore.JsonPatch.Test/Internal/DynamicObjectAdapterTest.cs
+++ b/test/Microsoft.AspNetCore.JsonPatch.Test/Internal/DynamicObjectAdapterTest.cs
@@ -130,11 +130,11 @@ namespace Microsoft.AspNetCore.JsonPatch.Internal
}
[Fact]
- public void TryReplace_ReplacesPropertyValue()
+ public void TryReplace_RemovesExistingValue_BeforeAddingNewValue()
{
// Arrange
var adapter = new DynamicObjectAdapter();
- dynamic target = new DynamicTestObject();
+ dynamic target = new WriteOnceDynamicTestObject();
target.NewProperty = new object();
var segment = "NewProperty";
var resolver = new DefaultContractResolver();
diff --git a/test/Microsoft.AspNetCore.JsonPatch.Test/WriteOnceDynamicTestObject.cs b/test/Microsoft.AspNetCore.JsonPatch.Test/WriteOnceDynamicTestObject.cs
new file mode 100644
index 0000000000..769ddcc154
--- /dev/null
+++ b/test/Microsoft.AspNetCore.JsonPatch.Test/WriteOnceDynamicTestObject.cs
@@ -0,0 +1,117 @@
+// 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 System.Dynamic;
+
+namespace Microsoft.AspNetCore.JsonPatch.Internal
+{
+ ///
+ ///
+ /// This class is used specifically to test that JSON patch "replace" operations are functionally equivalent to
+ /// "add" and "remove" operations applied sequentially using the same path.
+ ///
+ ///
+ /// This is done by asserting that no value exists for a particular key before setting its value. To replace the
+ /// value for a key, the key must first be removed, and then re-added with the new value.
+ ///
+ ///
+ /// See JsonPatch#110 for further details.
+ ///
+ ///
+ public class WriteOnceDynamicTestObject : DynamicObject
+ {
+ private Dictionary _dictionary = new Dictionary();
+
+ public object this[string key] { get => ((IDictionary)_dictionary)[key]; set => SetValueForKey(key, value); }
+
+ public ICollection Keys => ((IDictionary)_dictionary).Keys;
+
+ public ICollection