From ba942ccf29f8d932c03e70a706d56dfb627731fa Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 20 Jan 2023 16:55:54 +0100 Subject: [PATCH] Added Set Converter (#5703) --- .../src/Types/Utilities/ListTypeConverter.cs | 131 +++++++++++++++--- .../test/Execution.Tests/CodeFirstTests.cs | 4 - .../HotChocolate.Execution.Tests.csproj | 1 + .../TypeConverter/TypeConverterTests.cs | 88 +++++++++--- .../Core/test/Execution.Tests/StreamTests.cs | 1 + .../Core/test/Types.Tests/CodeFirstTests.cs | 1 - 6 files changed, 176 insertions(+), 50 deletions(-) diff --git a/src/HotChocolate/Core/src/Types/Utilities/ListTypeConverter.cs b/src/HotChocolate/Core/src/Types/Utilities/ListTypeConverter.cs index 19a8bb32c0c..afba7b0967b 100644 --- a/src/HotChocolate/Core/src/Types/Utilities/ListTypeConverter.cs +++ b/src/HotChocolate/Core/src/Types/Utilities/ListTypeConverter.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using HotChocolate.Internal; #nullable enable @@ -13,7 +15,17 @@ internal sealed class ListTypeConverter : IChangeTypeProvider { private static readonly MethodInfo _dictionaryConvert = typeof(ListTypeConverter).GetMethod( - "GenericDictionaryConverter", + nameof(GenericDictionaryConverter), + BindingFlags.Static | BindingFlags.NonPublic)!; + + private static readonly MethodInfo _setConvert = + typeof(ListTypeConverter).GetMethod( + nameof(HashSetConverter), + BindingFlags.Static | BindingFlags.NonPublic)!; + + private static readonly MethodInfo _collectionConvert = + typeof(ListTypeConverter).GetMethod( + nameof(GenericCollectionConverter), BindingFlags.Static | BindingFlags.NonPublic)!; public bool TryCreateConverter( @@ -25,45 +37,62 @@ internal sealed class ListTypeConverter : IChangeTypeProvider var sourceElement = ExtendedType.Tools.GetElementType(source); var targetElement = ExtendedType.Tools.GetElementType(target); - if (sourceElement is not null - && targetElement is not null - && root(sourceElement, targetElement, out var elementConverter)) + if (sourceElement is not null && + targetElement is not null && + root(sourceElement, targetElement, out var elementConverter)) { if (target.IsArray) { converter = input => GenericArrayConverter( - (ICollection?)input, targetElement, elementConverter); + (ICollection?)input, + targetElement, + elementConverter); return true; } - if (target.IsGenericType - && target.GetGenericTypeDefinition() == typeof(Dictionary<,>)) + if (target.IsGenericType && target.GetGenericTypeDefinition() == typeof(Dictionary<,>)) { var converterMethod = _dictionaryConvert.MakeGenericMethod(targetElement.GetGenericArguments()); - converter = source => converterMethod.Invoke( - null, new[] { source, elementConverter }); + converter = s => converterMethod.Invoke(null, new[] { s, elementConverter }); return true; } - if (target.IsGenericType - && target.IsInterface) + if (target is { IsGenericType: true, IsInterface: true }) { + var typeDefinition = target.GetGenericTypeDefinition(); + + if (typeDefinition == typeof(ISet<>)) + { + var converterMethod = _setConvert.MakeGenericMethod(targetElement); + converter = s => converterMethod.Invoke(null, new[] { s, elementConverter }); + return true; + } + var listType = typeof(List<>).MakeGenericType(targetElement); + if (target.IsAssignableFrom(listType)) { - converter = source => GenericListConverter( - (ICollection?)source, listType, elementConverter); + converter = s => GenericListConverter( + (ICollection?)s, + listType, + elementConverter); return true; } } - if (target.IsGenericType - && target.IsClass - && typeof(ICollection).IsAssignableFrom(target)) + if (target is { IsGenericType: true, IsClass: true } && + typeof(ICollection).IsAssignableFrom(target)) + { + converter = s => GenericListConverter((ICollection?)s, target, elementConverter); + return true; + } + + if (target is { IsGenericType: true, IsClass: true } && + IsGenericCollection(target)) { - converter = source => GenericListConverter( - (ICollection?)source, target, elementConverter); + var converterMethod = _collectionConvert.MakeGenericMethod(targetElement); + converter = s => converterMethod.Invoke(null, new[] { s, target, elementConverter }); return true; } } @@ -77,6 +106,7 @@ internal sealed class ListTypeConverter : IChangeTypeProvider Action addToDestination) { var i = 0; + foreach (var item in source) { addToDestination(item, i++); @@ -94,9 +124,7 @@ internal sealed class ListTypeConverter : IChangeTypeProvider } var array = Array.CreateInstance(elementType, input.Count); - ChangeListType( - input, - (item, index) => array.SetValue(elementConverter(item), index)); + ChangeListType(input, (item, index) => array.SetValue(elementConverter(item), index)); return array; } @@ -111,10 +139,25 @@ internal sealed class ListTypeConverter : IChangeTypeProvider } var list = (IList)Activator.CreateInstance(listType)!; - ChangeListType(input, (item, index) => list.Add(elementConverter(item))); + ChangeListType(input, (item, _) => list.Add(elementConverter(item))); return list; } + private static object? GenericCollectionConverter( + ICollection? input, + Type listType, + ChangeType elementConverter) + { + if (input is null) + { + return null; + } + + var collection = (ICollection)Activator.CreateInstance(listType)!; + ChangeListType(input, (item, _) => collection.Add((T)elementConverter(item))); + return collection; + } + private static object? GenericDictionaryConverter( ICollection? input, ChangeType elementConverter) @@ -128,7 +171,49 @@ internal sealed class ListTypeConverter : IChangeTypeProvider var list = (ICollection>)new Dictionary(); ChangeListType( input, - (item, index) => list.Add((KeyValuePair)elementConverter(item)!)); + (item, _) => list.Add((KeyValuePair)elementConverter(item)!)); return list; } + + private static object? HashSetConverter( + ICollection? input, + ChangeType elementConverter) + { + if (input is null) + { + return null; + } + + var set = new HashSet(); + + foreach (var value in input) + { + set.Add((TValue)elementConverter(value)!); + } + + return set; + } + + private static bool IsGenericCollection(Type type) + { + var interfaces = type.GetInterfaces(); +#if NET6_0_OR_GREATER + ref var start = ref MemoryMarshal.GetArrayDataReference(interfaces); +#else + ref var start = ref MemoryMarshal.GetReference(interfaces.AsSpan()); +#endif + + for (var i = 0; i < interfaces.Length; i++) + { + var interfaceType = Unsafe.Add(ref start, i); + + if (interfaceType.IsGenericType && + interfaceType.GetGenericTypeDefinition() == typeof(ISet<>)) + { + return true; + } + } + + return false; + } } diff --git a/src/HotChocolate/Core/test/Execution.Tests/CodeFirstTests.cs b/src/HotChocolate/Core/test/Execution.Tests/CodeFirstTests.cs index 4df5638186d..28fa19778e2 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/CodeFirstTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/CodeFirstTests.cs @@ -1,8 +1,4 @@ using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using HotChocolate.Language; using Microsoft.Extensions.DependencyInjection; using HotChocolate.Resolvers; diff --git a/src/HotChocolate/Core/test/Execution.Tests/HotChocolate.Execution.Tests.csproj b/src/HotChocolate/Core/test/Execution.Tests/HotChocolate.Execution.Tests.csproj index ae11dedc7e5..bc27600a1db 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/HotChocolate.Execution.Tests.csproj +++ b/src/HotChocolate/Core/test/Execution.Tests/HotChocolate.Execution.Tests.csproj @@ -3,6 +3,7 @@ HotChocolate.Execution.Tests HotChocolate.Execution + enable diff --git a/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/TypeConverterTests.cs b/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/TypeConverterTests.cs index 55cdf0724db..dc9f1333688 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/TypeConverterTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/Integration/TypeConverter/TypeConverterTests.cs @@ -1,12 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using HotChocolate.Tests; +using HotChocolate.Tests; using HotChocolate.Types; using HotChocolate.Utilities; using Microsoft.Extensions.DependencyInjection; using Snapshooter.Xunit; -using Xunit; using static HotChocolate.Tests.TestHelper; namespace HotChocolate.Execution.Integration.TypeConverter; @@ -26,12 +22,13 @@ public async Task VariablesAreCoercedToTypesOtherThanTheDefinedClrTypes() number } }", - request: r => r.AddVariableValue("a", + request: r => r.AddVariableValue( + "a", new Dictionary { - {"id", "934b987bc0d842bbabfd8a3b3f8b476e"}, - {"time", "2018-05-29T01:00Z"}, - {"number", (byte)123} + { "id", "934b987bc0d842bbabfd8a3b3f8b476e" }, + { "time", "2018-05-29T01:00Z" }, + { "number", (byte)123 } }), configure: c => c.AddQueryType()) .MatchSnapshotAsync(); @@ -79,12 +76,13 @@ public async Task VariableIsPartlyNotSerializedAndMustBeConvertedToClrType() number } }", - request: r => r.AddVariableValue("a", + request: r => r.AddVariableValue( + "a", new Dictionary { - {"id", "934b987bc0d842bbabfd8a3b3f8b476e"}, - {"time", "2018-05-29T01:00Z"}, - {"number", (byte)123} + { "id", "934b987bc0d842bbabfd8a3b3f8b476e" }, + { "time", "2018-05-29T01:00Z" }, + { "number", (byte)123 } }), configure: c => c.AddQueryType()) .MatchSnapshotAsync(); @@ -141,8 +139,6 @@ public void Register_Multiple_TypeConverters_As_Service() Assert.Equal("a_123", conversion.Convert('a')); } - - [Fact] public void Convert_Null_To_Value_Type_Default() { @@ -152,8 +148,58 @@ public void Convert_Null_To_Value_Type_Default() Assert.Equal(Guid.Empty, empty); } - public class QueryType - : ObjectType + [Fact] + public async Task UseSet_As_InputType() + { + var result = + await new ServiceCollection() + .AddGraphQLServer() + .AddQueryType() + .ExecuteRequestAsync("{ set(set: [\"abc\", \"abc\"]) }"); + + CookieCrumble.SnapshotExtensions.MatchInlineSnapshot( + result, + """ + { + "data": { + "set": [ + "abc" + ] + } + } + """); + } + + [Fact] + public async Task UseHashSet_As_InputType() + { + var result = + await new ServiceCollection() + .AddGraphQLServer() + .AddQueryType() + .ExecuteRequestAsync("{ set2(set: [\"abc\", \"abc\"]) }"); + + CookieCrumble.SnapshotExtensions.MatchInlineSnapshot( + result, + """ + { + "data": { + "set2": [ + "abc" + ] + } + } + """); + } + + public class QuerySet + { + public ISet Set(ISet set) => set; + + public HashSet Set2(HashSet set) => set; + } + + public class QueryType : ObjectType { protected override void Configure( IObjectTypeDescriptor descriptor) @@ -182,11 +228,9 @@ public class Foo [GraphQLType(typeof(NonNullType))] public Guid Id { get; set; } - [GraphQLType(typeof(DateTimeType))] - public DateTime? Time { get; set; } + [GraphQLType(typeof(DateTimeType))] public DateTime? Time { get; set; } - [GraphQLType(typeof(LongType))] - public int? Number { get; set; } + [GraphQLType(typeof(LongType))] public int? Number { get; set; } } public class IntToStringConverter : IChangeTypeProvider @@ -207,4 +251,4 @@ public class IntToStringConverter : IChangeTypeProvider return false; } } -} \ No newline at end of file +} diff --git a/src/HotChocolate/Core/test/Execution.Tests/StreamTests.cs b/src/HotChocolate/Core/test/Execution.Tests/StreamTests.cs index 3812a7ec755..bb987867cae 100644 --- a/src/HotChocolate/Core/test/Execution.Tests/StreamTests.cs +++ b/src/HotChocolate/Core/test/Execution.Tests/StreamTests.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using CookieCrumble; +using Xunit; namespace HotChocolate.Execution; diff --git a/src/HotChocolate/Core/test/Types.Tests/CodeFirstTests.cs b/src/HotChocolate/Core/test/Types.Tests/CodeFirstTests.cs index 13b20946e7b..02189833e61 100644 --- a/src/HotChocolate/Core/test/Types.Tests/CodeFirstTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/CodeFirstTests.cs @@ -9,7 +9,6 @@ using HotChocolate.Types; using Microsoft.Extensions.DependencyInjection; using Snapshooter.Xunit; -using Xunit; namespace HotChocolate;