diff --git a/src/HotChocolate/Core/src/Types/Configuration/Handlers/ExtendedTypeReferenceHandler.cs b/src/HotChocolate/Core/src/Types/Configuration/Handlers/ExtendedTypeReferenceHandler.cs index 8a982ed0a6a..f5a06b1298a 100644 --- a/src/HotChocolate/Core/src/Types/Configuration/Handlers/ExtendedTypeReferenceHandler.cs +++ b/src/HotChocolate/Core/src/Types/Configuration/Handlers/ExtendedTypeReferenceHandler.cs @@ -44,6 +44,7 @@ public void Handle(ITypeRegistrar typeRegistrar, TypeReference typeReference) { TryMapToExistingRegistration( typeRegistrar, + typeRef, typeInfo, typeReference.Context, typeReference.Scope); @@ -52,10 +53,20 @@ public void Handle(ITypeRegistrar typeRegistrar, TypeReference typeReference) private static void TryMapToExistingRegistration( ITypeRegistrar typeRegistrar, + ExtendedTypeReference typeRef, ITypeInfo typeInfo, TypeContext context, string? scope) { + // If there is an explicit runtime binding for the full type, keep the original + // type reference unresolved so discovery can apply that binding. + if (RuntimeTypeBindingHelper.RequiresExactBinding(typeRef.Type) + && typeRegistrar.HasRuntimeTypeBinding(typeRef)) + { + typeRegistrar.MarkUnresolved(typeRef); + return; + } + ExtendedTypeReference? normalizedTypeRef = null; var resolved = false; diff --git a/src/HotChocolate/Core/src/Types/Configuration/ITypeRegistrar.cs b/src/HotChocolate/Core/src/Types/Configuration/ITypeRegistrar.cs index d6b63609c18..99641ec0014 100644 --- a/src/HotChocolate/Core/src/Types/Configuration/ITypeRegistrar.cs +++ b/src/HotChocolate/Core/src/Types/Configuration/ITypeRegistrar.cs @@ -19,6 +19,8 @@ void Register( bool IsResolved(TypeReference typeReference); + bool HasRuntimeTypeBinding(ExtendedTypeReference typeReference); + TypeSystemObject CreateInstance(Type namedSchemaType); IReadOnlyCollection Unresolved { get; } diff --git a/src/HotChocolate/Core/src/Types/Configuration/RuntimeTypeBindingHelper.cs b/src/HotChocolate/Core/src/Types/Configuration/RuntimeTypeBindingHelper.cs new file mode 100644 index 00000000000..97e163bb947 --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Configuration/RuntimeTypeBindingHelper.cs @@ -0,0 +1,54 @@ +using System.Collections; +using HotChocolate.Internal; + +namespace HotChocolate.Configuration; + +internal static class RuntimeTypeBindingHelper +{ + public static bool RequiresExactBinding(IExtendedType runtimeType) + { + ArgumentNullException.ThrowIfNull(runtimeType); + + return IsByteArray(runtimeType) || IsDictionary(runtimeType.Source); + } + + private static bool IsByteArray(IExtendedType runtimeType) + => runtimeType.IsArray + && runtimeType.ElementType is { Source: { } elementType } + && elementType == typeof(byte); + + private static bool IsDictionary(Type type) + { + if (typeof(IDictionary).IsAssignableFrom(type)) + { + return true; + } + + if (type.IsGenericType) + { + var typeDefinition = type.GetGenericTypeDefinition(); + + if (typeDefinition == typeof(IDictionary<,>) + || typeDefinition == typeof(IReadOnlyDictionary<,>)) + { + return true; + } + } + + foreach (var implementedType in type.GetInterfaces()) + { + if (implementedType.IsGenericType) + { + var typeDefinition = implementedType.GetGenericTypeDefinition(); + + if (typeDefinition == typeof(IDictionary<,>) + || typeDefinition == typeof(IReadOnlyDictionary<,>)) + { + return true; + } + } + } + + return false; + } +} diff --git a/src/HotChocolate/Core/src/Types/Configuration/TypeReferenceResolver.cs b/src/HotChocolate/Core/src/Types/Configuration/TypeReferenceResolver.cs index 45950238cee..ce36a9e6aac 100644 --- a/src/HotChocolate/Core/src/Types/Configuration/TypeReferenceResolver.cs +++ b/src/HotChocolate/Core/src/Types/Configuration/TypeReferenceResolver.cs @@ -97,8 +97,17 @@ public bool TryGetType(TypeReference typeRef, [NotNullWhen(true)] out IType? typ switch (typeRef) { case ExtendedTypeReference r: - var typeFactory = _typeInspector.CreateTypeFactory(r.Type); - type = typeFactory.CreateType(typeDefinition); + if (_typeRegistry.IsExplicitBinding(r) + && RuntimeTypeBindingHelper.RequiresExactBinding(r.Type)) + { + type = CreateExplicitBoundType(typeDefinition, r.Type); + } + else + { + var typeFactory = _typeInspector.CreateTypeFactory(r.Type); + type = typeFactory.CreateType(typeDefinition); + } + _typeCache[typeId] = type; return true; @@ -155,6 +164,18 @@ private static IType CreateType( return namedType; } + private static IType CreateExplicitBoundType(ITypeDefinition typeDefinition, IExtendedType runtimeType) + { + IType type = typeDefinition; + + if (!runtimeType.IsNullable && typeDefinition.Kind is not TypeKind.NonNull) + { + type = new NonNullType(typeDefinition); + } + + return type; + } + private TypeId CreateId(TypeReference typeRef, TypeReference namedTypeRef) { switch (typeRef) diff --git a/src/HotChocolate/Core/src/Types/Configuration/TypeRegistrar.cs b/src/HotChocolate/Core/src/Types/Configuration/TypeRegistrar.cs index e0ff47b0e11..b71ee972f98 100644 --- a/src/HotChocolate/Core/src/Types/Configuration/TypeRegistrar.cs +++ b/src/HotChocolate/Core/src/Types/Configuration/TypeRegistrar.cs @@ -221,4 +221,11 @@ private RegisteredType InitializeType( .Build()); } } + + public bool HasRuntimeTypeBinding(ExtendedTypeReference typeReference) + { + ArgumentNullException.ThrowIfNull(typeReference); + + return _typeRegistry.TryGetTypeRef(typeReference, out _); + } } diff --git a/src/HotChocolate/Core/src/Types/Configuration/TypeRegistry.cs b/src/HotChocolate/Core/src/Types/Configuration/TypeRegistry.cs index 4eb9e9c6ad7..dc613bb73e9 100644 --- a/src/HotChocolate/Core/src/Types/Configuration/TypeRegistry.cs +++ b/src/HotChocolate/Core/src/Types/Configuration/TypeRegistry.cs @@ -11,6 +11,8 @@ internal sealed class TypeRegistry private readonly Dictionary _typeRegister = []; private readonly Dictionary _runtimeTypeRefs = new(new ExtendedTypeRefEqualityComparer()); + private readonly HashSet _explicitRuntimeTypeRefs = + new(new ExtendedTypeRefEqualityComparer()); private readonly Dictionary _nameRefs = new(StringComparer.Ordinal); private readonly Dictionary _lookups = new(new TypeRefEqualityComparer()); private readonly List _types = []; @@ -76,6 +78,13 @@ public bool TryGetTypeRef( return _runtimeTypeRefs.TryGetValue(runtimeTypeRef, out typeRef); } + public bool IsExplicitBinding(ExtendedTypeReference runtimeTypeRef) + { + ArgumentNullException.ThrowIfNull(runtimeTypeRef); + + return _explicitRuntimeTypeRefs.Contains(runtimeTypeRef); + } + public bool TryGetTypeRef( string typeName, [NotNullWhen(true)] out TypeReference? typeRef) @@ -93,12 +102,20 @@ public bool TryGetTypeRef( public IEnumerable GetTypeRefs() => _runtimeTypeRefs.Values; - public void TryRegister(ExtendedTypeReference runtimeTypeRef, TypeReference typeRef) + public void TryRegister( + ExtendedTypeReference runtimeTypeRef, + TypeReference typeRef, + bool explicitBinding = false) { ArgumentNullException.ThrowIfNull(runtimeTypeRef); ArgumentNullException.ThrowIfNull(typeRef); _runtimeTypeRefs.TryAdd(runtimeTypeRef, typeRef); + + if (explicitBinding) + { + _explicitRuntimeTypeRefs.Add(runtimeTypeRef); + } } public void Register(RegisteredType registeredType) diff --git a/src/HotChocolate/Core/src/Types/SchemaBuilder.Setup.cs b/src/HotChocolate/Core/src/Types/SchemaBuilder.Setup.cs index de9f72c32a7..9df8a4d3525 100644 --- a/src/HotChocolate/Core/src/Types/SchemaBuilder.Setup.cs +++ b/src/HotChocolate/Core/src/Types/SchemaBuilder.Setup.cs @@ -260,9 +260,12 @@ private static TypeInitializer CreateTypeInitializer( { foreach (var binding in bindings.Values) { + var runtimeTypeRef = binding.GetRuntimeTypeReference(context.TypeInspector); + typeRegistry.TryRegister( - binding.GetRuntimeTypeReference(context.TypeInspector), - binding.GetSchemaTypeReference(context.TypeInspector)); + runtimeTypeRef, + binding.GetSchemaTypeReference(context.TypeInspector), + explicitBinding: RuntimeTypeBindingHelper.RequiresExactBinding(runtimeTypeRef.Type)); } } diff --git a/src/HotChocolate/Core/test/Types.Tests/SchemaBuilderTests.cs b/src/HotChocolate/Core/test/Types.Tests/SchemaBuilderTests.cs index 85467456c0f..fc43611ff02 100644 --- a/src/HotChocolate/Core/test/Types.Tests/SchemaBuilderTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/SchemaBuilderTests.cs @@ -801,6 +801,36 @@ public void BindClrType_IntToString_IntFieldIsStringField() schema.ToString().MatchSnapshot(); } + [Fact] + public void BindClrType_DictionaryToAnyType_DictionaryFieldIsScalarField() + { + // arrange + // act + var schema = SchemaBuilder.New() + .AddQueryType() + .BindRuntimeType, AnyType>() + .Create(); + + // assert + var queryType = schema.Types.GetType("QueryWithDictionaryArgument"); + Assert.Equal("Any", queryType.Fields["foo"].Arguments["foo"].Type.Print()); + } + + [Fact] + public void BindClrType_ByteArrayToBase64Type_ByteArrayFieldIsScalarField() + { + // arrange + // act + var schema = SchemaBuilder.New() + .AddQueryType() + .BindRuntimeType() + .Create(); + + // assert + var queryType = schema.Types.GetType("QueryWithByteArrayField"); + Assert.Equal("Base64String!", queryType.Fields["foo"].Type.Print()); + } + [Fact] public void BindClrType_BuilderIsNull_ArgumentNullException() { @@ -2094,6 +2124,16 @@ public class QueryWithIntField public int Foo { get; set; } } + public class QueryWithDictionaryArgument + { + public bool Foo(IDictionary? foo) => true; + } + + public class QueryWithByteArrayField + { + public required byte[] Foo { get; set; } + } + public abstract class AbstractQuery { public required string Foo { get; set; }