diff --git a/SystemTextJsonPatch.Tests/IntegrationTests/DictionaryIntegrationTest.cs b/SystemTextJsonPatch.Tests/IntegrationTests/DictionaryIntegrationTest.cs index fe744d0..64717f3 100644 --- a/SystemTextJsonPatch.Tests/IntegrationTests/DictionaryIntegrationTest.cs +++ b/SystemTextJsonPatch.Tests/IntegrationTests/DictionaryIntegrationTest.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; using SystemTextJsonPatch.Exceptions; using Xunit; @@ -135,6 +137,27 @@ private class CustomerDictionary public IDictionary DictionaryOfStringToCustomer { get; } = new Dictionary(); } +#if NET7_0_OR_GREATER + [JsonDerivedType(typeof(Dog), "dog")] + [JsonDerivedType(typeof(Cat), "cat")] + private abstract class Animal + { + } + + private class Dog : Animal + { + } + + private class Cat : Animal + { + } + + private class AnimalDictionary + { + public IDictionary DictionaryOfIntToAnimal { get; } = new Dictionary(); + } +#endif + [Fact] public void TestPocoObjectSucceeds() { @@ -168,6 +191,32 @@ public void TestPocoObjectFailsWhenTestValueIsNotEqualToObjectValue() Assert.Equal("The current value 'James' at path 'Name' is not equal to the test value 'Mike'.", exception.Message); } + [Fact] + public void AddPocoObjectSucceeds() + { + // Arrange + var key1 = 100; + var value1 = new Customer() { Name = "James" }; + var key2 = 200; + var value2 = new Customer() { Name = "Mike" }; + var model = new CustomerDictionary(); + model.DictionaryOfStringToCustomer[key1] = value1; + var patchDocument = new JsonPatchDocument(); + patchDocument.Add($"/DictionaryOfStringToCustomer/{key2}", value2); + + // Act + patchDocument.ApplyTo(model); + + // Assert + Assert.Equal(2, model.DictionaryOfStringToCustomer.Count); + var actualValue1 = model.DictionaryOfStringToCustomer[key1]; + Assert.NotNull(actualValue1); + Assert.Equal("James", actualValue1.Name); + var actualValue2 = model.DictionaryOfStringToCustomer[key2]; + Assert.NotNull(actualValue2); + Assert.Equal("Mike", actualValue2.Name); + } + [Fact] public void AddReplacesPocoObjectSucceeds() { @@ -192,6 +241,27 @@ public void AddReplacesPocoObjectSucceeds() Assert.Equal("James", actualValue1.Name); } +#if NET7_0_OR_GREATER + [Fact] + public void AddReplacesPocoObjectWithDifferentTypeSucceeds() + { + // Arrange + var key1 = 100; + var value1 = new Cat(); + var model = new AnimalDictionary(); + model.DictionaryOfIntToAnimal[key1] = value1; + var patchDocument = new JsonPatchDocument(); + patchDocument.Add($"/DictionaryOfIntToAnimal/{key1}", new Dog()); + + // Act + patchDocument.ApplyTo(model); + + // Assert + var actualValue1 = Assert.Single(model.DictionaryOfIntToAnimal).Value; + Assert.IsType(actualValue1); + } +#endif + [Fact] public void RemovePocoObjectSucceeds() { diff --git a/SystemTextJsonPatch/Internal/PocoAdapter.cs b/SystemTextJsonPatch/Internal/PocoAdapter.cs index 574f71f..733768b 100644 --- a/SystemTextJsonPatch/Internal/PocoAdapter.cs +++ b/SystemTextJsonPatch/Internal/PocoAdapter.cs @@ -1,7 +1,10 @@ using System; using System.Collections; using System.Collections.Generic; +using System.ComponentModel; using System.Dynamic; +using System.Linq; +using System.Reflection; using System.Text.Json; using System.Text.Json.Nodes; using SystemTextJsonPatch.Internal.Proxies; @@ -186,14 +189,30 @@ private static bool TryGetJsonProperty(object target, string segment, JsonSerial return new DynamicObjectPropertyProxy(dyObj, propertyName); } - if (target is IDictionary dictionary) + var genericDictionaryType = target.GetType().GetInterfaces() + .FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IDictionary<,>)); + if (genericDictionaryType is not null) { - return new DictionaryPropertyProxy(dictionary, propertyName); + var args = genericDictionaryType.GetGenericArguments(); + var keyType = args[0]; + var valueType = args[1]; + var converter = TypeDescriptor.GetConverter(keyType); + if (converter.CanConvertFrom(typeof(string))) + { + var key = converter.ConvertFromInvariantString(propertyName); + var proxyType = typeof(DictionaryTypedPropertyProxy<,>).MakeGenericType(keyType, valueType); + return (IPropertyProxy)Activator.CreateInstance( + proxyType, + BindingFlags.NonPublic | BindingFlags.Instance, + null, + [target, key], + null)!; + } } - if (target is IDictionary typedDictionary) + if (target is IDictionary dictionary) { - return new DictionaryTypedPropertyProxy(typedDictionary, propertyName); + return new DictionaryPropertyProxy(dictionary, propertyName); } if (target is JsonArray jsonArray) diff --git a/SystemTextJsonPatch/Internal/Proxies/DictionaryTypedPropertyProxy.cs b/SystemTextJsonPatch/Internal/Proxies/DictionaryTypedPropertyProxy.cs index 112b6da..5a22393 100644 --- a/SystemTextJsonPatch/Internal/Proxies/DictionaryTypedPropertyProxy.cs +++ b/SystemTextJsonPatch/Internal/Proxies/DictionaryTypedPropertyProxy.cs @@ -3,12 +3,12 @@ namespace SystemTextJsonPatch.Internal.Proxies { - internal sealed class DictionaryTypedPropertyProxy : IPropertyProxy + internal sealed class DictionaryTypedPropertyProxy : IPropertyProxy { - private readonly IDictionary _dictionary; - private readonly string _propertyName; + private readonly IDictionary _dictionary; + private readonly TKey _propertyName; - internal DictionaryTypedPropertyProxy(IDictionary dictionary, string propertyName) + internal DictionaryTypedPropertyProxy(IDictionary dictionary, TKey propertyName) { _dictionary = dictionary; _propertyName = propertyName; @@ -25,11 +25,11 @@ public void SetValue(object target, object? convertedValue) { if (_dictionary.ContainsKey(_propertyName)) { - _dictionary[_propertyName] = convertedValue; + _dictionary[_propertyName] = (TValue?)convertedValue; } else { - _dictionary.Add(_propertyName, convertedValue); + _dictionary.Add(_propertyName, (TValue?)convertedValue); } } @@ -41,18 +41,6 @@ public void RemoveValue(object target) public bool CanRead => true; public bool CanWrite => true; - public Type PropertyType - { - get - { - _dictionary.TryGetValue(_propertyName, out var val); - if (val == null) - { - return typeof(object); - } - - return val.GetType(); - } - } + public Type PropertyType => typeof(TValue); } }