From 67340fdeaa5df733019cf4f86ac0d7946252b534 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Sun, 26 Jul 2020 13:35:06 +0200 Subject: [PATCH 1/3] Added initial implementation --- .../Core/Execution/Utilities/ArgumentValue.cs | 22 ++- .../Utilities/ArgumentVariableValue.cs | 6 +- .../Execution/Utilities/FieldCollector.cs | 9 +- .../Execution/Utilities/FieldSelection.cs | 6 +- .../ResolverContext.CoerceArgumentValue.cs | 4 + .../Descriptors/DescriptorContextTests.cs | 24 ++- .../Types/InterfaceTypeExtensionTests.cs | 2 +- .../Types/Relay/IdAttributeTests.cs | 53 ++++++ .../IdAttributeTests.Id_On_Arguments.snap | 1 + src/Core/Types/SchemaBuilder.Create.cs | 7 +- src/Core/Types/SchemaBuilder.Lazy.cs | 3 +- src/Core/Types/Types/Argument.cs | 8 +- .../Types/Attributes/SubscribeAttribute.cs | 2 +- src/Core/Types/Types/Contracts/IInputField.cs | 16 ++ .../Conventions/DescriptorContext.cs | 21 ++- .../Conventions/IDescriptorContext.cs | 3 + .../Definitions/ArgumentDefinition.cs | 3 + src/Core/Types/Types/InputField.cs | 6 +- src/Core/Types/Types/InputObjectType.cs | 8 - .../PagingObjectFieldDescriptorExtensions.cs | 12 +- .../Types/Types/Relay/FieldValueSerializer.cs | 140 ++++++++++++++ src/Core/Types/Types/Relay/IDAttribute.cs | 174 ++++++++++++++++++ src/Core/Types/Types/Relay/IdSerializer.cs | 1 - .../Types/Utilities/ExtendedTypeRewriter.cs | 70 +++++++ .../InputObjectToDictionaryConverter.cs | 7 +- .../InputObjectToObjectValueConverter.cs | 10 +- .../Serialization/InputObjectParserHelper.cs | 2 + 27 files changed, 572 insertions(+), 48 deletions(-) create mode 100644 src/Core/Types.Tests/Types/Relay/IdAttributeTests.cs create mode 100644 src/Core/Types.Tests/Types/Relay/__snapshots__/IdAttributeTests.Id_On_Arguments.snap create mode 100644 src/Core/Types/Types/Relay/FieldValueSerializer.cs create mode 100644 src/Core/Types/Types/Relay/IDAttribute.cs diff --git a/src/Core/Core/Execution/Utilities/ArgumentValue.cs b/src/Core/Core/Execution/Utilities/ArgumentValue.cs index d88da749443..bea7fe69a95 100644 --- a/src/Core/Core/Execution/Utilities/ArgumentValue.cs +++ b/src/Core/Core/Execution/Utilities/ArgumentValue.cs @@ -6,11 +6,16 @@ namespace HotChocolate.Execution { public readonly struct ArgumentValue { - public ArgumentValue(IInputType type, ValueKind kind, object value) + public ArgumentValue( + IInputType type, + ValueKind kind, + object value, + IFieldValueSerializer serializer) { Type = type ?? throw new ArgumentNullException(nameof(type)); - Value = value; Kind = kind; + Value = value; + Serializer = serializer; Error = null; Literal = null; } @@ -22,14 +27,19 @@ public ArgumentValue(IInputType type, IError error) Kind = null; Value = null; Literal = null; + Serializer = null; } - public ArgumentValue(IInputType type, ValueKind kind, IValueNode literal) + public ArgumentValue( + IInputType type, + ValueKind kind, + IValueNode literal, + IFieldValueSerializer serializer) { Type = type ?? throw new ArgumentNullException(nameof(type)); - Literal = literal - ?? throw new ArgumentNullException(nameof(literal)); + Literal = literal ?? throw new ArgumentNullException(nameof(literal)); Kind = kind; + Serializer = serializer; Value = null; Error = null; } @@ -43,5 +53,7 @@ public ArgumentValue(IInputType type, ValueKind kind, IValueNode literal) public IValueNode Literal { get; } public IError Error { get; } + + public IFieldValueSerializer Serializer { get; } } } diff --git a/src/Core/Core/Execution/Utilities/ArgumentVariableValue.cs b/src/Core/Core/Execution/Utilities/ArgumentVariableValue.cs index f3d23c3eceb..c3e93917ee2 100644 --- a/src/Core/Core/Execution/Utilities/ArgumentVariableValue.cs +++ b/src/Core/Core/Execution/Utilities/ArgumentVariableValue.cs @@ -7,11 +7,13 @@ internal readonly struct ArgumentVariableValue public ArgumentVariableValue( IInputType type, NameString variableName, - object defaultValue) + object defaultValue, + IFieldValueSerializer serializer) { Type = type; VariableName = variableName; DefaultValue = defaultValue; + Serializer = serializer; } public IInputType Type { get; } @@ -19,5 +21,7 @@ public ArgumentVariableValue( public NameString VariableName { get; } public object DefaultValue { get; } + + public IFieldValueSerializer Serializer { get; } } } diff --git a/src/Core/Core/Execution/Utilities/FieldCollector.cs b/src/Core/Core/Execution/Utilities/FieldCollector.cs index 7e1986c2f49..cdaac3854ca 100644 --- a/src/Core/Core/Execution/Utilities/FieldCollector.cs +++ b/src/Core/Core/Execution/Utilities/FieldCollector.cs @@ -288,7 +288,8 @@ private void CoerceArgumentValue( new ArgumentVariableValue( argument.Type, variable.Name.Value, - defaultValue); + defaultValue, + argument.Serializer); } else { @@ -340,7 +341,8 @@ private void CreateArgumentValue( new ArgumentValue( argument.Type, literal.GetValueKind(), - ParseLiteral(argument.Type, literal)); + ParseLiteral(argument.Type, literal), + argument.Serializer); } else { @@ -348,7 +350,8 @@ private void CreateArgumentValue( new ArgumentValue( argument.Type, literal.GetValueKind(), - literal); + literal, + argument.Serializer); } } diff --git a/src/Core/Core/Execution/Utilities/FieldSelection.cs b/src/Core/Core/Execution/Utilities/FieldSelection.cs index 3bc7fa34be4..339fda68c19 100644 --- a/src/Core/Core/Execution/Utilities/FieldSelection.cs +++ b/src/Core/Core/Execution/Utilities/FieldSelection.cs @@ -116,12 +116,14 @@ public IReadOnlyDictionary CoerceArguments( if (value is IValueNode literal) { kind = literal.GetValueKind(); - args[var.Key] = new ArgumentValue(var.Value.Type, kind, literal); + args[var.Key] = new ArgumentValue( + var.Value.Type, kind, literal, var.Value.Serializer); } else { Scalars.TryGetKind(value, out kind); - args[var.Key] = new ArgumentValue(var.Value.Type, kind, value); + args[var.Key] = new ArgumentValue( + var.Value.Type, kind, value, var.Value.Serializer); } } else diff --git a/src/Core/Core/Execution/Utilities/ResolverContext.CoerceArgumentValue.cs b/src/Core/Core/Execution/Utilities/ResolverContext.CoerceArgumentValue.cs index 5d192d40945..9547c9d9ba5 100644 --- a/src/Core/Core/Execution/Utilities/ResolverContext.CoerceArgumentValue.cs +++ b/src/Core/Core/Execution/Utilities/ResolverContext.CoerceArgumentValue.cs @@ -82,6 +82,10 @@ private T CoerceArgumentValue( return default; } + value = argumentValue.Serializer is null + ? value + : argumentValue.Serializer.Deserialize(value); + if (value is T resolved) { return resolved; diff --git a/src/Core/Types.Tests/Types/Descriptors/DescriptorContextTests.cs b/src/Core/Types.Tests/Types/Descriptors/DescriptorContextTests.cs index 671b901f220..55d804fac0c 100644 --- a/src/Core/Types.Tests/Types/Descriptors/DescriptorContextTests.cs +++ b/src/Core/Types.Tests/Types/Descriptors/DescriptorContextTests.cs @@ -14,7 +14,11 @@ public void Create_ServicesNull_ArgumentException() var options = new SchemaOptions(); // act - Action action = () => DescriptorContext.Create(options, null); + Action action = () => + DescriptorContext.Create( + options, + null, + () => throw new NotSupportedException()); // assert Assert.Throws(action); @@ -24,10 +28,14 @@ public void Create_ServicesNull_ArgumentException() public void Create_OptionsNull_ArgumentException() { // arrange - var service = new EmptyServiceProvider(); + var services = new EmptyServiceProvider(); // act - Action action = () => DescriptorContext.Create(null, service); + Action action = () => + DescriptorContext.Create( + null, + services, + () => throw new NotSupportedException()); // assert Assert.Throws(action); @@ -45,7 +53,10 @@ public void Create_With_Custom_NamingConventions() // act DescriptorContext context = - DescriptorContext.Create(options, services); + DescriptorContext.Create( + options, + services, + () => throw new NotSupportedException()); // assert Assert.Equal(naming, context.Naming); @@ -65,7 +76,10 @@ public void Create_With_Custom_TypeInspector() // act DescriptorContext context = - DescriptorContext.Create(options, services); + DescriptorContext.Create( + options, + services, + () => throw new NotSupportedException()); // assert Assert.Equal(inspector, context.Inspector); diff --git a/src/Core/Types.Tests/Types/InterfaceTypeExtensionTests.cs b/src/Core/Types.Tests/Types/InterfaceTypeExtensionTests.cs index fb500367a73..d07858ad2fc 100644 --- a/src/Core/Types.Tests/Types/InterfaceTypeExtensionTests.cs +++ b/src/Core/Types.Tests/Types/InterfaceTypeExtensionTests.cs @@ -29,7 +29,7 @@ public void InterfaceTypeExtension_AddField() [Obsolete] [Fact] - public void InterfaceTypeExtension_DepricateField() + public void InterfaceTypeExtension_DeprecateField() { // arrange FieldResolverDelegate resolver = diff --git a/src/Core/Types.Tests/Types/Relay/IdAttributeTests.cs b/src/Core/Types.Tests/Types/Relay/IdAttributeTests.cs new file mode 100644 index 00000000000..7b51dc0942c --- /dev/null +++ b/src/Core/Types.Tests/Types/Relay/IdAttributeTests.cs @@ -0,0 +1,53 @@ +using System; +using System.Threading.Tasks; +using HotChocolate.Execution; +using Snapshooter.Xunit; +using Xunit; + +namespace HotChocolate.Types.Relay +{ + public class IdAttributeTests + { + [Fact] + public async Task Id_On_Arguments() + { + // arrange + var idSerializer = new IdSerializer(); + string intId = idSerializer.Serialize("Query", 1); + string stringId = idSerializer.Serialize("Query", "abc"); + string guidId = idSerializer.Serialize("Query", Guid.Empty); + + // act + IExecutionResult result = + await SchemaBuilder.New() + .AddQueryType() + .Create() + .MakeExecutable() + .ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery( + @"query foo ($intId: ID! $stringId: ID! $guidId: ID!) { + intId(id: $intId) + stringId(id: $stringId) + guidId(id: $guidId) + }") + .SetVariableValue("intId", intId) + .SetVariableValue("stringId", stringId) + .SetVariableValue("guidId", guidId) + .Create()); + + // assert + result.ToJson().MatchSnapshot(); + } + + public class Query + { + public string IntId([ID] int id) => id.ToString(); + + public string StringId([ID] string id) => id.ToString(); + + public string GuidId([ID] Guid id) => id.ToString(); + + } + } +} \ No newline at end of file diff --git a/src/Core/Types.Tests/Types/Relay/__snapshots__/IdAttributeTests.Id_On_Arguments.snap b/src/Core/Types.Tests/Types/Relay/__snapshots__/IdAttributeTests.Id_On_Arguments.snap new file mode 100644 index 00000000000..fc41d1cd4c0 --- /dev/null +++ b/src/Core/Types.Tests/Types/Relay/__snapshots__/IdAttributeTests.Id_On_Arguments.snap @@ -0,0 +1 @@ +{"data":{"intId":"1","stringId":"abc","guidId":"00000000-0000-0000-0000-000000000000"}} diff --git a/src/Core/Types/SchemaBuilder.Create.cs b/src/Core/Types/SchemaBuilder.Create.cs index 5a15438094d..276d169bed5 100644 --- a/src/Core/Types/SchemaBuilder.Create.cs +++ b/src/Core/Types/SchemaBuilder.Create.cs @@ -16,11 +16,13 @@ public partial class SchemaBuilder { public Schema Create() { + var lazy = new LazySchema(); + IServiceProvider services = _services ?? new EmptyServiceProvider(); DescriptorContext descriptorContext = - DescriptorContext.Create(_options, services); + DescriptorContext.Create(_options, services, () => lazy.Schema); IBindingLookup bindingLookup = _bindingCompiler.Compile(descriptorContext); @@ -28,8 +30,6 @@ public Schema Create() IReadOnlyCollection types = GetTypeReferences(services, bindingLookup); - var lazy = new LazySchema(); - TypeInitializer initializer = CreateTypeInitializer( services, @@ -54,6 +54,7 @@ public Schema Create() schema.CompleteSchema(definition); lazy.Schema = schema; + descriptorContext.TriggerSchemaResolved(); return schema; } diff --git a/src/Core/Types/SchemaBuilder.Lazy.cs b/src/Core/Types/SchemaBuilder.Lazy.cs index 48514724fd6..38413962de1 100644 --- a/src/Core/Types/SchemaBuilder.Lazy.cs +++ b/src/Core/Types/SchemaBuilder.Lazy.cs @@ -15,7 +15,8 @@ public ISchema Schema { if (!_isSet) { - throw new InvalidOperationException(); + throw new InvalidOperationException( + "Schema is not ready yet."); } return _schema; diff --git a/src/Core/Types/Types/Argument.cs b/src/Core/Types/Types/Argument.cs index 54bbee28f65..2b494675bb8 100644 --- a/src/Core/Types/Types/Argument.cs +++ b/src/Core/Types/Types/Argument.cs @@ -1,4 +1,3 @@ -using System; using System.Globalization; using HotChocolate.Configuration; using HotChocolate.Language; @@ -16,11 +15,14 @@ public Argument( : base(definition) { SyntaxNode = definition.SyntaxNode; + Serializer = definition.Serializer; DefaultValue = definition.DefaultValue; } public InputValueDefinitionNode SyntaxNode { get; } + public IFieldValueSerializer Serializer { get; private set; } + public IValueNode DefaultValue { get; private set; } protected override void OnCompleteField( @@ -40,8 +42,8 @@ protected override void OnCompleteField( else { base.OnCompleteField(context, definition); - DefaultValue = FieldInitHelper.CreateDefaultValue( - context, definition, Type); + Serializer = definition.Serializer; + DefaultValue = FieldInitHelper.CreateDefaultValue(context, definition, Type); } } } diff --git a/src/Core/Types/Types/Attributes/SubscribeAttribute.cs b/src/Core/Types/Types/Attributes/SubscribeAttribute.cs index 14e6a099f90..c5a5d0a739e 100644 --- a/src/Core/Types/Types/Attributes/SubscribeAttribute.cs +++ b/src/Core/Types/Types/Attributes/SubscribeAttribute.cs @@ -87,7 +87,7 @@ public override void OnConfigure( { MethodInfo? subscribeResolver = member.DeclaringType?.GetMethod( With, BindingFlags.Public | BindingFlags.Instance); - + if (subscribeResolver is null) { throw SubscribeAttribute_SubscribeResolverNotFound(member, With); diff --git a/src/Core/Types/Types/Contracts/IInputField.cs b/src/Core/Types/Types/Contracts/IInputField.cs index 713061d74d9..696e74a7243 100644 --- a/src/Core/Types/Types/Contracts/IInputField.cs +++ b/src/Core/Types/Types/Contracts/IInputField.cs @@ -1,5 +1,7 @@ using HotChocolate.Language; +#nullable enable + namespace HotChocolate.Types { /// @@ -14,9 +16,23 @@ public interface IInputField /// IInputType Type { get; } + /// + /// Gets the field serializer. + /// + IFieldValueSerializer? Serializer { get; } + /// /// Gets the default value literal of this field. /// IValueNode DefaultValue { get; } } + + public interface IFieldValueSerializer + { + object? Serialize(object? value); + + object? Deserialize(object? value); + + IValueNode Rewrite(IValueNode value); + } } diff --git a/src/Core/Types/Types/Descriptors/Conventions/DescriptorContext.cs b/src/Core/Types/Types/Descriptors/Conventions/DescriptorContext.cs index 53673373164..7806eaed7b9 100644 --- a/src/Core/Types/Types/Descriptors/Conventions/DescriptorContext.cs +++ b/src/Core/Types/Types/Descriptors/Conventions/DescriptorContext.cs @@ -1,30 +1,45 @@ using System; using HotChocolate.Configuration; +using HotChocolate.Utilities; namespace HotChocolate.Types.Descriptors { public sealed class DescriptorContext : IDescriptorContext { + internal event EventHandler SchemaResolved; + private DescriptorContext( + IServiceProvider services, IReadOnlySchemaOptions options, + Func resolveSchema, INamingConventions naming, ITypeInspector inspector) { + Services = services; Options = options; + ResolveSchema = resolveSchema; Naming = naming; Inspector = inspector; } + public IServiceProvider Services { get; } + public IReadOnlySchemaOptions Options { get; } public INamingConventions Naming { get; } public ITypeInspector Inspector { get; } + internal Func ResolveSchema { get; } + + internal void TriggerSchemaResolved() => + SchemaResolved?.Invoke(this, EventArgs.Empty); + public static DescriptorContext Create( IReadOnlySchemaOptions options, - IServiceProvider services) + IServiceProvider services, + Func resolveSchema) { if (options == null) { @@ -57,13 +72,15 @@ public static DescriptorContext Create( inspector = new DefaultTypeInspector(); } - return new DescriptorContext(options, naming, inspector); + return new DescriptorContext(services, options, resolveSchema, naming, inspector); } public static DescriptorContext Create() { return new DescriptorContext( + new EmptyServiceProvider(), new SchemaOptions(), + () => throw new NotSupportedException(), new DefaultNamingConventions(), new DefaultTypeInspector()); } diff --git a/src/Core/Types/Types/Descriptors/Conventions/IDescriptorContext.cs b/src/Core/Types/Types/Descriptors/Conventions/IDescriptorContext.cs index 1db69d1e202..1181178f12a 100644 --- a/src/Core/Types/Types/Descriptors/Conventions/IDescriptorContext.cs +++ b/src/Core/Types/Types/Descriptors/Conventions/IDescriptorContext.cs @@ -1,9 +1,12 @@ +using System; using HotChocolate.Configuration; namespace HotChocolate.Types.Descriptors { public interface IDescriptorContext { + IServiceProvider Services { get; } + IReadOnlySchemaOptions Options { get; } INamingConventions Naming { get; } diff --git a/src/Core/Types/Types/Descriptors/Definitions/ArgumentDefinition.cs b/src/Core/Types/Types/Descriptors/Definitions/ArgumentDefinition.cs index 427c99b4d51..52308f2c4a3 100644 --- a/src/Core/Types/Types/Descriptors/Definitions/ArgumentDefinition.cs +++ b/src/Core/Types/Types/Descriptors/Definitions/ArgumentDefinition.cs @@ -13,5 +13,8 @@ public class ArgumentDefinition public object? NativeDefaultValue { get; set; } public ParameterInfo? Parameter { get; set; } + + public IFieldValueSerializer? Serializer { get; set; } + } } diff --git a/src/Core/Types/Types/InputField.cs b/src/Core/Types/Types/InputField.cs index f7933030b1c..30b3e3f83a4 100644 --- a/src/Core/Types/Types/InputField.cs +++ b/src/Core/Types/Types/InputField.cs @@ -34,6 +34,8 @@ public InputField(InputFieldDefinition definition) public InputValueDefinitionNode SyntaxNode { get; } + public IFieldValueSerializer Serializer { get; private set; } + public IValueNode DefaultValue { get; private set; } internal protected PropertyInfo Property { get; } @@ -156,8 +158,8 @@ protected override void OnCompleteField( InputFieldDefinition definition) { base.OnCompleteField(context, definition); - DefaultValue = FieldInitHelper.CreateDefaultValue( - context, definition, Type); + Serializer = definition.Serializer; + DefaultValue = FieldInitHelper.CreateDefaultValue(context, definition, Type); } } } diff --git a/src/Core/Types/Types/InputObjectType.cs b/src/Core/Types/Types/InputObjectType.cs index cdec39f58bc..259b04945d0 100644 --- a/src/Core/Types/Types/InputObjectType.cs +++ b/src/Core/Types/Types/InputObjectType.cs @@ -40,8 +40,6 @@ public InputObjectType( public FieldCollection Fields { get; private set; } - #region IInputType - public bool IsInstanceOfType(IValueNode literal) { if (literal == null) @@ -185,10 +183,6 @@ public virtual bool TryDeserialize(object serialized, out object value) } } - #endregion - - #region Initialization - protected override InputObjectTypeDefinition CreateDefinition( IInitializationContext context) { @@ -263,7 +257,5 @@ protected virtual void OnCompleteFields( fields.Add(new InputField(fieldDefinition)); } } - - #endregion } } diff --git a/src/Core/Types/Types/Relay/Extensions/PagingObjectFieldDescriptorExtensions.cs b/src/Core/Types/Types/Relay/Extensions/PagingObjectFieldDescriptorExtensions.cs index ff738c5dc36..c3220fa0b70 100644 --- a/src/Core/Types/Types/Relay/Extensions/PagingObjectFieldDescriptorExtensions.cs +++ b/src/Core/Types/Types/Relay/Extensions/PagingObjectFieldDescriptorExtensions.cs @@ -33,7 +33,7 @@ private static IObjectFieldDescriptor UsePaging( .Type>() .Use(placeholder) .Extend() - .OnBeforeCompletion((context, defintion) => + .OnBeforeCompletion((context, definition) => { if (entityType is null) { @@ -44,15 +44,15 @@ private static IObjectFieldDescriptor UsePaging( entityType = ((IHasClrType)type.NamedType()).ClrType; } - MemberInfo member = defintion.ResolverMember ?? defintion.Member; - Type resultType = defintion.Resolver is { } && defintion.ResultType is { } - ? defintion.ResultType + MemberInfo member = definition.ResolverMember ?? definition.Member; + Type resultType = definition.Resolver is { } && definition.ResultType is { } + ? definition.ResultType : member.GetReturnType(true) ?? typeof(object); resultType = UnwrapType(resultType); FieldMiddleware middleware = CreateMiddleware(resultType, entityType); - int index = defintion.MiddlewareComponents.IndexOf(placeholder); - defintion.MiddlewareComponents[index] = middleware; + int index = definition.MiddlewareComponents.IndexOf(placeholder); + definition.MiddlewareComponents[index] = middleware; }) .DependsOn(); diff --git a/src/Core/Types/Types/Relay/FieldValueSerializer.cs b/src/Core/Types/Types/Relay/FieldValueSerializer.cs new file mode 100644 index 00000000000..17a42a611fc --- /dev/null +++ b/src/Core/Types/Types/Relay/FieldValueSerializer.cs @@ -0,0 +1,140 @@ +using System.Collections; +using System.Collections.Generic; +using HotChocolate.Execution; +using HotChocolate.Language; +using HotChocolate.Utilities; + +#nullable enable + +namespace HotChocolate.Types.Relay +{ + internal sealed class FieldValueSerializer : IFieldValueSerializer + { + private readonly NameString _typeName; + private readonly IIdSerializer _innerSerializer; + private readonly bool _validate; + private readonly bool _list; + private NameString _schemaName; + + public FieldValueSerializer( + NameString typeName, + IIdSerializer innerSerializer, + bool validateType, + bool isListType) + { + _typeName = typeName; + _innerSerializer = innerSerializer; + _validate = validateType; + _list = isListType; + } + + public void Initialize(NameString schemaName) + { + _schemaName = schemaName; + } + + public object? Deserialize(object? value) + { + if (value is null) + { + return null; + } + else if (value is string s) + { + IdValue id = _innerSerializer.Deserialize(s); + + if (!_validate || _typeName.Equals(id.TypeName)) + { + return id.Value; + } + + throw new QueryException( + ErrorBuilder.New() + .SetMessage("The ID `{0}` is not an ID of `{1}`.", s, _typeName) + .Build()); + } + else if (value is IEnumerable stringEnumerable) + { + var list = new List(); + + foreach (string sv in stringEnumerable) + { + IdValue id = _innerSerializer.Deserialize(sv); + + if (!_validate || _typeName.Equals(id.TypeName)) + { + list.Add(id.Value); + } + } + + return list; + } + + throw new QueryException( + ErrorBuilder.New() + .SetMessage("The specified value is not a valid ID value.") + .Build()); + } + + public object? Serialize(object? value) + { + if (value is null) + { + return null; + } + + if (_list && DotNetTypeInfoFactory.IsListType(value.GetType())) + { + var list = new List(); + + foreach (object item in (IEnumerable)value) + { + list.Add(_innerSerializer.Serialize(_schemaName, _typeName, item)); + } + + return list; + } + + return _innerSerializer.Serialize(_schemaName, _typeName, value); + } + + public IValueNode Rewrite(IValueNode value) + { + if (value.Kind == NodeKind.NullValue) + { + return value; + } + + if (value is ListValueNode list) + { + var items = new List(); + + foreach (IValueNode item in list.Items) + { + items.Add(Rewrite(item)); + } + + return list.WithItems(items); + } + + switch (value) + { + case StringValueNode stringValue: + return stringValue.WithValue( + _innerSerializer.Serialize( + _schemaName, _typeName, stringValue.Value)); + + case IntValueNode intValue: + return new StringValueNode( + _innerSerializer.Serialize( + _schemaName, _typeName, long.Parse(intValue.Value))); + + default: + throw new QueryException( + ErrorBuilder.New() + .SetMessage("The specified literal is not a valid ID value.") + .Build()); + } + } + } +} \ No newline at end of file diff --git a/src/Core/Types/Types/Relay/IDAttribute.cs b/src/Core/Types/Types/Relay/IDAttribute.cs new file mode 100644 index 00000000000..79544c29496 --- /dev/null +++ b/src/Core/Types/Types/Relay/IDAttribute.cs @@ -0,0 +1,174 @@ +using System; +using System.Reflection; +using System.Threading.Tasks; +using HotChocolate.Configuration; +using HotChocolate.Resolvers; +using HotChocolate.Types.Descriptors; +using HotChocolate.Types.Descriptors.Definitions; +using HotChocolate.Utilities; + +#nullable enable + +namespace HotChocolate.Types.Relay +{ + public class IDAttribute : DescriptorAttribute + { + public IDAttribute(string? typeName = null) + { + TypeName = typeName; + } + + public string? TypeName { get; } + + protected internal override void TryConfigure( + IDescriptorContext context, + IDescriptor descriptor, + ICustomAttributeProvider element) + { + if (descriptor is IInputFieldDescriptor ifd + && element is PropertyInfo) + { + var internalContext = (DescriptorContext)context; + ifd.Extend().OnBeforeCreate(RewriteInputFieldType); + ifd.Extend().OnBeforeCompletion( + (c, d) => AddSerializerToInputField(internalContext, c, d, TypeName)); + } + + if (descriptor is IArgumentDescriptor ad + && element is ParameterInfo) + { + var internalContext = (DescriptorContext)context; + ad.Extend().OnBeforeCreate(RewriteInputFieldType); + ad.Extend().OnBeforeCompletion( + (c, d) => AddSerializerToInputField(internalContext, c, d, TypeName)); + } + + if (descriptor is IObjectFieldDescriptor ofd + && element is MemberInfo) + { + var internalContext = (DescriptorContext)context; + FieldMiddleware placeholder = n => c => Task.CompletedTask; + ofd.Use(placeholder); + ofd.Extend().OnBeforeCreate(RewriteObjectFieldType); + ofd.Extend().OnBeforeCompletion( + (c, d) => AddSerializerToObjectField( + internalContext, c, d, placeholder, TypeName)); + } + + if (descriptor is IInterfaceFieldDescriptor infd + && element is MemberInfo) + { + infd.Extend().OnBeforeCreate(RewriteInterfaceFieldType); + } + } + + private static void RewriteInputFieldType( + ArgumentDefinition definition) + { + var typeReference = (IClrTypeReference)definition.Type; + Type rewritten = ExtendedTypeRewriter.RewriteToSchemaType( + ExtendedType.FromType(typeReference.Type), + typeof(IdType)); + definition.Type = typeReference.WithType(rewritten); + } + + private static void AddSerializerToInputField( + DescriptorContext descriptorContext, + ICompletionContext completionContext, + ArgumentDefinition definition, + string? typeName) + { + var innerSerializer = + (IIdSerializer)descriptorContext.Services.GetService(typeof(IIdSerializer)) ?? + new IdSerializer(); + + FieldValueSerializer serializer = CreateSerializer( + descriptorContext, + completionContext, + ((ClrTypeReference)definition.Type).Type, + typeName); + + definition.Serializer = serializer; + + descriptorContext.SchemaResolved += (sender, args) => + { + ISchema schema = descriptorContext.ResolveSchema(); + + NameString schemaName = schema.Name.HasValue + ? schema.Name + : Schema.DefaultName; + + serializer.Initialize(schemaName); + }; + } + + private static void RewriteObjectFieldType( + ObjectFieldDefinition definition) + { + var typeReference = (IClrTypeReference)definition.Type; + Type rewritten = ExtendedTypeRewriter.RewriteToSchemaType( + ExtendedType.FromType(typeReference.Type), + typeof(IdType)); + definition.Type = typeReference.WithType(rewritten); + } + + private static void RewriteInterfaceFieldType( + InterfaceFieldDefinition definition) + { + var typeReference = (IClrTypeReference)definition.Type; + Type rewritten = ExtendedTypeRewriter.RewriteToSchemaType( + ExtendedType.FromType(typeReference.Type), + typeof(IdType)); + definition.Type = typeReference.WithType(rewritten); + } + + private static void AddSerializerToObjectField( + DescriptorContext descriptorContext, + ICompletionContext completionContext, + ObjectFieldDefinition definition, + FieldMiddleware placeholder, + string? typeName) + { + FieldValueSerializer serializer = CreateSerializer( + descriptorContext, + completionContext, + ((ClrTypeReference)definition.Type).Type, + typeName); + + int index = definition.MiddlewareComponents.IndexOf(placeholder); + definition.MiddlewareComponents[index] = next => async context => + { + await next(context).ConfigureAwait(false); + context.Result = serializer.Serialize(context.Result); + }; + + descriptorContext.SchemaResolved += (sender, args) => + { + ISchema schema = descriptorContext.ResolveSchema(); + + NameString schemaName = schema.Name.HasValue + ? schema.Name + : Schema.DefaultName; + + serializer.Initialize(schemaName); + }; + } + + private static FieldValueSerializer CreateSerializer( + DescriptorContext descriptorContext, + ICompletionContext completionContext, + Type type, + string? typeName) + { + var innerSerializer = + (IIdSerializer)descriptorContext.Services.GetService(typeof(IIdSerializer)) ?? + new IdSerializer(); + + return new FieldValueSerializer( + typeName is null ? typeName : completionContext.Type.Name.Value, + innerSerializer, + typeName is { }, + DotNetTypeInfoFactory.IsListType(type)); + } + } +} \ No newline at end of file diff --git a/src/Core/Types/Types/Relay/IdSerializer.cs b/src/Core/Types/Types/Relay/IdSerializer.cs index 69b96e3556c..3176dc2b92c 100644 --- a/src/Core/Types/Types/Relay/IdSerializer.cs +++ b/src/Core/Types/Types/Relay/IdSerializer.cs @@ -3,7 +3,6 @@ using System.Buffers; using System.Buffers.Text; using HotChocolate.Language; -using HotChocolate.Properties; namespace HotChocolate.Types.Relay { diff --git a/src/Core/Types/Utilities/ExtendedTypeRewriter.cs b/src/Core/Types/Utilities/ExtendedTypeRewriter.cs index 4c20833eb34..fa3fb33caa4 100644 --- a/src/Core/Types/Utilities/ExtendedTypeRewriter.cs +++ b/src/Core/Types/Utilities/ExtendedTypeRewriter.cs @@ -68,6 +68,54 @@ public static Type Rewrite(IExtendedType type, params Nullable[] nullable) return rewritten!; } + public static Type RewriteToSchemaType(IExtendedType type, Type namedType) + { + var components = new Stack(); + IExtendedType? current = type; + + do + { + if (!IsNonEssentialComponent(current)) + { + bool makeNullable = current.IsNullable; + + if (current.IsNullable == makeNullable) + { + components.Push(current); + } + else + { + components.Push(new ExtendedType( + current.Type, makeNullable, + current.Kind, current.TypeArguments)); + } + } + current = GetInnerType(current); + } while (current != null && components.Count < 7); + + current = null; + Type? rewritten = null; + + while (components.Count > 0) + { + if (rewritten is null) + { + current = components.Pop(); + rewritten = RewriteSchemaType(namedType, current.IsNullable); + } + else + { + current = components.Pop(); + rewritten = current.IsArray && !typeof(IType).IsAssignableFrom(rewritten) + ? rewritten.MakeArrayType() + : MakeListSchemaType(rewritten); + rewritten = RewriteSchemaType(rewritten, current.IsNullable); + } + } + + return rewritten!; + } + private static Type Rewrite(Type type, bool isNullable) { if (type.IsValueType) @@ -94,17 +142,39 @@ private static Type Rewrite(Type type, bool isNullable) } } + private static Type RewriteSchemaType(Type type, bool isNullable) + { + if (isNullable) + { + return type; + } + else + { + return MakeNonNullSchemaType(type); + } + } + private static Type MakeListType(Type elementType) { return typeof(List<>).MakeGenericType(elementType); } + private static Type MakeListSchemaType(Type elementType) + { + return typeof(ListType<>).MakeGenericType(elementType); + } + private static Type MakeNonNullType(Type nullableType) { Type wrapper = typeof(NativeType<>).MakeGenericType(nullableType); return typeof(NonNullType<>).MakeGenericType(wrapper); } + private static Type MakeNonNullSchemaType(Type nullableType) + { + return typeof(NonNullType<>).MakeGenericType(nullableType); + } + private static bool IsNonEssentialComponent(IExtendedType type) { diff --git a/src/Core/Types/Utilities/InputObjectToDictionaryConverter.cs b/src/Core/Types/Utilities/InputObjectToDictionaryConverter.cs index 7cba9d0195b..33f0bdc57a4 100644 --- a/src/Core/Types/Utilities/InputObjectToDictionaryConverter.cs +++ b/src/Core/Types/Utilities/InputObjectToDictionaryConverter.cs @@ -80,7 +80,12 @@ private void VisitInputObject( foreach (InputField field in type.Fields) { object fieldValue = field.GetValue(obj); - Action setField = value => dict[field.Name] = value; + Action setField = value => + { + value = field.Serializer is null + ? value : field.Serializer.Serialize(value); + dict[field.Name] = value; + }; VisitValue(field.Type, fieldValue, setField, processed); } } diff --git a/src/Core/Types/Utilities/InputObjectToObjectValueConverter.cs b/src/Core/Types/Utilities/InputObjectToObjectValueConverter.cs index 8f849efae03..5fb2656ab96 100644 --- a/src/Core/Types/Utilities/InputObjectToObjectValueConverter.cs +++ b/src/Core/Types/Utilities/InputObjectToObjectValueConverter.cs @@ -31,8 +31,7 @@ public ObjectValueNode Convert( } ObjectValueNode objectValueNode = null; - Action setValue = - value => objectValueNode = (ObjectValueNode)value; + Action setValue = value => objectValueNode = (ObjectValueNode)value; VisitInputObject(type, obj, setValue, new HashSet()); return objectValueNode; } @@ -92,7 +91,7 @@ private void VisitInputObject( foreach (InputField field in type.Fields) { - if(field.TryGetValue(obj, out object fieldValue)) + if (field.TryGetValue(obj, out object fieldValue)) { if (fieldValue is IOptional optional && !optional.HasValue) { @@ -100,7 +99,12 @@ private void VisitInputObject( } Action setField = value => + { + value = field.Serializer is null + ? value + : field.Serializer.Rewrite(value); fields.Add(new ObjectFieldNode(field.Name, value)); + }; VisitValue(field.Type, fieldValue, setField, processed); } } diff --git a/src/Core/Types/Utilities/Serialization/InputObjectParserHelper.cs b/src/Core/Types/Utilities/Serialization/InputObjectParserHelper.cs index f265ee47891..5190c4272d0 100644 --- a/src/Core/Types/Utilities/Serialization/InputObjectParserHelper.cs +++ b/src/Core/Types/Utilities/Serialization/InputObjectParserHelper.cs @@ -58,6 +58,7 @@ private static void Parse( if (type.Fields.TryGetField(fieldValue.Name.Value, out InputField field)) { object value = field.Type.ParseLiteral(fieldValue.Value); + value = field.Serializer is null ? value : field.Serializer.Deserialize(value); target[field.Name] = ConvertValue(field, converter, value); } else @@ -113,6 +114,7 @@ private static void Deserialize( if (type.Fields.TryGetField(fieldValue.Key, out InputField field)) { object value = field.Type.Deserialize(fieldValue.Value); + value = field.Serializer is null ? value : field.Serializer.Deserialize(value); target[field.Name] = ConvertValue(field, converter, value); } else From a7ef12f65a5c45fd2ad0a72399bfdc8d7adfec2d Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Sun, 26 Jul 2020 13:50:39 +0200 Subject: [PATCH 2/3] Fixed more tests --- .../Types/Relay/IdAttributeTests.cs | 61 ++++++++++++++++++- .../IdAttributeTests.Id_On_Objects.snap | 4 ++ ...teTests.Id_Type_Is_Correctly_Inferred.snap | 28 +++++++++ src/Core/Types/Types/Relay/IDAttribute.cs | 4 +- 4 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 src/Core/Types.Tests/Types/Relay/__snapshots__/IdAttributeTests.Id_On_Objects.snap create mode 100644 src/Core/Types.Tests/Types/Relay/__snapshots__/IdAttributeTests.Id_Type_Is_Correctly_Inferred.snap diff --git a/src/Core/Types.Tests/Types/Relay/IdAttributeTests.cs b/src/Core/Types.Tests/Types/Relay/IdAttributeTests.cs index 7b51dc0942c..7946d8deef8 100644 --- a/src/Core/Types.Tests/Types/Relay/IdAttributeTests.cs +++ b/src/Core/Types.Tests/Types/Relay/IdAttributeTests.cs @@ -40,14 +40,71 @@ await SchemaBuilder.New() result.ToJson().MatchSnapshot(); } + [Fact] + public async Task Id_On_Objects() + { + // arrange + var idSerializer = new IdSerializer(); + string someId = idSerializer.Serialize("Some", 1); + + // act + IExecutionResult result = + await SchemaBuilder.New() + .AddQueryType() + .AddType() + .Create() + .MakeExecutable() + .ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery( + @"query foo ($someId: ID!) { + foo(input: { someId: $someId }) { + someId + } + }") + .SetVariableValue("someId", someId) + .Create()); + + // assert + new { + result = result.ToJson(), + someId + }.MatchSnapshot(); + } + + [Fact] + public void Id_Type_Is_Correctly_Inferred() + { + SchemaBuilder.New() + .AddQueryType() + .AddType() + .Create() + .ToString() + .MatchSnapshot(); + } + public class Query { public string IntId([ID] int id) => id.ToString(); - public string StringId([ID] string id) => id.ToString(); - public string GuidId([ID] Guid id) => id.ToString(); + public IFooPayload Foo(FooInput input) => new FooPayload { SomeId = input.SomeId }; + } + + public class FooInput + { + [ID("Some")] public string SomeId { get; set; } + } + + public class FooPayload : IFooPayload + { + [ID("Bar")] public string SomeId { get; set; } + } + + public interface IFooPayload + { + [ID] string SomeId { get; set; } } } } \ No newline at end of file diff --git a/src/Core/Types.Tests/Types/Relay/__snapshots__/IdAttributeTests.Id_On_Objects.snap b/src/Core/Types.Tests/Types/Relay/__snapshots__/IdAttributeTests.Id_On_Objects.snap new file mode 100644 index 00000000000..b358f31b08f --- /dev/null +++ b/src/Core/Types.Tests/Types/Relay/__snapshots__/IdAttributeTests.Id_On_Objects.snap @@ -0,0 +1,4 @@ +{ + "result": "{\"data\":{\"foo\":{\"someId\":\"QmFyCmQx\"}}}", + "someId": "U29tZQppMQ==" +} diff --git a/src/Core/Types.Tests/Types/Relay/__snapshots__/IdAttributeTests.Id_Type_Is_Correctly_Inferred.snap b/src/Core/Types.Tests/Types/Relay/__snapshots__/IdAttributeTests.Id_Type_Is_Correctly_Inferred.snap new file mode 100644 index 00000000000..8afd9b64f41 --- /dev/null +++ b/src/Core/Types.Tests/Types/Relay/__snapshots__/IdAttributeTests.Id_Type_Is_Correctly_Inferred.snap @@ -0,0 +1,28 @@ +schema { + query: Query +} + +interface IFooPayload { + someId: ID +} + +type FooPayload implements IFooPayload { + someId: ID +} + +type Query { + foo(input: FooInput): IFooPayload + guidId(id: ID!): String + intId(id: ID!): String + stringId(id: ID): String +} + +input FooInput { + someId: ID +} + +"The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"4\"`) or integer (such as `4`) input value will be accepted as an ID." +scalar ID + +"The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text." +scalar String diff --git a/src/Core/Types/Types/Relay/IDAttribute.cs b/src/Core/Types/Types/Relay/IDAttribute.cs index 79544c29496..8ad34110c34 100644 --- a/src/Core/Types/Types/Relay/IDAttribute.cs +++ b/src/Core/Types/Types/Relay/IDAttribute.cs @@ -136,7 +136,7 @@ private static void AddSerializerToObjectField( typeName); int index = definition.MiddlewareComponents.IndexOf(placeholder); - definition.MiddlewareComponents[index] = next => async context => + definition.MiddlewareComponents[index] = next => async context => { await next(context).ConfigureAwait(false); context.Result = serializer.Serialize(context.Result); @@ -165,7 +165,7 @@ private static FieldValueSerializer CreateSerializer( new IdSerializer(); return new FieldValueSerializer( - typeName is null ? typeName : completionContext.Type.Name.Value, + typeName is { } ? typeName : completionContext.Type.Name.Value, innerSerializer, typeName is { }, DotNetTypeInfoFactory.IsListType(type)); From cdd17cb0fca739cbbf504ad5254b33b887a924fa Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Sun, 26 Jul 2020 14:34:32 +0200 Subject: [PATCH 3/3] Fixed test issues and inference issues --- src/Core/Types.Tests/Types/ObjectTypeTests.cs | 20 +++++++++++++++++++ .../Types/Relay/IdAttributeTests.cs | 2 +- ...sts.Infer_NonNull_Int_Array_Correctly.snap | 10 ++++++++++ .../Utilities/DotNetTypeInfoFactoryTests.cs | 20 +++++++++++++++++++ .../Utilities/ExtendedTypeRewriterTests.cs | 1 + .../Descriptors/ObjectTypeDescriptorBase~1.cs | 2 -- .../Types/Utilities/DotNetTypeInfoFactory.cs | 11 ++++++++++ 7 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 src/Core/Types.Tests/Types/__snapshots__/ObjectTypeTests.Infer_NonNull_Int_Array_Correctly.snap diff --git a/src/Core/Types.Tests/Types/ObjectTypeTests.cs b/src/Core/Types.Tests/Types/ObjectTypeTests.cs index 7b8c9a877e2..4d74173fd38 100644 --- a/src/Core/Types.Tests/Types/ObjectTypeTests.cs +++ b/src/Core/Types.Tests/Types/ObjectTypeTests.cs @@ -1535,6 +1535,26 @@ public void Required_Attribute_Is_Recognized() .MatchSnapshot(); } + [Fact] + public void Infer_NonNull_Int_Array_Correctly() + { + SchemaBuilder.New() + .AddQueryType() + .Create() + .ToString() + .MatchSnapshot(); + } + +#nullable enable + public class Query + { + public byte[] Array() + { + throw new NotImplementedException(); + } + } +#nullable disable + public class GenericFoo { public T Value { get; } diff --git a/src/Core/Types.Tests/Types/Relay/IdAttributeTests.cs b/src/Core/Types.Tests/Types/Relay/IdAttributeTests.cs index 7946d8deef8..3ee6bdaf892 100644 --- a/src/Core/Types.Tests/Types/Relay/IdAttributeTests.cs +++ b/src/Core/Types.Tests/Types/Relay/IdAttributeTests.cs @@ -21,6 +21,7 @@ public async Task Id_On_Arguments() IExecutionResult result = await SchemaBuilder.New() .AddQueryType() + .AddType() .Create() .MakeExecutable() .ExecuteAsync( @@ -88,7 +89,6 @@ public class Query public string IntId([ID] int id) => id.ToString(); public string StringId([ID] string id) => id.ToString(); public string GuidId([ID] Guid id) => id.ToString(); - public IFooPayload Foo(FooInput input) => new FooPayload { SomeId = input.SomeId }; } diff --git a/src/Core/Types.Tests/Types/__snapshots__/ObjectTypeTests.Infer_NonNull_Int_Array_Correctly.snap b/src/Core/Types.Tests/Types/__snapshots__/ObjectTypeTests.Infer_NonNull_Int_Array_Correctly.snap new file mode 100644 index 00000000000..1e92115f726 --- /dev/null +++ b/src/Core/Types.Tests/Types/__snapshots__/ObjectTypeTests.Infer_NonNull_Int_Array_Correctly.snap @@ -0,0 +1,10 @@ +schema { + query: Query +} + +type Query { + array: [Byte!]! +} + +"The `Byte` scalar type represents non-fractional whole numeric values. Byte can represent values between 0 and 255." +scalar Byte diff --git a/src/Core/Types.Tests/Utilities/DotNetTypeInfoFactoryTests.cs b/src/Core/Types.Tests/Utilities/DotNetTypeInfoFactoryTests.cs index 169fdaed60d..9123400ac81 100644 --- a/src/Core/Types.Tests/Utilities/DotNetTypeInfoFactoryTests.cs +++ b/src/Core/Types.Tests/Utilities/DotNetTypeInfoFactoryTests.cs @@ -222,6 +222,26 @@ public void Create_TypeInfo_From_RewrittenType() Assert.IsType(schemaType).ElementType).Type); } + [Fact] + public void Infer_Nullability_From_Wrapped_ValueTypes() + { + // arrange + Type type = typeof(NonNullType>>>); + var factory = new DotNetTypeInfoFactory(); + + // act + bool success = factory.TryCreate(type, out TypeInfo typeInfo); + + // assert + Assert.True(success); + IType schemaType = typeInfo.TypeFactory.Invoke(new IntType()); + Assert.IsType( + Assert.IsType( + Assert.IsType( + Assert.IsType(schemaType) + .InnerType()).InnerType()).InnerType()); + } + private class CustomStringList : CustomStringListBase { diff --git a/src/Core/Types.Tests/Utilities/ExtendedTypeRewriterTests.cs b/src/Core/Types.Tests/Utilities/ExtendedTypeRewriterTests.cs index a22d5d5d0e3..16a7b74e560 100644 --- a/src/Core/Types.Tests/Utilities/ExtendedTypeRewriterTests.cs +++ b/src/Core/Types.Tests/Utilities/ExtendedTypeRewriterTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using HotChocolate.Types; +using Snapshooter.Xunit; using Xunit; #nullable enable diff --git a/src/Core/Types/Types/Descriptors/ObjectTypeDescriptorBase~1.cs b/src/Core/Types/Types/Descriptors/ObjectTypeDescriptorBase~1.cs index 2a416bd3fe2..90b512fafa0 100644 --- a/src/Core/Types/Types/Descriptors/ObjectTypeDescriptorBase~1.cs +++ b/src/Core/Types/Types/Descriptors/ObjectTypeDescriptorBase~1.cs @@ -1,10 +1,8 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; using System.Reflection; using HotChocolate.Language; -using HotChocolate.Resolvers.Expressions; using HotChocolate.Types.Descriptors.Definitions; namespace HotChocolate.Types.Descriptors diff --git a/src/Core/Types/Utilities/DotNetTypeInfoFactory.cs b/src/Core/Types/Utilities/DotNetTypeInfoFactory.cs index 13b87818310..2847927a4b5 100644 --- a/src/Core/Types/Utilities/DotNetTypeInfoFactory.cs +++ b/src/Core/Types/Utilities/DotNetTypeInfoFactory.cs @@ -200,6 +200,17 @@ private static bool TryCreate3ComponentType( return true; } + if (IsNonNullType(components[0]) + && IsListType(components[1]) + && components[2].IsValueType) + { + typeInfo = new TypeInfo( + components[2], + components, + t => new NonNullType(new ListType(new NonNullType(t)))); + return true; + } + if (IsListType(components[0]) && IsNonNullType(components[1]) && IsPossibleNamedType(components[2]))