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 Values => ((IDictionary)_dictionary).Values; + + public int Count => ((IDictionary)_dictionary).Count; + + public bool IsReadOnly => ((IDictionary)_dictionary).IsReadOnly; + + public void Add(string key, object value) + { + SetValueForKey(key, value); + } + + public void Add(KeyValuePair item) + { + SetValueForKey(item.Key, item.Value); + } + + public void Clear() + { + ((IDictionary)_dictionary).Clear(); + } + + public bool Contains(KeyValuePair item) + { + return ((IDictionary)_dictionary).Contains(item); + } + + public bool ContainsKey(string key) + { + return ((IDictionary)_dictionary).ContainsKey(key); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + ((IDictionary)_dictionary).CopyTo(array, arrayIndex); + } + + public IEnumerator> GetEnumerator() + { + return ((IDictionary)_dictionary).GetEnumerator(); + } + + public bool Remove(string key) + { + return ((IDictionary)_dictionary).Remove(key); + } + + public bool Remove(KeyValuePair item) + { + return ((IDictionary)_dictionary).Remove(item); + } + + public bool TryGetValue(string key, out object value) + { + return ((IDictionary)_dictionary).TryGetValue(key, out value); + } + + public override bool TryGetMember(GetMemberBinder binder, out object result) + { + var name = binder.Name; + + return TryGetValue(name, out result); + } + + public override bool TrySetMember(SetMemberBinder binder, object value) + { + SetValueForKey(binder.Name, value); + + return true; + } + + private void SetValueForKey(string key, object value) + { + if (value == null) + { + _dictionary.Remove(key); + return; + } + + if (_dictionary.ContainsKey(key)) + { + throw new ArgumentException($"Value for {key} already exists"); + } + + _dictionary[key] = value; + } + } +}