Skip to content

Commit

Permalink
Merge pull request #29 from rmja/generic-dictionary
Browse files Browse the repository at this point in the history
Fix issue when adding a new complex value to a generic dictionary
  • Loading branch information
Havunen authored May 30, 2024
2 parents 67651b7 + 5bb2d6c commit e00dfc2
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 23 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using SystemTextJsonPatch.Exceptions;
using Xunit;

Expand Down Expand Up @@ -135,6 +137,27 @@ private class CustomerDictionary
public IDictionary<int, Customer> DictionaryOfStringToCustomer { get; } = new Dictionary<int, Customer>();
}

#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<int, Animal> DictionaryOfIntToAnimal { get; } = new Dictionary<int, Animal>();
}
#endif

[Fact]
public void TestPocoObjectSucceeds()
{
Expand Down Expand Up @@ -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()
{
Expand All @@ -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<Dog>(actualValue1);
}
#endif

[Fact]
public void RemovePocoObjectSucceeds()
{
Expand Down
27 changes: 23 additions & 4 deletions SystemTextJsonPatch/Internal/PocoAdapter.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<string, object?> typedDictionary)
if (target is IDictionary dictionary)
{
return new DictionaryTypedPropertyProxy(typedDictionary, propertyName);
return new DictionaryPropertyProxy(dictionary, propertyName);
}

if (target is JsonArray jsonArray)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@

namespace SystemTextJsonPatch.Internal.Proxies
{
internal sealed class DictionaryTypedPropertyProxy : IPropertyProxy
internal sealed class DictionaryTypedPropertyProxy<TKey, TValue> : IPropertyProxy
{
private readonly IDictionary<string, object?> _dictionary;
private readonly string _propertyName;
private readonly IDictionary<TKey, TValue?> _dictionary;
private readonly TKey _propertyName;

internal DictionaryTypedPropertyProxy(IDictionary<string, object?> dictionary, string propertyName)
internal DictionaryTypedPropertyProxy(IDictionary<TKey, TValue?> dictionary, TKey propertyName)
{
_dictionary = dictionary;
_propertyName = propertyName;
Expand All @@ -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);
}
}

Expand All @@ -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);
}
}

0 comments on commit e00dfc2

Please sign in to comment.