diff --git a/src/HotChocolate/Core/src/Types.OffsetPagination/Extensions/OffsetPagingObjectFieldDescriptorExtensions.cs b/src/HotChocolate/Core/src/Types.OffsetPagination/Extensions/OffsetPagingObjectFieldDescriptorExtensions.cs index f51e20c1142..440fde9dee5 100644 --- a/src/HotChocolate/Core/src/Types.OffsetPagination/Extensions/OffsetPagingObjectFieldDescriptorExtensions.cs +++ b/src/HotChocolate/Core/src/Types.OffsetPagination/Extensions/OffsetPagingObjectFieldDescriptorExtensions.cs @@ -119,19 +119,30 @@ public static IObjectFieldDescriptor UseOffsetPaging( ? c.TypeInspector.GetTypeRef(itemType) : null; - if (typeRef is null - && d.Type is SyntaxTypeReference syntaxTypeRef - && syntaxTypeRef.Type.IsListType()) + if (typeRef is null) { - typeRef = syntaxTypeRef.WithType(syntaxTypeRef.Type.ElementType()); - } + var currentTypeRef = d.Type; - if (typeRef is null - && d.Type is ExtendedTypeReference extendedTypeRef - && c.TypeInspector.TryCreateTypeInfo(extendedTypeRef.Type, out var typeInfo) - && GetElementType(typeInfo) is { } elementType) - { - typeRef = TypeReference.Create(elementType, TypeContext.Output); + if (currentTypeRef is FactoryTypeReference factoryTypeRef + && factoryTypeRef.TypeStructure.IsListType()) + { + typeRef = factoryTypeRef.TypeDefinition; + } + + if (typeRef is null + && currentTypeRef is SyntaxTypeReference syntaxTypeRef + && syntaxTypeRef.Type.IsListType()) + { + typeRef = syntaxTypeRef.WithType(syntaxTypeRef.Type.ElementType()); + } + + if (typeRef is null + && currentTypeRef is ExtendedTypeReference extendedTypeRef + && c.TypeInspector.TryCreateTypeInfo(extendedTypeRef.Type, out var typeInfo) + && GetElementType(typeInfo) is { } elementType) + { + typeRef = TypeReference.Create(elementType, TypeContext.Output); + } } var resolverMember = d.ResolverMember ?? d.Member; @@ -247,12 +258,14 @@ public static IInterfaceFieldDescriptor UseOffsetPaging( { var currentTypeRef = d.Type; - if (currentTypeRef is FactoryTypeReference factoryTypeRef) + if (currentTypeRef is FactoryTypeReference factoryTypeRef + && factoryTypeRef.TypeStructure.IsListType()) { - currentTypeRef = factoryTypeRef.GetElementType(); + typeRef = factoryTypeRef.TypeDefinition; } - if (currentTypeRef is ExtendedTypeReference extendedTypeRef + if (typeRef is null + && currentTypeRef is ExtendedTypeReference extendedTypeRef && c.TypeInspector.TryCreateTypeInfo(extendedTypeRef.Type, out var typeInfo) && GetElementType(typeInfo) is { } elementType) { diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/HotChocolate.Types.Analyzers.Tests.csproj b/src/HotChocolate/Core/test/Types.Analyzers.Tests/HotChocolate.Types.Analyzers.Tests.csproj index f4438cf86d2..78a45915e2a 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/HotChocolate.Types.Analyzers.Tests.csproj +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/HotChocolate.Types.Analyzers.Tests.csproj @@ -20,6 +20,7 @@ + diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/SourceGeneratorOffsetPagingReproTests.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/SourceGeneratorOffsetPagingReproTests.cs new file mode 100644 index 00000000000..665e1a7c15f --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/SourceGeneratorOffsetPagingReproTests.cs @@ -0,0 +1,185 @@ +using System.Reflection; +using System.Runtime.Loader; +using Basic.Reference.Assemblies; +using GreenDonut; +using GreenDonut.Data; +using HotChocolate.Data.Filters; +using HotChocolate.Execution; +using HotChocolate.Execution.Configuration; +using HotChocolate.Execution.Processing; +using HotChocolate.Features; +using HotChocolate.Language; +using HotChocolate.Types.Analyzers; +using HotChocolate.Types.Pagination; +using Microsoft.AspNetCore.Builder; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.Types; + +public class SourceGeneratorOffsetPagingReproTests +{ + [Fact] + public async Task QueryType_SourceGenerator_Path_Works_Like_AddQueryType_Path() + { + var assembly = CompileReproAssembly(); + + var sourceGeneratorException = await BuildSchemaWithSourceGeneratorRegistrationAsync(assembly); + var addQueryTypeException = await BuildSchemaWithAddQueryTypeRegistrationAsync(assembly); + + Assert.Null(sourceGeneratorException); + Assert.Null(addQueryTypeException); + } + + private static async Task BuildSchemaWithSourceGeneratorRegistrationAsync(Assembly assembly) + { + var services = new ServiceCollection(); + var builder = services.AddGraphQLServer(disableDefaultSecurity: true); + + var addTypesMethod = assembly + .GetTypes() + .Where(t => t is { IsAbstract: true, IsSealed: true } + && t.Namespace == "Microsoft.Extensions.DependencyInjection") + .SelectMany(t => t.GetMethods(BindingFlags.Public | BindingFlags.Static)) + .Single(m => + { + var p = m.GetParameters(); + return m.Name.StartsWith("Add", StringComparison.Ordinal) + && m.Name.EndsWith("Types", StringComparison.Ordinal) + && m.ReturnType == typeof(IRequestExecutorBuilder) + && p.Length == 1 + && p[0].ParameterType == typeof(IRequestExecutorBuilder); + }); + + addTypesMethod.Invoke(null, [builder]); + + return await Record.ExceptionAsync( + async () => await builder.BuildSchemaAsync()); + } + + private static async Task BuildSchemaWithAddQueryTypeRegistrationAsync(Assembly assembly) + { + var runtimeQueryType = assembly.GetType("Repro.RuntimeQuery") + ?? throw new InvalidOperationException("Could not locate runtime query type."); + + var builder = new ServiceCollection() + .AddGraphQLServer(disableDefaultSecurity: true) + .AddQueryType(runtimeQueryType); + + return await Record.ExceptionAsync( + async () => await builder.BuildSchemaAsync()); + } + + private static Assembly CompileReproAssembly() + { + const string source = """ + using System.Collections.Generic; + using System.Threading.Tasks; + using HotChocolate.Types; + + namespace Repro; + + [QueryType] + public static partial class SourceGeneratedQuery + { + [UseOffsetPaging] + public static async Task> UglyLegacyResolver() + { + await Task.Yield(); + return new(); + } + } + + public class RuntimeQuery + { + [UseOffsetPaging] + public async Task> UglyLegacyResolver() + { + await Task.Yield(); + return new(); + } + } + """; + + var parseOptions = CSharpParseOptions.Default; + var syntaxTree = CSharpSyntaxTree.ParseText(source, parseOptions); + + IEnumerable references = + [ +#if NET8_0 + .. Net80.References.All, +#elif NET9_0 + .. Net90.References.All, +#elif NET10_0 + .. Net100.References.All, +#endif + MetadataReference.CreateFromFile(typeof(ITypeSystemMember).Assembly.Location), + MetadataReference.CreateFromFile(typeof(RequestDelegate).Assembly.Location), + MetadataReference.CreateFromFile(typeof(RequestContext).Assembly.Location), + MetadataReference.CreateFromFile(typeof(HotChocolateExecutionSelectionExtensions).Assembly.Location), + MetadataReference.CreateFromFile(typeof(IRequestExecutorBuilder).Assembly.Location), + MetadataReference.CreateFromFile(typeof(ISelection).Assembly.Location), + MetadataReference.CreateFromFile(typeof(QueryTypeAttribute).Assembly.Location), + MetadataReference.CreateFromFile(typeof(Connection).Assembly.Location), + MetadataReference.CreateFromFile(typeof(PageConnection<>).Assembly.Location), + MetadataReference.CreateFromFile(typeof(ISchemaDefinition).Assembly.Location), + MetadataReference.CreateFromFile(typeof(IFeatureProvider).Assembly.Location), + MetadataReference.CreateFromFile(typeof(OperationType).Assembly.Location), + MetadataReference.CreateFromFile(typeof(ParentAttribute).Assembly.Location), + MetadataReference.CreateFromFile( + typeof(HotChocolateAspNetCoreServiceCollectionExtensions).Assembly.Location), + MetadataReference.CreateFromFile(typeof(DataLoaderBase<,>).Assembly.Location), + MetadataReference.CreateFromFile(typeof(IDataLoader).Assembly.Location), + MetadataReference.CreateFromFile(typeof(PagingArguments).Assembly.Location), + MetadataReference.CreateFromFile(typeof(IPredicateBuilder).Assembly.Location), + MetadataReference.CreateFromFile(typeof(DefaultPredicateBuilder).Assembly.Location), + MetadataReference.CreateFromFile(typeof(IFilterContext).Assembly.Location), + MetadataReference.CreateFromFile(typeof(WebApplication).Assembly.Location), + MetadataReference.CreateFromFile(typeof(IServiceCollection).Assembly.Location), + MetadataReference.CreateFromFile(typeof(Microsoft.AspNetCore.Authorization.AuthorizeAttribute).Assembly.Location), + MetadataReference.CreateFromFile(typeof(Authorization.AuthorizeAttribute).Assembly.Location), + MetadataReference.CreateFromFile(typeof(UseOffsetPagingAttribute).Assembly.Location) + ]; + + var compilation = CSharpCompilation.Create( + assemblyName: "SourceGeneratorOffsetPagingRepro", + syntaxTrees: [syntaxTree], + references: references, + options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + var driver = CSharpGeneratorDriver + .Create(new GraphQLServerGenerator()) + .RunGenerators(compilation); + + var generatedTrees = driver + .GetRunResult() + .Results + .SelectMany(t => t.GeneratedSources) + .Select(s => CSharpSyntaxTree.ParseText( + s.SourceText, + parseOptions, + path: s.HintName)); + + var updatedCompilation = compilation.AddSyntaxTrees(generatedTrees); + + using var stream = new MemoryStream(); + var emitResult = updatedCompilation.Emit(stream); + + if (!emitResult.Success) + { + throw new InvalidOperationException( + string.Join( + Environment.NewLine, + emitResult.Diagnostics + .OrderBy(d => d.Severity) + .ThenBy(d => d.Id) + .Select(d => d.ToString()))); + } + + stream.Position = 0; + + var context = new AssemblyLoadContext("SourceGeneratorOffsetPagingRepro", isCollectible: true); + return context.LoadFromStream(stream); + } +} diff --git a/src/HotChocolate/Core/test/Types.OffsetPagination.Tests/IntegrationTests.cs b/src/HotChocolate/Core/test/Types.OffsetPagination.Tests/IntegrationTests.cs index 10160c0e9aa..70bb9026936 100644 --- a/src/HotChocolate/Core/test/Types.OffsetPagination.Tests/IntegrationTests.cs +++ b/src/HotChocolate/Core/test/Types.OffsetPagination.Tests/IntegrationTests.cs @@ -695,6 +695,20 @@ public async Task Simple_EnumerableValueType_ReturnsError() Assert.Equal("Cannot handle the specified data source.", error.Message); } + [Fact] + public async Task Attribute_Dictionary_ReturnType_ThrowsSchemaException() + { + // act + var schema = + await new ServiceCollection() + .AddGraphQL() + .AddQueryType() + .BuildSchemaAsync(); + + // assert + schema.MatchSnapshot(); + } + public class QueryType : ObjectType { protected override void Configure(IObjectTypeDescriptor descriptor) @@ -830,6 +844,16 @@ public ImmutableArray Test() return []; } } + + public class UglyLegacyQuery + { + [UseOffsetPaging] + public async Task> UglyLegacyResolver() + { + await Task.Yield(); + return []; + } + } } public class MockExecutable(IQueryable source) : IExecutable diff --git a/src/HotChocolate/Core/test/Types.OffsetPagination.Tests/__snapshots__/IntegrationTests.Attribute_Dictionary_ReturnType_ThrowsSchemaException.graphql b/src/HotChocolate/Core/test/Types.OffsetPagination.Tests/__snapshots__/IntegrationTests.Attribute_Dictionary_ReturnType_ThrowsSchemaException.graphql new file mode 100644 index 00000000000..7b0c7cc89a7 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.OffsetPagination.Tests/__snapshots__/IntegrationTests.Attribute_Dictionary_ReturnType_ThrowsSchemaException.graphql @@ -0,0 +1,28 @@ +schema { + query: UglyLegacyQuery +} + +"Information about the offset pagination." +type CollectionSegmentInfo { + "Indicates whether more items exist following the set defined by the clients arguments." + hasNextPage: Boolean! + "Indicates whether more items exist prior the set defined by the clients arguments." + hasPreviousPage: Boolean! +} + +type KeyValuePairOfStringAndString { + key: String! + value: String! +} + +type UglyLegacyQuery { + uglyLegacyResolver(skip: Int take: Int): UglyLegacyResolverCollectionSegment +} + +"A segment of a collection." +type UglyLegacyResolverCollectionSegment { + "Information to aid in pagination." + pageInfo: CollectionSegmentInfo! + "A flattened list of the items." + items: [KeyValuePairOfStringAndString!] +}