diff --git a/src/HotChocolate/Data/src/Data/Projections/Expressions/QueryableProjectionProvider.cs b/src/HotChocolate/Data/src/Data/Projections/Expressions/QueryableProjectionProvider.cs index a99155e6c17..0a7e0a99757 100644 --- a/src/HotChocolate/Data/src/Data/Projections/Expressions/QueryableProjectionProvider.cs +++ b/src/HotChocolate/Data/src/Data/Projections/Expressions/QueryableProjectionProvider.cs @@ -104,10 +104,20 @@ private ApplyProjection CreateApplicator() var inMemory = IsInMemoryQuery(input); + var runtimeType = context.Selection.Type.UnwrapRuntimeType(); + + // Explicit union fields can unwrap to object even if the resolver returns + // a concrete base type (e.g. IQueryable). Use the resolver element + // type to keep the projection lambda parameter type stable. + if (runtimeType == typeof(object) && typeof(TEntityType) != typeof(object)) + { + runtimeType = typeof(TEntityType); + } + var visitorContext = new QueryableProjectionContext( context, context.ObjectType, - context.Selection.Type.UnwrapRuntimeType(), + runtimeType, inMemory); var visitor = new QueryableProjectionVisitor(); diff --git a/src/HotChocolate/Data/test/Data.Tests/Issue6953Tests.cs b/src/HotChocolate/Data/test/Data.Tests/Issue6953Tests.cs new file mode 100644 index 00000000000..ef1757cccad --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Tests/Issue6953Tests.cs @@ -0,0 +1,116 @@ +using HotChocolate.Execution; +using HotChocolate.Types; +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.Data; + +public class Issue6953Tests +{ + [Fact] + public async Task UseProjection_On_List_Union_With_Fragments_Does_Not_Throw() + { + var executor = await CreateExecutorAsync(); + + var result = await executor.ExecuteAsync( + """ + { + unionTest { + __typename + ... on ChildA { + a + } + ... on ChildB { + b + } + } + } + """); + + var operationResult = result.ExpectOperationResult(); + Assert.Empty(operationResult.Errors ?? []); + } + + [Fact] + public async Task UseProjection_On_List_Union_With_Typename_Only_Does_Not_Throw() + { + var executor = await CreateExecutorAsync(); + + var result = await executor.ExecuteAsync( + """ + { + unionTest { + __typename + } + } + """); + + var operationResult = result.ExpectOperationResult(); + Assert.Empty(operationResult.Errors ?? []); + } + + private static ValueTask CreateExecutorAsync() + => new ServiceCollection() + .AddGraphQL() + .AddProjections() + .AddQueryType() + .AddType() + .AddType() + .AddType() + .ModifyRequestOptions(o => o.IncludeExceptionDetails = true) + .BuildRequestExecutorAsync(); + + public class Query + { + [UseProjection] + [GraphQLType(typeof(ListType))] + public IQueryable GetUnionTest() + => Data.AsQueryable(); + } + + public class Base + { + public string C { get; set; } = string.Empty; + } + + public class ChildA : Base + { + public string A { get; set; } = string.Empty; + } + + public class ChildB : Base + { + public string B { get; set; } = string.Empty; + } + + public class ChildAType : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(x => x.A).Type>(); + } + } + + public class ChildBType : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(x => x.B).Type>(); + } + } + + public class UnionTestType : UnionType + { + protected override void Configure(IUnionTypeDescriptor descriptor) + { + descriptor.Name("UnionTestType"); + descriptor.Type(); + descriptor.Type(); + } + } + + private static readonly Base[] Data = + [ + new ChildA { C = "shared-a", A = "value-a" }, + new ChildB { C = "shared-b", B = "value-b" } + ]; +}