diff --git a/src/HotChocolate/Core/src/Types/Configuration/ExtendedTypeRefEqualityComparer.cs b/src/HotChocolate/Core/src/Types/Configuration/ExtendedTypeRefEqualityComparer.cs index a33a0256d68..d5ed6b5cb2a 100644 --- a/src/HotChocolate/Core/src/Types/Configuration/ExtendedTypeRefEqualityComparer.cs +++ b/src/HotChocolate/Core/src/Types/Configuration/ExtendedTypeRefEqualityComparer.cs @@ -46,6 +46,11 @@ private static bool Equals(IExtendedType? x, IExtendedType? y) return false; } + if (IsKeyValuePair(x) || IsKeyValuePair(y)) + { + return x.Equals(y); + } + return ReferenceEquals(x.Type, y.Type) && x.Kind == y.Kind; } @@ -66,6 +71,11 @@ public int GetHashCode(ExtendedTypeReference obj) private static int GetHashCode(IExtendedType obj) { + if (IsKeyValuePair(obj)) + { + return obj.GetHashCode(); + } + unchecked { var hashCode = (obj.Type.GetHashCode() * 397) @@ -79,4 +89,7 @@ private static int GetHashCode(IExtendedType obj) return hashCode; } } + + private static bool IsKeyValuePair(IExtendedType type) + => type.IsGeneric && type.Definition == typeof(KeyValuePair<,>); } diff --git a/src/HotChocolate/Core/src/Types/Configuration/Handlers/DefaultTypeDiscoveryHandler.cs b/src/HotChocolate/Core/src/Types/Configuration/Handlers/DefaultTypeDiscoveryHandler.cs index e367cc5c423..83e03f6430a 100644 --- a/src/HotChocolate/Core/src/Types/Configuration/Handlers/DefaultTypeDiscoveryHandler.cs +++ b/src/HotChocolate/Core/src/Types/Configuration/Handlers/DefaultTypeDiscoveryHandler.cs @@ -1,10 +1,8 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using HotChocolate.Internal; -using HotChocolate.Types.Helpers; using HotChocolate.Types; using HotChocolate.Types.Descriptors; -using System.Diagnostics; namespace HotChocolate.Configuration; @@ -18,11 +16,6 @@ public override bool TryInferType( TypeDiscoveryInfo typeInfo, [NotNullWhen(true)] out TypeReference[]? schemaTypeRefs) { - if (TryCreateKeyValuePairTypeRef(typeReference, typeInfo, out schemaTypeRefs)) - { - return true; - } - TypeReference? schemaType; if (typeInfo.IsStatic) @@ -67,19 +60,39 @@ public override bool TryInferType( } else if (IsObjectType(typeInfo)) { - schemaType = - TypeInspector.CreateTypeRef( - typeof(ObjectType<>), - typeInfo, - typeReference); + if (typeInfo.IsKeyValuePair) + { + schemaType = + TypeReference.Create( + new KeyValuePairObjectType(typeInfo.ExtendedRuntimeType), + scope: typeReference.Scope); + } + else + { + schemaType = + TypeInspector.CreateTypeRef( + typeof(ObjectType<>), + typeInfo, + typeReference); + } } else if (IsInputObjectType(typeInfo)) { - schemaType = - TypeInspector.CreateTypeRef( - typeof(InputObjectType<>), - typeInfo, - typeReference); + if (typeInfo.IsKeyValuePair) + { + schemaType = + TypeReference.Create( + new KeyValuePairInputObjectType(typeInfo.ExtendedRuntimeType), + scope: typeReference.Scope); + } + else + { + schemaType = + TypeInspector.CreateTypeRef( + typeof(InputObjectType<>), + typeInfo, + typeReference); + } } else if (IsEnumType(typeInfo)) { @@ -107,183 +120,6 @@ public override bool TryInferType( return true; } - private static bool TryCreateKeyValuePairTypeRef( - TypeReference typeReference, - TypeDiscoveryInfo typeInfo, - [NotNullWhen(true)] out TypeReference[]? schemaTypeRefs) - { - // Only extended type references can represent dictionaries. - if (typeReference is not ExtendedTypeReference { Type: { } extendedType }) - { - schemaTypeRefs = null; - return false; - } - - // We only handle generic KeyValuePair types here. - if (!extendedType.IsGeneric - || extendedType.Definition != typeof(KeyValuePair<,>)) - { - schemaTypeRefs = null; - return false; - } - - // For output types we create an object type to represent the key-value pair. - if (typeInfo.Context is TypeContext.Output or TypeContext.None) - { - var typeName = CreateKeyValuePairTypeName( - extendedType, - TypeKind.Object); - - schemaTypeRefs = - [ - TypeReference.Create( - typeName, - typeReference, - _ => CreateKeyValuePairObjectType(extendedType, typeName), - typeReference.Context, - typeReference.Scope) - ]; - return true; - } - - // For input types we create an input object type instead. - if (typeInfo.Context is TypeContext.Input) - { - var typeName = CreateKeyValuePairTypeName( - extendedType, - TypeKind.InputObject); - - schemaTypeRefs = - [ - TypeReference.Create( - typeName, - typeReference, - _ => CreateKeyValuePairInputObjectType(extendedType, typeName), - typeReference.Context, - typeReference.Scope) - ]; - return true; - } - - // We should never get here as all context options are exhausted above. - Debug.Fail("Unexpected TypeContext value."); - schemaTypeRefs = null; - return false; - } - - private static ObjectType CreateKeyValuePairObjectType( - IExtendedType keyValuePairType, - string typeName) - { - var runtimeType = keyValuePairType.Type; - var keyType = keyValuePairType.TypeArguments[0]; - var valueType = keyValuePairType.TypeArguments[1]; - var keyProperty = runtimeType.GetProperty("Key")!; - var valueProperty = runtimeType.GetProperty("Value")!; - - return new ObjectType( - descriptor => - { - descriptor.Name(typeName); - - descriptor.Field(keyProperty) - .Name("key") - .Extend() - .OnBeforeCreate( - (_, field) => - { - field.SetMoreSpecificType(keyType, TypeContext.Output); - field.SourceType = runtimeType; - field.ResolverType = runtimeType; - }); - - descriptor.Field(valueProperty) - .Name("value") - .Extend() - .OnBeforeCreate( - (_, field) => - { - field.SetMoreSpecificType(valueType, TypeContext.Output); - field.SourceType = runtimeType; - field.ResolverType = runtimeType; - }); - - descriptor.Extend() - .OnBeforeCreate( - (_, type) => - { - type.RuntimeType = runtimeType; - type.FieldBindingType = typeof(object); - }); - }); - } - - private static InputObjectType CreateKeyValuePairInputObjectType( - IExtendedType keyValuePairType, - string typeName) - { - var runtimeType = keyValuePairType.Type; - var keyType = keyValuePairType.TypeArguments[0]; - var valueType = keyValuePairType.TypeArguments[1]; - var keyProperty = runtimeType.GetProperty("Key")!; - var valueProperty = runtimeType.GetProperty("Value")!; - var keyGetter = keyProperty.GetMethod!; - var valueGetter = valueProperty.GetMethod!; - - return new InputObjectType( - descriptor => - { - descriptor.Name(typeName); - - descriptor.Field("key") - .Extend() - .OnBeforeCreate( - (_, field) => field.SetMoreSpecificType(keyType, TypeContext.Input)); - - descriptor.Field("value") - .Extend() - .OnBeforeCreate( - (_, field) => field.SetMoreSpecificType(valueType, TypeContext.Input)); - - descriptor.Extend() - .OnBeforeCreate( - (_, type) => - { - type.RuntimeType = runtimeType; - type.CreateInstance = - values => Activator.CreateInstance(runtimeType, values[0], values[1])!; - type.GetFieldData = - (obj, values) => - { - values[0] = keyGetter.Invoke(obj, []); - values[1] = valueGetter.Invoke(obj, []); - }; - }); - }); - } - - private static string CreateKeyValuePairTypeName(IExtendedType type, TypeKind kind) - { - var keyType = type.TypeArguments[0]; - var valueType = type.TypeArguments[1]; - var keyName = keyType.Type.Name; - var valueName = valueType.Type.Name; - - if (keyType.IsNullable && keyType.Type.IsValueType) - { - keyName = $"Nullable{keyName}"; - } - - if (valueType.IsNullable && valueType.Type.IsValueType) - { - valueName = $"Nullable{valueName}"; - } - - return kind is TypeKind.InputObject - ? $"KeyValuePairOf{keyName}And{valueName}Input" - : $"KeyValuePairOf{keyName}And{valueName}"; - } - public override bool TryInferKind( TypeReference typeReference, TypeDiscoveryInfo typeInfo, diff --git a/src/HotChocolate/Core/src/Types/Configuration/TypeDiscoverer.cs b/src/HotChocolate/Core/src/Types/Configuration/TypeDiscoverer.cs index 7c79ca5b378..7b78444c1ea 100644 --- a/src/HotChocolate/Core/src/Types/Configuration/TypeDiscoverer.cs +++ b/src/HotChocolate/Core/src/Types/Configuration/TypeDiscoverer.cs @@ -182,6 +182,16 @@ private bool TryInferTypes() continue; } + if (unresolvedTypeRef is ExtendedTypeReference fallbackTypeRef + && _typeRegistry.TryGetNonInferredTypeRef(fallbackTypeRef, out var fallbackReference)) + { + inferred = true; + _typeRegistry.TryRegister(fallbackTypeRef, fallbackReference); + _unregistered.Enqueue(fallbackReference, (TypeReferenceStrength.VeryWeak, _nextTypeRefIndex++)); + _resolved.Add(unresolvedTypeRef); + continue; + } + // if we do not have a type binding or if we have a directive we will try to infer the type. if (unresolvedTypeRef is ExtendedTypeReference or ExtendedTypeDirectiveReference && _context.TryInferSchemaType(unresolvedTypeRef, out var schemaTypeRefs)) diff --git a/src/HotChocolate/Core/src/Types/Configuration/TypeRegistry.cs b/src/HotChocolate/Core/src/Types/Configuration/TypeRegistry.cs index dc613bb73e9..20ad9682bb5 100644 --- a/src/HotChocolate/Core/src/Types/Configuration/TypeRegistry.cs +++ b/src/HotChocolate/Core/src/Types/Configuration/TypeRegistry.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using HotChocolate.Internal; using HotChocolate.Types; using HotChocolate.Types.Descriptors; using HotChocolate.Utilities; @@ -78,6 +79,54 @@ public bool TryGetTypeRef( return _runtimeTypeRefs.TryGetValue(runtimeTypeRef, out typeRef); } + public bool TryGetNonInferredTypeRef( + ExtendedTypeReference runtimeTypeRef, + [NotNullWhen(true)] out TypeReference? typeRef) + { + ArgumentNullException.ThrowIfNull(runtimeTypeRef); + + if (RuntimeTypeBindingHelper.RequiresExactBinding(runtimeTypeRef.Type) + || !IsKeyValuePair(runtimeTypeRef.Type)) + { + typeRef = null; + return false; + } + + foreach (var (candidateRef, candidateTypeRef) in _runtimeTypeRefs) + { + if (!candidateRef.Scope.EqualsOrdinal(runtimeTypeRef.Scope)) + { + continue; + } + + if (candidateRef.Context != runtimeTypeRef.Context + && candidateRef.Context != TypeContext.None + && runtimeTypeRef.Context != TypeContext.None) + { + continue; + } + + if (candidateRef.Type.Type != runtimeTypeRef.Type.Type + || candidateRef.Type.Kind != runtimeTypeRef.Type.Kind) + { + continue; + } + + if (_typeRegister.TryGetValue(candidateTypeRef, out var registeredType) + && !registeredType.IsInferred) + { + typeRef = candidateTypeRef; + return true; + } + } + + typeRef = null; + return false; + } + + private static bool IsKeyValuePair(IExtendedType type) + => type.IsGeneric && type.Definition == typeof(KeyValuePair<,>); + public bool IsExplicitBinding(ExtendedTypeReference runtimeTypeRef) { ArgumentNullException.ThrowIfNull(runtimeTypeRef); diff --git a/src/HotChocolate/Core/src/Types/Internal/TypeDiscoveryInfo.cs b/src/HotChocolate/Core/src/Types/Internal/TypeDiscoveryInfo.cs index 513b0dcafc9..b227c2bc429 100644 --- a/src/HotChocolate/Core/src/Types/Internal/TypeDiscoveryInfo.cs +++ b/src/HotChocolate/Core/src/Types/Internal/TypeDiscoveryInfo.cs @@ -37,23 +37,26 @@ public TypeDiscoveryInfo(TypeReference typeReference) TypeDiscoveryInfo_TypeRefKindNotSupported); } - RuntimeType = extendedType.Type; + ExtendedRuntimeType = extendedType; - var attributes = RuntimeType.Attributes; - IsPublic = IsPublicInternal(RuntimeType); + var attributes = extendedType.Type.Attributes; + IsPublic = IsPublicInternal(extendedType.Type); IsComplex = IsComplexTypeInternal(extendedType, IsPublic); IsInterface = (attributes & TypeAttributes.Interface) == TypeAttributes.Interface; IsAbstract = (attributes & TypeAttributes.Abstract) == TypeAttributes.Abstract; IsStatic = IsAbstract && (attributes & TypeAttributes.Sealed) == TypeAttributes.Sealed; - IsEnum = RuntimeType.IsEnum; - Attribute = GetTypeAttributeInternal(RuntimeType); + IsEnum = extendedType.Type.IsEnum; + IsKeyValuePair = extendedType.IsGeneric && extendedType.Definition == typeof(KeyValuePair<,>); + Attribute = GetTypeAttributeInternal(extendedType.Type); Context = typeReference.Context; } + public IExtendedType ExtendedRuntimeType { get; } + /// /// Gets the runtime type. /// - public Type RuntimeType { get; } + public Type RuntimeType => ExtendedRuntimeType.Type; /// /// The type attribute if one was annotated to the . @@ -95,6 +98,11 @@ public TypeDiscoveryInfo(TypeReference typeReference) /// public bool IsDirectiveRef { get; } + /// + /// Specifies if the is a . + /// + public bool IsKeyValuePair { get; } + /// /// Specifies the of the type reference. /// diff --git a/src/HotChocolate/Core/src/Types/Properties/TypeResources.Designer.cs b/src/HotChocolate/Core/src/Types/Properties/TypeResources.Designer.cs index 59e3d3ade36..3582ea99c68 100644 --- a/src/HotChocolate/Core/src/Types/Properties/TypeResources.Designer.cs +++ b/src/HotChocolate/Core/src/Types/Properties/TypeResources.Designer.cs @@ -3241,5 +3241,14 @@ internal static string VariableValueBuilder_VarNameEmpty { return ResourceManager.GetString("VariableValueBuilder_VarNameEmpty", resourceCulture); } } + + /// + /// Looks up a localized string similar to {0} requires a KeyValuePair<TKey, TValue> runtime type.. + /// + internal static string ThrowHelper_KeyValuePairType_InvalidRuntimeType { + get { + return ResourceManager.GetString("ThrowHelper_KeyValuePairType_InvalidRuntimeType", resourceCulture); + } + } } } diff --git a/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx b/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx index 4ceae5fbbb0..1c0535c593d 100644 --- a/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx +++ b/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx @@ -1083,4 +1083,7 @@ Type: `{0}` The batch resolver method '{0}.{1}' must return a list type (e.g. List<T>, IReadOnlyList<T>, ImmutableArray<T> or T[]). Batch resolvers return one result per parent object, so the return type must be a collection. + + {0} requires a KeyValuePair<TKey, TValue> runtime type. + diff --git a/src/HotChocolate/Core/src/Abstractions/NameFormattingHelpers.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/NameFormattingHelpers.cs similarity index 87% rename from src/HotChocolate/Core/src/Abstractions/NameFormattingHelpers.cs rename to src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/NameFormattingHelpers.cs index b0e03af730b..a038f819d86 100644 --- a/src/HotChocolate/Core/src/Abstractions/NameFormattingHelpers.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/NameFormattingHelpers.cs @@ -1,9 +1,10 @@ using System.Buffers; using System.Reflection; using System.Text; +using HotChocolate.Internal; using HotChocolate.Utilities; -namespace HotChocolate; +namespace HotChocolate.Types.Descriptors; /// /// Contains helpers and extensions to reformat the name of a type system member to conform with @@ -78,6 +79,48 @@ public static string GetGraphQLName(this MemberInfo member) "Only properties and methods are accepted as members."); } + public static string GetGraphQLName(IExtendedType type) + => GetGraphQLName(type, isRoot: true); + + private static string GetGraphQLName(IExtendedType type, bool isRoot) + { + var typeName = type.IsGeneric + ? CreateGenericTypeName(type) + : type.Type.Name; + + if (!isRoot && type.IsNullable) + { + typeName = $"Nullable{typeName}"; + } + + return NameUtils.MakeValidGraphQLName(typeName)!; + } + + private static string CreateGenericTypeName(IExtendedType type) + { + var typeName = type.Definition!.Name; + var genericDelimiter = typeName.LastIndexOf('`'); + + if (genericDelimiter >= 0) + { + typeName = typeName[..genericDelimiter]; + } + + var result = typeName + "Of"; + + for (var i = 0; i < type.TypeArguments.Count; i++) + { + if (i > 0) + { + result += "And"; + } + + result += GetGraphQLName(type.TypeArguments[i], isRoot: false); + } + + return result; + } + private static string FormatMethodName(MethodInfo method) { var name = method.Name; diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/InputObjectTypeDescriptor.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/InputObjectTypeDescriptor.cs index 95e9b85753e..abf02fbfbc2 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/InputObjectTypeDescriptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/InputObjectTypeDescriptor.cs @@ -96,6 +96,20 @@ protected override void OnCreateConfiguration( Context.Descriptors.Pop(); } + internal void InferFieldsFromFieldBindingType() + { + var fields = TypeMemHelper.RentInputFieldConfigurationMap(); + var handledMembers = TypeMemHelper.RentMemberSet(); + + InferFieldsFromFieldBindingType(fields, handledMembers); + + Configuration.Fields.Clear(); + Configuration.Fields.AddRange(fields.Values); + + TypeMemHelper.Return(fields); + TypeMemHelper.Return(handledMembers); + } + protected void InferFieldsFromFieldBindingType( IDictionary fields, ISet handledMembers) diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectTypeDescriptor.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectTypeDescriptor.cs index fcd8b38ed5a..f2596413ac9 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectTypeDescriptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectTypeDescriptor.cs @@ -16,14 +16,14 @@ public class ObjectTypeDescriptor { private readonly List _fields = []; - protected ObjectTypeDescriptor(IDescriptorContext context, Type clrType) + protected ObjectTypeDescriptor(IDescriptorContext context, Type runtimeType) : base(context) { - ArgumentNullException.ThrowIfNull(clrType); + ArgumentNullException.ThrowIfNull(runtimeType); - Configuration.RuntimeType = clrType; - Configuration.Name = context.Naming.GetTypeName(clrType, TypeKind.Object); - Configuration.Description = context.Naming.GetTypeDescription(clrType, TypeKind.Object); + Configuration.RuntimeType = runtimeType; + Configuration.Name = context.Naming.GetTypeName(runtimeType, TypeKind.Object); + Configuration.Description = context.Naming.GetTypeDescription(runtimeType, TypeKind.Object); } protected ObjectTypeDescriptor(IDescriptorContext context) @@ -424,8 +424,8 @@ public static ObjectTypeDescriptor New( public static ObjectTypeDescriptor New( IDescriptorContext context, - Type clrType) => - new(context, clrType); + Type runtimeType) => + new(context, runtimeType); public static ObjectTypeDescriptor New( IDescriptorContext context) => diff --git a/src/HotChocolate/Core/src/Types/Types/KeyValuePairInputObjectType.cs b/src/HotChocolate/Core/src/Types/Types/KeyValuePairInputObjectType.cs new file mode 100644 index 00000000000..15fa5f68c78 --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Types/KeyValuePairInputObjectType.cs @@ -0,0 +1,42 @@ +using HotChocolate.Internal; +using HotChocolate.Types.Descriptors; +using HotChocolate.Utilities; + +namespace HotChocolate.Types; + +/// +/// This helper type is used to represent a as a GraphQL input object type. +/// +internal sealed class KeyValuePairInputObjectType(IExtendedType runtimeType) : InputObjectType +{ + protected override void Configure(IInputObjectTypeDescriptor descriptor) + { + if (!runtimeType.IsGeneric + || runtimeType.Definition != typeof(KeyValuePair<,>) + || runtimeType.TypeArguments.Count != 2) + { + throw ThrowHelper.KeyValuePairType_InvalidRuntimeType(nameof(KeyValuePairInputObjectType)); + } + + ConfigureInternal((InputObjectTypeDescriptor)descriptor); + } + + private void ConfigureInternal(InputObjectTypeDescriptor descriptor) + { + var descriptorExtension = descriptor.Extend(); + var context = descriptorExtension.Context; + var configuration = descriptorExtension.Configuration; + var keyType = runtimeType.TypeArguments[0]; + var valueType = runtimeType.TypeArguments[1]; + + var typeName = NameFormattingHelpers.GetGraphQLName(runtimeType); + configuration.Name = context.Naming.GetTypeName(typeName, TypeKind.InputObject); + configuration.Description = context.Naming.GetTypeDescription(runtimeType.Type, TypeKind.InputObject); + configuration.Fields.BindingBehavior = context.Options.DefaultBindingBehavior; + configuration.RuntimeType = runtimeType.Type; + + descriptor.InferFieldsFromFieldBindingType(); + descriptor.Field("key").Extend().Configuration.Type = TypeReference.Create(keyType, TypeContext.Input); + descriptor.Field("value").Extend().Configuration.Type = TypeReference.Create(valueType, TypeContext.Input); + } +} diff --git a/src/HotChocolate/Core/src/Types/Types/KeyValuePairObjectType.cs b/src/HotChocolate/Core/src/Types/Types/KeyValuePairObjectType.cs new file mode 100644 index 00000000000..824be0cf3c9 --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Types/KeyValuePairObjectType.cs @@ -0,0 +1,44 @@ +using HotChocolate.Internal; +using HotChocolate.Types.Descriptors; +using HotChocolate.Utilities; + +namespace HotChocolate.Types; + +/// +/// This helper type is used to represent a as a GraphQL object type. +/// +internal sealed class KeyValuePairObjectType(IExtendedType runtimeType) : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + if (!runtimeType.IsGeneric + || runtimeType.Definition != typeof(KeyValuePair<,>) + || runtimeType.TypeArguments.Count != 2) + { + throw ThrowHelper.KeyValuePairType_InvalidRuntimeType(nameof(KeyValuePairObjectType)); + } + + ConfigureInternal((ObjectTypeDescriptor)descriptor); + } + + private void ConfigureInternal(ObjectTypeDescriptor descriptor) + { + var descriptorExtension = descriptor.Extend(); + var context = descriptorExtension.Context; + var configuration = descriptorExtension.Configuration; + var keyType = runtimeType.TypeArguments[0]; + var valueType = runtimeType.TypeArguments[1]; + + var typeName = NameFormattingHelpers.GetGraphQLName(runtimeType); + configuration.Name = context.Naming.GetTypeName(typeName, TypeKind.Object); + configuration.Description = context.Naming.GetTypeDescription(runtimeType.Type, TypeKind.Object); + configuration.Fields.BindingBehavior = context.Options.DefaultBindingBehavior; + configuration.FieldBindingFlags = context.Options.DefaultFieldBindingFlags; + configuration.FieldBindingType = runtimeType.Type; + configuration.RuntimeType = runtimeType.Type; + + descriptor.InferFieldsFromFieldBindingType(); + descriptor.Field("key").Extend().Configuration.Type = TypeReference.Create(keyType, TypeContext.Output); + descriptor.Field("value").Extend().Configuration.Type = TypeReference.Create(valueType, TypeContext.Output); + } +} diff --git a/src/HotChocolate/Core/src/Types/Utilities/ObjectToDictionaryConverter.cs b/src/HotChocolate/Core/src/Types/Utilities/ObjectToDictionaryConverter.cs index dcf2fab2aff..e5263d26b76 100644 --- a/src/HotChocolate/Core/src/Types/Utilities/ObjectToDictionaryConverter.cs +++ b/src/HotChocolate/Core/src/Types/Utilities/ObjectToDictionaryConverter.cs @@ -4,6 +4,7 @@ using System.Collections.Concurrent; using System.Reflection; using HotChocolate.Properties; +using HotChocolate.Types.Descriptors; namespace HotChocolate.Utilities; diff --git a/src/HotChocolate/Core/src/Types/Utilities/ReflectionUtils.cs b/src/HotChocolate/Core/src/Types/Utilities/ReflectionUtils.cs index 7cb3f36bd38..1619aa70eaf 100644 --- a/src/HotChocolate/Core/src/Types/Utilities/ReflectionUtils.cs +++ b/src/HotChocolate/Core/src/Types/Utilities/ReflectionUtils.cs @@ -4,6 +4,7 @@ using System.Linq.Expressions; using System.Reflection; using HotChocolate.Properties; +using HotChocolate.Types.Descriptors; namespace HotChocolate.Utilities; diff --git a/src/HotChocolate/Core/src/Types/Utilities/ThrowHelper.cs b/src/HotChocolate/Core/src/Types/Utilities/ThrowHelper.cs index 067c4fe08f5..68461c525bb 100644 --- a/src/HotChocolate/Core/src/Types/Utilities/ThrowHelper.cs +++ b/src/HotChocolate/Core/src/Types/Utilities/ThrowHelper.cs @@ -203,6 +203,14 @@ public static SchemaException DataLoader_InvalidType(Type dataLoaderType) dataLoaderType.FullName ?? dataLoaderType.Name) .Build()); + public static SchemaException KeyValuePairType_InvalidRuntimeType(string typeName) + => new SchemaException( + SchemaErrorBuilder.New() + .SetMessage( + TypeResources.ThrowHelper_KeyValuePairType_InvalidRuntimeType, + typeName) + .Build()); + public static SchemaException NonGenericExecutableNotAllowed() => new SchemaException( SchemaErrorBuilder diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/InputObjectTypeDictionaryTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/InputObjectTypeDictionaryTests.cs index 1b52f22c49f..900a060c982 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/InputObjectTypeDictionaryTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/InputObjectTypeDictionaryTests.cs @@ -56,7 +56,15 @@ public void Dictionary_Input_With_Nullable_Value_Is_Correctly_Detected() var keyValuePairType = schema.Types.GetType( fooInputType.Fields["contextData"].Type.TypeName()); - Assert.False(keyValuePairType.Fields["value"].Type.IsNonNullType()); + keyValuePairType + .ToString() + .MatchInlineSnapshot( + """ + input KeyValuePairOfStringAndNullableStringInput { + key: String! + value: String + } + """); } [Fact] @@ -70,10 +78,128 @@ public void Dictionary_Output_With_Nullable_Value_Is_Correctly_Detected() // assert var queryType = schema.Types.GetType("NullableDictionaryOutputQuery"); - var keyValuePairType = schema.Types.GetType( - queryType.Fields["contextData"].Type.TypeName()); + var keyValuePairType = schema.Types.GetType(queryType.Fields["contextData"].Type.TypeName()); + + keyValuePairType + .ToString() + .MatchInlineSnapshot( + """ + type KeyValuePairOfStringAndNullableString { + key: String! + value: String + } + """); + } + + [Fact] + public void Dictionary_Output_With_Nullable_Reference_Value_Uses_Nullable_Name_Prefix() + { + // arrange + // act + var schema = SchemaBuilder.New() + .AddQueryType() + .Create(); + + // assert + var queryType = schema.Types.GetType("NullableDictionaryOutputQuery"); + var keyValuePairType = queryType.Fields["contextData"].Type.NamedType(); + + keyValuePairType + .ToString() + .MatchInlineSnapshot( + """ + type KeyValuePairOfStringAndNullableString { + key: String! + value: String + } + """); + } + + [Fact] + public void Dictionary_Input_With_Nullable_Reference_Value_Is_Disambiguated_From_NonNull_Reference_Value() + { + // arrange + // act + var schema = SchemaBuilder.New() + .AddQueryType() + .Create(); + + // assert + var nonNullInputType = schema.Types.GetType("NonNullReferenceDictionaryInput"); + var nullableInputType = schema.Types.GetType("NullableReferenceDictionaryInput"); + var nonNullType = nonNullInputType.Fields["contextData"].Type.NamedType(); + var nullableType = nullableInputType.Fields["contextData"].Type.NamedType(); + + Snapshot.Create() + .Add(nonNullType.ToString(), "nonNull") + .Add(nullableType.ToString(), "nullable") + .MatchInline( + """ + nonNull + --------------- + input KeyValuePairOfStringAndStringInput { + key: String! + value: String! + } + --------------- + + nullable + --------------- + input KeyValuePairOfStringAndNullableStringInput { + key: String! + value: String + } + --------------- + + """); + } + + [Fact] + public void Dictionary_Output_With_Nullable_ValueType_Value_Uses_Nullable_Name_Prefix() + { + // arrange + // act + var schema = SchemaBuilder.New() + .AddQueryType() + .Create(); + + // assert + var queryType = schema.Types.GetType("NullableValueTypeDictionaryOutputQuery"); + var keyValuePairType = schema.Types.GetType(queryType.Fields["contextData"].Type.TypeName()); - Assert.False(keyValuePairType.Fields["value"].Type.IsNonNullType()); + keyValuePairType + .ToString() + .MatchInlineSnapshot( + """ + type KeyValuePairOfStringAndNullableInt32 { + key: String! + value: Int + } + """); + } + + [Fact] + public void Dictionary_Output_With_IList_Value_Uses_Valid_KeyValuePair_Name() + { + // arrange + // act + var schema = SchemaBuilder.New() + .AddQueryType() + .Create(); + + // assert + var outputType = schema.Types.GetType("DictionaryWithListOutput"); + var keyValuePairType = schema.Types.GetType(outputType.Fields["highlights"].Type.TypeName()); + + keyValuePairType + .ToString() + .MatchInlineSnapshot( + """ + type KeyValuePairOfStringAndIListOfString { + key: String! + value: [String!]! + } + """); } public class Query @@ -110,11 +236,6 @@ public void Explicit_ObjectType_For_KeyValuePair_Overrides_Inferred_Type() // proving the explicit ObjectType definition is used instead of the // auto-inferred KeyValuePairOfStringAndString. schema.MatchSnapshot(); - - var customType = schema.Types.GetType("StringPair"); - Assert.Equal("first", customType.Fields["first"].Name); - Assert.Equal("second", customType.Fields["second"].Name); - Assert.Equal(2, customType.Fields.Count(f => !f.IsIntrospectionField)); } public class NullableDictionaryInput @@ -132,6 +253,39 @@ public class NullableDictionaryOutputQuery public Dictionary? GetContextData() => null; } + public class NullableValueTypeDictionaryOutputQuery + { + public Dictionary? GetContextData() => null; + } + + public class ReferenceNullabilityDisambiguationInputQuery + { + public string GetFoo(NonNullReferenceDictionaryInput input) => "ok"; + + public string GetBar(NullableReferenceDictionaryInput input) => "ok"; + } + + public class NonNullReferenceDictionaryInput + { + public Dictionary? ContextData { get; set; } + } + + public class NullableReferenceDictionaryInput + { + public Dictionary? ContextData { get; set; } + } + + public class DictionaryWithListOutputQuery + { + public DictionaryWithListOutput GetResult() => new(); + } + + public class DictionaryWithListOutput + { + public IDictionary> Highlights { get; internal set; } = + new Dictionary>(); + } + public class CustomKeyValuePairType : ObjectType> { protected override void Configure(IObjectTypeDescriptor> descriptor)