diff --git a/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/TypeFileBuilderBase.cs b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/TypeFileBuilderBase.cs index a4b19a30b36..e96f976e248 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/TypeFileBuilderBase.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/FileBuilders/TypeFileBuilderBase.cs @@ -1741,11 +1741,9 @@ private void WriteAssignTypeRef( { case SchemaTypeReferenceKind.ExtendedTypeReference: Writer.WriteIndentedLine( - "{0} = typeInspector.GetTypeRef(typeof({1}), {2}.{3}){4}", + "{0} = {1}{2}", propertyName, - typeReference.TypeString, - WellKnownTypes.TypeContext, - context, + CreateTypeDefinitionReferenceExpression(typeReference, context), lineEnd); break; @@ -1768,10 +1766,8 @@ private void WriteAssignTypeRef( using (Writer.IncreaseIndent()) { Writer.WriteIndentedLine( - "typeInspector.GetTypeRef(typeof({0}), {1}.{2}),", - typeReference.TypeString, - WellKnownTypes.TypeContext, - context); + "{0},", + CreateTypeDefinitionReferenceExpression(typeReference, context)); Writer.WriteIndentedLine( "{0}){1}", typeReference.TypeStructure, @@ -1784,6 +1780,28 @@ private void WriteAssignTypeRef( } } + private static string CreateTypeDefinitionReferenceExpression( + SchemaTypeReference typeReference, + string context) + { + if (typeReference.Nullability is { } nullability) + { + return string.Format( + "global::{0}.Create(typeInspector.GetType(typeof({1}), {2}), {3}.{4})", + WellKnownTypes.TypeReference, + typeReference.TypeString, + nullability, + WellKnownTypes.TypeContext, + context); + } + + return string.Format( + "typeInspector.GetTypeRef(typeof({0}), {1}.{2})", + typeReference.TypeString, + WellKnownTypes.TypeContext, + context); + } + private static string GetResolverArgumentAssignments(int parameterCount) { if (parameterCount == 0) diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Helpers/TypeReferenceBuilder.cs b/src/HotChocolate/Core/src/Types.Analyzers/Helpers/TypeReferenceBuilder.cs index be33da743ec..b18135c9d79 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Helpers/TypeReferenceBuilder.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Helpers/TypeReferenceBuilder.cs @@ -83,22 +83,24 @@ public static SchemaTypeReference CreateTypeReference( } // Next, we create a key that describes the type and ensures we are only executing the type factory once. - var (typeStructure, typeDefinition, isSimpleType) = CreateTypeKey(unwrapped); + var (typeStructure, typeDefinition, nullability, isSimpleType) = CreateTypeKey(unwrapped); if (isSimpleType) { return new SchemaTypeReference( SchemaTypeReferenceKind.ExtendedTypeReference, - typeDefinition); + typeDefinition, + nullability: nullability); } return new SchemaTypeReference( SchemaTypeReferenceKind.FactoryTypeReference, typeDefinition, - typeStructure); + typeStructure, + nullability); } - private static (string TypeStructure, string TypeDefinition, bool IsSimpleType) CreateTypeKey( + private static (string TypeStructure, string TypeDefinition, string? Nullability, bool IsSimpleType) CreateTypeKey( ITypeSymbol unwrappedType) { bool isNullable; @@ -127,7 +129,7 @@ private static (string TypeStructure, string TypeDefinition, bool IsSimpleType) if (underlyingType is INamedTypeSymbol namedType && TryGetListElementType(namedType, out var listElementType)) { - var (typeStructure, typeDefinition, _) = CreateTypeKey(listElementType); + var (typeStructure, typeDefinition, elementNullability, _) = CreateTypeKey(listElementType); if (isNullable) { @@ -145,12 +147,12 @@ private static (string TypeStructure, string TypeDefinition, bool IsSimpleType) typeStructure); } - return (typeStructure, typeDefinition, false); + return (typeStructure, typeDefinition, elementNullability, false); } if (IsArrayType(unwrappedType, out var arrayElementType)) { - var (typeStructure, typeDefinition, _) = CreateTypeKey(arrayElementType); + var (typeStructure, typeDefinition, elementNullability, _) = CreateTypeKey(arrayElementType); if (isNullable) { @@ -168,11 +170,14 @@ private static (string TypeStructure, string TypeDefinition, bool IsSimpleType) typeStructure); } - return (typeStructure, typeDefinition, false); + return (typeStructure, typeDefinition, elementNullability, false); } var typeName = GetFullyQualifiedTypeName(underlyingType); var compliantTypeName = MakeGraphQLCompliant(typeName); + var nullability = ShouldPreserveNullability(underlyingType) + ? CreateNullabilityLiteral(underlyingType, isNullable) + : null; if (isNullable) { @@ -180,7 +185,7 @@ private static (string TypeStructure, string TypeDefinition, bool IsSimpleType) "new global::{0}(\"{1}\")", WellKnownTypes.NamedTypeNode, compliantTypeName); - return (typeStructure, typeName, IsSimpleType: unwrappedType.IsReferenceType); + return (typeStructure, typeName, nullability, IsSimpleType: unwrappedType.IsReferenceType); } else { @@ -189,10 +194,69 @@ private static (string TypeStructure, string TypeDefinition, bool IsSimpleType) WellKnownTypes.NonNullTypeNode, WellKnownTypes.NamedTypeNode, compliantTypeName); - return (typeStructure, typeName, IsSimpleType: false); + return (typeStructure, typeName, nullability, IsSimpleType: false); } } + private static bool ShouldPreserveNullability(ITypeSymbol typeSymbol) + => typeSymbol is INamedTypeSymbol { IsGenericType: true }; + + private static string CreateNullabilityLiteral( + ITypeSymbol typeSymbol, + bool isNullable) + { + var flags = new List(); + CollectNullability(typeSymbol, isNullable, flags); + + return flags.Count == 0 + ? "[]" + : $"[{string.Join(", ", flags)}]"; + } + + private static void CollectNullability( + ITypeSymbol typeSymbol, + bool isNullable, + List flags) + { + flags.Add(isNullable ? "true" : "false"); + + if (typeSymbol is not INamedTypeSymbol namedType || !namedType.IsGenericType) + { + return; + } + + // Nullable is represented by the wrapped value type and a nullable root flag. + if (namedType.OriginalDefinition.SpecialType is SpecialType.System_Nullable_T) + { + if (namedType.TypeArguments.Length == 1 + && namedType.TypeArguments[0] is INamedTypeSymbol innerNamed + && innerNamed.IsGenericType) + { + foreach (var argument in innerNamed.TypeArguments) + { + CollectNullability(argument, IsNullable(argument), flags); + } + } + return; + } + + foreach (var argument in namedType.TypeArguments) + { + CollectNullability(argument, IsNullable(argument), flags); + } + } + + private static bool IsNullable(ITypeSymbol typeSymbol) + { + if (typeSymbol is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T }) + { + return true; + } + + return typeSymbol.IsReferenceType + && typeSymbol.NullableAnnotation == NullableAnnotation.Annotated; + } + private static ITypeSymbol? UnwrapListElementType(ITypeSymbol typeSymbol) { if (typeSymbol is IArrayTypeSymbol arrayType) diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Models/SchemaTypeReference.cs b/src/HotChocolate/Core/src/Types.Analyzers/Models/SchemaTypeReference.cs index 60bf9ad4306..0aacf08f874 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Models/SchemaTypeReference.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Models/SchemaTypeReference.cs @@ -5,11 +5,13 @@ public readonly struct SchemaTypeReference public SchemaTypeReference( SchemaTypeReferenceKind kind, string typeString, - string? typeStructure = null) + string? typeStructure = null, + string? nullability = null) { Kind = kind; TypeString = typeString; TypeStructure = typeStructure; + Nullability = nullability; } public SchemaTypeReferenceKind Kind { get; } @@ -17,4 +19,6 @@ public SchemaTypeReference( public string TypeString { get; } public string? TypeStructure { get; } + + public string? Nullability { get; } } diff --git a/src/HotChocolate/Core/src/Types.OffsetPagination/Extensions/OffsetPagingObjectFieldDescriptorExtensions.cs b/src/HotChocolate/Core/src/Types.OffsetPagination/Extensions/OffsetPagingObjectFieldDescriptorExtensions.cs index 440fde9dee5..eb5285b8864 100644 --- a/src/HotChocolate/Core/src/Types.OffsetPagination/Extensions/OffsetPagingObjectFieldDescriptorExtensions.cs +++ b/src/HotChocolate/Core/src/Types.OffsetPagination/Extensions/OffsetPagingObjectFieldDescriptorExtensions.cs @@ -126,7 +126,8 @@ public static IObjectFieldDescriptor UseOffsetPaging( if (currentTypeRef is FactoryTypeReference factoryTypeRef && factoryTypeRef.TypeStructure.IsListType()) { - typeRef = factoryTypeRef.TypeDefinition; + // Preserve list element nullability from the generated type structure. + typeRef = factoryTypeRef.GetElementType(); } if (typeRef is null @@ -261,7 +262,8 @@ public static IInterfaceFieldDescriptor UseOffsetPaging( if (currentTypeRef is FactoryTypeReference factoryTypeRef && factoryTypeRef.TypeStructure.IsListType()) { - typeRef = factoryTypeRef.TypeDefinition; + // Preserve list element nullability from the generated type structure. + typeRef = factoryTypeRef.GetElementType(); } if (typeRef is null diff --git a/src/HotChocolate/Core/src/Types/Configuration/Handlers/SourceGeneratorTypeReferenceHandler.cs b/src/HotChocolate/Core/src/Types/Configuration/Handlers/SourceGeneratorTypeReferenceHandler.cs index 98eff0fe2a2..180d308c278 100644 --- a/src/HotChocolate/Core/src/Types/Configuration/Handlers/SourceGeneratorTypeReferenceHandler.cs +++ b/src/HotChocolate/Core/src/Types/Configuration/Handlers/SourceGeneratorTypeReferenceHandler.cs @@ -1,4 +1,5 @@ using HotChocolate.Types.Descriptors; +using HotChocolate.Types; namespace HotChocolate.Configuration; @@ -9,15 +10,16 @@ internal sealed class SourceGeneratorTypeReferenceHandler( { private readonly ExtendedTypeReferenceHandler _innerHandler = new(context.TypeInspector); - private readonly HashSet _handled = []; + private readonly HashSet<(string Key, int TypeHash, TypeContext Context)> _handled = []; public TypeReferenceKind Kind => TypeReferenceKind.Factory; public void Handle(ITypeRegistrar typeRegistrar, TypeReference typeReference) { var typeRef = (FactoryTypeReference)typeReference; + var marker = (typeRef.Key, typeRef.TypeDefinition.GetHashCode(), typeRef.TypeDefinition.Context); - if (_handled.Add(typeRef.Key)) + if (_handled.Add(marker)) { typeRegistry.Register(typeRef, typeRef.TypeDefinition); _innerHandler.Handle(typeRegistrar, typeRef.TypeDefinition); diff --git a/src/HotChocolate/Core/src/Types/Types/Scalars/AnyType.cs b/src/HotChocolate/Core/src/Types/Types/Scalars/AnyType.cs index 2420eb53f74..313d46b02ab 100644 --- a/src/HotChocolate/Core/src/Types/Types/Scalars/AnyType.cs +++ b/src/HotChocolate/Core/src/Types/Types/Scalars/AnyType.cs @@ -87,7 +87,7 @@ protected override void OnCoerceOutputValue(JsonElement runtimeValue, ResultElem case JsonValueKind.String: { var value = JsonMarshal.GetRawUtf8Value(runtimeValue); - resultValue.SetStringValue(value[1..^1]); + resultValue.SetStringValue(value[1..^1], isEncoded: true); break; } diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/SourceGeneratorOffsetPagingReproTests.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/SourceGeneratorOffsetPagingReproTests.cs index b02ffb067d9..5f7c31e09be 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/SourceGeneratorOffsetPagingReproTests.cs +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/SourceGeneratorOffsetPagingReproTests.cs @@ -1,5 +1,6 @@ using System.Reflection; using System.Runtime.Loader; +using System.Text.Json; using Basic.Reference.Assemblies; using GreenDonut; using GreenDonut.Data; @@ -54,6 +55,146 @@ public async Task Module_QueryType_Dictionary_Result_SourceGenerator_Path_Works_ Assert.Contains("\"value\": \"bar\"", sourceGenerated.Result, StringComparison.Ordinal); } + [Fact] + public async Task Module_Dictionary_MutationConventions_Input_Accepts_KeyValuePair_When_Query_Has_Dictionary_Output() + { + var assembly = CompileModuleDictionaryMutationConventionsReproAssembly(); + + var sourceGenerated = await ExecuteWithSourceGeneratorRegistrationAsync( + assembly, + registrationMethodName: "AddDemo", + query: + """ + mutation m { + patchUserSettings( + input: { + settings: [{ key: "open-workspace", value: "applications" }] + } + ) { + keyValuePairOfStringAndString { + key + value + } + } + } + """, + configureBuilder: static b => b.AddMutationConventions()); + + var addQueryAndMutationType = await ExecuteWithAddQueryAndMutationTypeRegistrationAsync( + assembly, + runtimeQueryTypeName: "Repro.RuntimeQuery", + runtimeMutationTypeName: "Repro.RuntimeMutation", + query: + """ + mutation m { + patchUserSettings( + input: { + settings: [{ key: "open-workspace", value: "applications" }] + } + ) { + keyValuePairOfStringAndString { + key + value + } + } + } + """, + configureBuilder: static b => b.AddMutationConventions()); + + Assert.Equal(addQueryAndMutationType.Result, sourceGenerated.Result); + Assert.DoesNotContain("\"errors\"", sourceGenerated.Result, StringComparison.Ordinal); + Assert.Contains("\"key\": \"open-workspace\"", sourceGenerated.Result, StringComparison.Ordinal); + Assert.Contains("\"value\": \"applications\"", sourceGenerated.Result, StringComparison.Ordinal); + Assert.Contains("type KeyValuePairOfStringAndString", sourceGenerated.Schema, StringComparison.Ordinal); + Assert.Contains("input KeyValuePairOfStringAndStringInput", sourceGenerated.Schema, StringComparison.Ordinal); + Assert.Contains("key: String!", sourceGenerated.Schema, StringComparison.Ordinal); + Assert.Contains("value: String!", sourceGenerated.Schema, StringComparison.Ordinal); + } + + [Fact] + public async Task Module_OffsetPaging_TaskIEnumerableOfNonNullReferenceType_Infers_NonNull_Items() + { + var assembly = CompileModuleOffsetPagingNullabilityReproAssembly(); + + var sourceGenerated = await ExecuteWithSourceGeneratorRegistrationAsync( + assembly, + registrationMethodName: "AddDemo", + query: + """ + { + foos { + items { + bar + } + } + } + """); + + var addQueryType = await ExecuteWithAddQueryTypeRegistrationAsync( + assembly, + runtimeQueryTypeName: "Repro.RuntimeQuery", + query: + """ + { + foos { + items { + bar + } + } + } + """); + + Assert.Contains("type FoosCollectionSegment", sourceGenerated.Schema, StringComparison.Ordinal); + Assert.Contains("items: [Foo!]", sourceGenerated.Schema, StringComparison.Ordinal); + Assert.Equal(addQueryType.Result, sourceGenerated.Result); + } + + [Fact] + public async Task Module_AnyType_Output_Does_Not_Double_Escape_Json_Escape_Sequences() + { + var assembly = CompileModuleAnyTypeEscapingReproAssembly(); + + var sourceGenerated = await ExecuteWithSourceGeneratorRegistrationAsync( + assembly, + registrationMethodName: "AddDemo", + query: + """ + { + foo + } + """, + configureBuilder: static b => b.AddJsonTypeConverter()); + + var addQueryType = await ExecuteWithAddQueryTypeRegistrationAsync( + assembly, + runtimeQueryTypeName: "Repro.RuntimeQuery", + query: + """ + { + foo + } + """, + configureBuilder: static b => b.AddJsonTypeConverter()); + + using var sourceGeneratedJson = JsonDocument.Parse(sourceGenerated.Result); + using var addQueryTypeJson = JsonDocument.Parse(addQueryType.Result); + + var sourceGeneratedDescription = sourceGeneratedJson.RootElement + .GetProperty("data") + .GetProperty("foo") + .GetProperty("description") + .GetString(); + + var addQueryTypeDescription = addQueryTypeJson.RootElement + .GetProperty("data") + .GetProperty("foo") + .GetProperty("description") + .GetString(); + + Assert.Equal("Special char: ü", sourceGeneratedDescription); + Assert.Equal("Special char: ü", addQueryTypeDescription); + } + private static async Task BuildSchemaWithSourceGeneratorRegistrationAsync(Assembly assembly) { var services = new ServiceCollection(); @@ -93,7 +234,8 @@ public async Task Module_QueryType_Dictionary_Result_SourceGenerator_Path_Works_ private static async Task ExecuteWithSourceGeneratorRegistrationAsync( Assembly assembly, string registrationMethodName, - string query) + string query, + Action? configureBuilder = null) { var builder = new ServiceCollection().AddGraphQLServer(disableDefaultSecurity: true); @@ -109,6 +251,7 @@ private static async Task ExecuteWithSourceGeneratorRegistratio }); addModuleMethod.Invoke(null, [builder]); + configureBuilder?.Invoke(builder); var executor = await builder.BuildRequestExecutorAsync(); var result = await executor.ExecuteAsync(query); @@ -118,7 +261,8 @@ private static async Task ExecuteWithSourceGeneratorRegistratio private static async Task ExecuteWithAddQueryTypeRegistrationAsync( Assembly assembly, string runtimeQueryTypeName, - string query) + string query, + Action? configureBuilder = null) { var runtimeQueryType = assembly.GetType(runtimeQueryTypeName) ?? throw new InvalidOperationException("Could not locate runtime query type."); @@ -126,6 +270,30 @@ private static async Task ExecuteWithAddQueryTypeRegistrationAs var builder = new ServiceCollection() .AddGraphQLServer(disableDefaultSecurity: true) .AddQueryType(runtimeQueryType); + configureBuilder?.Invoke(builder); + + var executor = await builder.BuildRequestExecutorAsync(); + var result = await executor.ExecuteAsync(query); + return new ExecutionResult(executor.Schema.ToString(), result.ToJson()); + } + + private static async Task ExecuteWithAddQueryAndMutationTypeRegistrationAsync( + Assembly assembly, + string runtimeQueryTypeName, + string runtimeMutationTypeName, + string query, + Action? configureBuilder = null) + { + var runtimeQueryType = assembly.GetType(runtimeQueryTypeName) + ?? throw new InvalidOperationException("Could not locate runtime query type."); + var runtimeMutationType = assembly.GetType(runtimeMutationTypeName) + ?? throw new InvalidOperationException("Could not locate runtime mutation type."); + + var builder = new ServiceCollection() + .AddGraphQLServer(disableDefaultSecurity: true) + .AddQueryType(runtimeQueryType) + .AddMutationType(runtimeMutationType); + configureBuilder?.Invoke(builder); var executor = await builder.BuildRequestExecutorAsync(); var result = await executor.ExecuteAsync(query); @@ -212,6 +380,134 @@ public Dictionary Foo() return CompileReproAssembly(source, "SourceGeneratorDictionaryModuleRepro"); } + private static Assembly CompileModuleDictionaryMutationConventionsReproAssembly() + { + const string source = """ + using System.Collections.Generic; + using System.Threading.Tasks; + using HotChocolate; + using HotChocolate.Types; + + [assembly: Module("Demo")] + + namespace Repro; + + [MutationType] + public static partial class SourceGeneratedMutation + { + public static async Task?> PatchUserSettingsAsync( + Dictionary settings) + { + await Task.Yield(); + return settings; + } + } + + [QueryType] + public static partial class SourceGeneratedQuery + { + public static async Task> GetUserSettingsAsync( + List settingIdentifiers) + { + await Task.Yield(); + return new(); + } + } + + public class RuntimeMutation + { + public async Task?> PatchUserSettingsAsync( + Dictionary settings) + { + await Task.Yield(); + return settings; + } + } + + public class RuntimeQuery + { + public async Task> GetUserSettingsAsync( + List settingIdentifiers) + { + await Task.Yield(); + return new(); + } + } + """; + + return CompileReproAssembly(source, "SourceGeneratorDictionaryMutationConventionsRepro"); + } + + private static Assembly CompileModuleOffsetPagingNullabilityReproAssembly() + { + const string source = """ + using System.Collections.Generic; + using System.Threading.Tasks; + using HotChocolate; + using HotChocolate.Types; + + [assembly: Module("Demo")] + + namespace Repro; + + [QueryType] + public static partial class SourceGeneratedQuery + { + [UseOffsetPaging] + public static async Task> GetFoos() + { + await Task.Yield(); + return []; + } + } + + public class RuntimeQuery + { + [UseOffsetPaging] + public async Task> GetFoos() + { + await Task.Yield(); + return []; + } + } + + public record Foo(string Bar); + """; + + return CompileReproAssembly(source, "SourceGeneratorOffsetPagingNullabilityRepro"); + } + + private static Assembly CompileModuleAnyTypeEscapingReproAssembly() + { + const string source = """ + using HotChocolate; + using HotChocolate.Types; + + [assembly: Module("Demo")] + + namespace Repro; + + [QueryType] + public static partial class SourceGeneratedQuery + { + [GraphQLType] + public static object GetFoo() + => new Foo("Special char: ü"); + } + + public class RuntimeQuery + { + [GraphQLType] + public object GetFoo() + => new Foo("Special char: ü"); + } + + public record Foo(string Description); + """; + + return CompileReproAssembly(source, "SourceGeneratorAnyTypeEscapingRepro"); + } + private static Assembly CompileReproAssembly(string source, string assemblyName) { var parseOptions = CSharpParseOptions.Default; diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/CollectionInferenceTests.Infer_Dictionary_As_List_Of_KeyValuePair.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/CollectionInferenceTests.Infer_Dictionary_As_List_Of_KeyValuePair.md index 80bbbb085be..50a7199b6bd 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/CollectionInferenceTests.Infer_Dictionary_As_List_Of_KeyValuePair.md +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/CollectionInferenceTests.Infer_Dictionary_As_List_Of_KeyValuePair.md @@ -83,7 +83,7 @@ namespace TestNamespace var naming = field.Context.Naming; configuration.Type = global::HotChocolate.Types.Descriptors.TypeReference.Create( - typeInspector.GetTypeRef(typeof(global::System.Collections.Generic.KeyValuePair), HotChocolate.Types.TypeContext.Output), + global::HotChocolate.Types.Descriptors.TypeReference.Create(typeInspector.GetType(typeof(global::System.Collections.Generic.KeyValuePair), [false, false, true]), HotChocolate.Types.TypeContext.Output), new global::HotChocolate.Language.NonNullTypeNode(new global::HotChocolate.Language.ListTypeNode(new global::HotChocolate.Language.NonNullTypeNode(new global::HotChocolate.Language.NamedTypeNode("global__System_Collections_Generic_KeyValuePairOfintAndstring"))))); configuration.ResultType = typeof(global::System.Collections.Generic.Dictionary); diff --git a/src/HotChocolate/Core/test/Types.Mutations.Tests/IssueDictionaryMutationConventionsReproTests.cs b/src/HotChocolate/Core/test/Types.Mutations.Tests/IssueDictionaryMutationConventionsReproTests.cs new file mode 100644 index 00000000000..2ad88eb00ae --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Mutations.Tests/IssueDictionaryMutationConventionsReproTests.cs @@ -0,0 +1,81 @@ +using HotChocolate.Execution; +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.Types; + +public class IssueDictionaryMutationConventionsReproTests +{ + [Fact] + public async Task Mutation_Convention_Dictionary_Input_Should_Accept_Key_And_Value_When_Query_Has_Dictionary_Output() + { + // arrange + var executor = await new ServiceCollection() + .AddGraphQL() + .AddQueryType() + .AddMutationType() + .AddMutationConventions() + .BuildRequestExecutorAsync(); + + // act + var result = await executor.ExecuteAsync( + """ + mutation m { + patchUserSettings( + input: { + settings: [{ key: "open-workspace", value: "applications" }] + } + ) { + keyValuePairOfStringAndString { + key + value + } + } + } + """); + + // assert + var operationResult = result.ExpectOperationResult(); + Assert.True(operationResult.Errors is null or { Count: 0 }, operationResult.ToJson()); + } + + [Fact] + public async Task Dictionary_String_Key_And_Value_Should_Be_NonNull_On_Input_And_Output_KeyValuePair_Types() + { + // arrange + var schema = await new ServiceCollection() + .AddGraphQL() + .AddQueryType() + .AddMutationType() + .AddMutationConventions() + .BuildSchemaAsync(); + + // act + var keyValuePairType = schema.Types.GetType("KeyValuePairOfStringAndString"); + var keyValuePairInputType = schema.Types.GetType("KeyValuePairOfStringAndStringInput"); + + // assert + Assert.True(keyValuePairType.Fields["key"].Type.IsNonNullType()); + Assert.True(keyValuePairType.Fields["value"].Type.IsNonNullType()); + Assert.True(keyValuePairInputType.Fields["key"].Type.IsNonNullType()); + Assert.True(keyValuePairInputType.Fields["value"].Type.IsNonNullType()); + } + + public class Query + { + public async Task> GetUserSettingsAsync(List settingIdentifiers) + { + await Task.Yield(); + return []; + } + } + + public class Mutation + { + public async Task?> PatchUserSettingsAsync( + Dictionary settings) + { + await Task.Yield(); + return settings; + } + } +} diff --git a/src/HotChocolate/Core/test/Types.OffsetPagination.Tests/OffsetPagingNullabilityRegressionTests.cs b/src/HotChocolate/Core/test/Types.OffsetPagination.Tests/OffsetPagingNullabilityRegressionTests.cs new file mode 100644 index 00000000000..bfc35591d84 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.OffsetPagination.Tests/OffsetPagingNullabilityRegressionTests.cs @@ -0,0 +1,39 @@ +using HotChocolate.Execution; +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.Types.Pagination; + +public class OffsetPagingNullabilityRegressionTests +{ + [Fact] + public async Task OffsetPaging_TaskEnumerableOfNonNullReferenceType_Infers_NonNull_Items() + { + // arrange + var schema = await new ServiceCollection() + .AddGraphQL() + .AddQueryType() + .BuildSchemaAsync(); + + // act + var segmentType = schema.Types.GetType("FoosCollectionSegment"); + var itemsType = segmentType.Fields["items"].Type; + + // assert + Assert.False(itemsType.IsNonNullType()); + var listType = Assert.IsType(itemsType); + Assert.True(listType.ElementType.IsNonNullType()); + Assert.Equal("Foo", listType.ElementType.NamedType().Name); + } + + public class Query + { + [UseOffsetPaging] + public async Task> GetFoos() + { + await Task.Yield(); + return []; + } + } + + public record Foo(string Bar); +} diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/IssueAnyTypeDoubleEscapingReproTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/IssueAnyTypeDoubleEscapingReproTests.cs new file mode 100644 index 00000000000..b381530edfd --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/IssueAnyTypeDoubleEscapingReproTests.cs @@ -0,0 +1,43 @@ +using System.Text.Json; +using HotChocolate.Execution; +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.Types; + +public class IssueAnyTypeDoubleEscapingReproTests +{ + [Fact] + public async Task AnyType_Output_Should_Not_Double_Escape_String_Escape_Sequences() + { + // arrange + // act + var result = await new ServiceCollection() + .AddGraphQLServer() + .AddJsonTypeConverter() + .AddQueryType() + .ExecuteRequestAsync( + """ + { + foo + } + """); + + // assert + using var json = JsonDocument.Parse(result.ToJson()); + var description = json.RootElement + .GetProperty("data") + .GetProperty("foo") + .GetProperty("description") + .GetString(); + + Assert.Equal("Special char: ü", description); + } + + public class Query + { + [GraphQLType] + public object Foo => new FooObject("Special char: ü"); + } + + public record FooObject(string Description); +}