Skip to content

Commit 6448b70

Browse files
authored
Reworked the dictionary serializer to allow for a more flexible AnyType. (#6367)
1 parent 41e1cb8 commit 6448b70

File tree

5 files changed

+124
-33
lines changed

5 files changed

+124
-33
lines changed

.github/workflows/codeql-analysis.yml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,14 @@ jobs:
4949
# By default, queries listed here will override any specified in a config file.
5050
# Prefix the list here with "+" to use these queries and those in the config file.
5151
# queries: ./path/to/local/query, your-org/your-repo/queries@main
52-
52+
5353
- name: Setup .NET Core SDK
54-
uses: actions/setup-dotnet@v1.9.0
54+
uses: actions/setup-dotnet@v3
55+
with:
56+
dotnet-version: |
57+
6.x
58+
7.x
59+
8.x
5560
5661
- name: Build
5762
run: |

src/HotChocolate/Core/src/Types/Utilities/ObjectToDictionaryConverter.cs

Lines changed: 62 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,35 @@
1+
using System.Linq;
12
using System;
23
using System.Collections;
34
using System.Collections.Concurrent;
45
using System.Collections.Generic;
6+
using System.Dynamic;
57
using System.Reflection;
68

79
namespace HotChocolate.Utilities;
810

9-
internal class ObjectToDictionaryConverter
11+
internal class ObjectToDictionaryConverter(ITypeConverter converter)
1012
{
11-
private readonly ITypeConverter _converter;
12-
private readonly ConcurrentDictionary<Type, List<PropertyInfo>> _properties = new();
13-
14-
public ObjectToDictionaryConverter(ITypeConverter converter)
15-
{
16-
_converter = converter
17-
?? throw new ArgumentNullException(nameof(converter));
18-
}
13+
private readonly ITypeConverter _converter = converter ?? throw new ArgumentNullException(nameof(converter));
14+
private readonly ConcurrentDictionary<Type, PropertyInfo[]> _properties = new();
1915

2016
public object Convert(object obj)
2117
{
22-
if (obj is null)
18+
if(obj is null)
2319
{
2420
throw new ArgumentNullException(nameof(obj));
2521
}
2622

2723
object value = null;
28-
Action<object> setValue = v => value = v;
29-
VisitValue(obj, setValue, new HashSet<object>());
24+
void SetValue(object v) => value = v;
25+
VisitValue(obj, SetValue, new HashSet<object>());
3026
return value;
3127
}
3228

3329
private void VisitValue(
3430
object obj,
3531
Action<object> setValue,
36-
ISet<object> processed)
32+
HashSet<object> processed)
3733
{
3834
if (obj is null)
3935
{
@@ -81,19 +77,56 @@ private void VisitValue(
8177
private void VisitObject(
8278
object obj,
8379
Action<object> setValue,
84-
ISet<object> processed)
80+
HashSet<object> processed)
8581
{
8682
if (processed.Add(obj))
8783
{
88-
var dict = new Dictionary<string, object>();
89-
setValue(dict);
84+
var current = new Dictionary<string, object>();
85+
setValue(current);
9086

91-
if (obj is IReadOnlyDictionary<string, object> d)
87+
if (obj is Dictionary<string, object> dict1)
88+
{
89+
foreach (var item in dict1)
90+
{
91+
void SetField(object v) => current[item.Key] = v;
92+
VisitValue(item.Value, SetField, processed);
93+
}
94+
}
95+
else if (obj is IDictionary<string, object> dict2)
96+
{
97+
foreach (var item in dict2)
98+
{
99+
void SetField(object v) => current[item.Key] = v;
100+
VisitValue(item.Value, SetField, processed);
101+
}
102+
}
103+
else if (obj is IReadOnlyDictionary<string, object> dict3)
92104
{
93-
foreach (var item in d)
105+
foreach (var item in dict3)
94106
{
95-
Action<object> setField = v => dict[item.Key] = v;
96-
VisitValue(item.Value, setField, processed);
107+
void SetField(object v) => current[item.Key] = v;
108+
VisitValue(item.Value, SetField, processed);
109+
}
110+
}
111+
else if (obj is IDictionary dict4)
112+
{
113+
foreach (var item in dict4)
114+
{
115+
if (item is DictionaryEntry entry)
116+
{
117+
void SetField(object v) => current[entry.Key.ToString()!] = v;
118+
VisitValue(entry.Value, SetField, processed);
119+
}
120+
else if (item is KeyValuePair<string, object> pair)
121+
{
122+
void SetField(object v) => current[pair.Key] = v;
123+
VisitValue(pair.Value, SetField, processed);
124+
}
125+
else
126+
{
127+
throw new NotSupportedException(
128+
$"The dictionary entry type `{item.GetType().FullName}` is not supported.");
129+
}
97130
}
98131
}
99132
else
@@ -102,8 +135,8 @@ private void VisitObject(
102135
{
103136
var name = property.GetGraphQLName();
104137
var value = property.GetValue(obj);
105-
Action<object> setField = v => dict[name] = v;
106-
VisitValue(value, setField, processed);
138+
void SetField(object v) => current[name] = v;
139+
VisitValue(value, SetField, processed);
107140
}
108141
}
109142
}
@@ -112,28 +145,29 @@ private void VisitObject(
112145
private void VisitList(
113146
ICollection list,
114147
Action<object> setValue,
115-
ISet<object> processed)
148+
HashSet<object> processed)
116149
{
117150
var valueList = new List<object>();
118151
setValue(valueList);
119152

120-
Action<object> addItem = item => valueList.Add(item);
153+
void AddItem(object item) => valueList.Add(item);
121154

122155
foreach (var element in list)
123156
{
124-
VisitValue(element, addItem, processed);
157+
VisitValue(element, AddItem, processed);
125158
}
126159
}
127160

128-
private IReadOnlyList<PropertyInfo> GetProperties(object value)
161+
private ReadOnlySpan<PropertyInfo> GetProperties(object value)
129162
{
130163
var type = value.GetType();
164+
131165
if (!_properties.TryGetValue(type, out var properties))
132166
{
133-
properties = new List<PropertyInfo>(
134-
ReflectionUtils.GetProperties(type).Values);
167+
properties = ReflectionUtils.GetProperties(type).Values.ToArray();
135168
_properties.TryAdd(type, properties);
136169
}
170+
137171
return properties;
138172
}
139173
}

src/HotChocolate/Core/test/Types.Tests/Types/Scalars/AnyTypeTests.cs

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Collections.Immutable;
4+
using System.Dynamic;
35
using System.Threading.Tasks;
46
using HotChocolate.Execution;
57
using HotChocolate.Language;
68
using HotChocolate.Tests;
79
using Microsoft.Extensions.DependencyInjection;
810
using Snapshooter.Xunit;
9-
using Xunit;
1011
using static HotChocolate.Tests.TestHelper;
1112

1213
namespace HotChocolate.Types;
@@ -1096,8 +1097,8 @@ public void Deserialize_List()
10961097
// assert
10971098
Assert.Collection(
10981099
Assert.IsType<object[]>(value)!,
1099-
x => Assert.Equal("Foo",x),
1100-
x => Assert.Equal("Bar",x));
1100+
x => Assert.Equal("Foo", x),
1101+
x => Assert.Equal("Bar", x));
11011102
}
11021103

11031104
[Fact]
@@ -1110,6 +1111,43 @@ await ExpectValid(
11101111
.MatchSnapshotAsync();
11111112
}
11121113

1114+
[Fact]
1115+
public async Task UseExpandoObjectWithAny()
1116+
{
1117+
Snapshot.FullName();
1118+
await ExpectValid(
1119+
"{ something }",
1120+
configure: c => c.AddQueryType<SomeQuery>())
1121+
.MatchSnapshotAsync();
1122+
}
1123+
1124+
[Fact]
1125+
public async Task UseImmutableDictWithAny()
1126+
{
1127+
Snapshot.FullName();
1128+
await ExpectValid(
1129+
"{ somethingImmutable }",
1130+
configure: c => c.AddQueryType<SomeQuery>())
1131+
.MatchSnapshotAsync();
1132+
}
1133+
1134+
public class SomeQuery
1135+
{
1136+
[GraphQLType<AnyType>]
1137+
public object GetSomething()
1138+
{
1139+
dynamic obj = new ExpandoObject();
1140+
obj.a = "Foo";
1141+
return obj;
1142+
}
1143+
1144+
[GraphQLType<AnyType>]
1145+
public ImmutableDictionary<string, object> GetSomethingImmutable()
1146+
{
1147+
return ImmutableDictionary<string, object>.Empty.Add("a", "Foo");
1148+
}
1149+
}
1150+
11131151
public class Foo
11141152
{
11151153
public Bar Bar { get; set; } = new Bar();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"data": {
3+
"something": {
4+
"a": "Foo"
5+
}
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"data": {
3+
"somethingImmutable": {
4+
"a": "Foo"
5+
}
6+
}
7+
}

0 commit comments

Comments
 (0)