From a83c12e8eb18beab96b0774797542d6a6ce92e17 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Wed, 25 Feb 2026 21:03:08 +0000 Subject: [PATCH] Fix projection for fluent union list fields --- .../QueryableProjectionScopeExtensions.cs | 47 +++++-- .../test/Data.Tests/Issue5528ReproTests.cs | 117 ++++++++++++++++++ 2 files changed, 156 insertions(+), 8 deletions(-) create mode 100644 src/HotChocolate/Data/test/Data.Tests/Issue5528ReproTests.cs diff --git a/src/HotChocolate/Data/src/Data/Projections/Expressions/QueryableProjectionScopeExtensions.cs b/src/HotChocolate/Data/src/Data/Projections/Expressions/QueryableProjectionScopeExtensions.cs index 3a7eb114a08..b8e169a861b 100644 --- a/src/HotChocolate/Data/src/Data/Projections/Expressions/QueryableProjectionScopeExtensions.cs +++ b/src/HotChocolate/Data/src/Data/Projections/Expressions/QueryableProjectionScopeExtensions.cs @@ -67,19 +67,22 @@ public static Expression CreateSelection( Expression source, Type sourceType) { + var elementType = GetElementType(sourceType) ?? scope.RuntimeType; + var selector = CreateMemberInitLambda(scope, elementType); + var selection = Expression.Call( typeof(Enumerable), nameof(Enumerable.Select), [ scope.RuntimeType, - scope.RuntimeType + elementType ], source, - scope.CreateMemberInitLambda()); + selector); if (sourceType.IsArray) { - return ToArray(scope, selection); + return ToArray(selection, elementType); } if (TryGetSetType(sourceType, out var setType)) @@ -87,27 +90,40 @@ public static Expression CreateSelection( return ToSet(selection, setType); } - return ToList(scope, selection); + return ToList(selection, elementType); } - private static Expression ToArray(QueryableProjectionScope scope, Expression source) + private static Expression CreateMemberInitLambda( + QueryableProjectionScope scope, + Type targetType) + { + var projection = scope.CreateMemberInit(); + if (targetType != scope.RuntimeType) + { + projection = Expression.Convert(projection, targetType); + } + + return Expression.Lambda(projection, scope.Parameter); + } + + private static Expression ToArray(Expression source, Type elementType) { return Expression.Call( typeof(Enumerable), nameof(Enumerable.ToArray), [ - scope.RuntimeType + elementType ], source); } - private static Expression ToList(QueryableProjectionScope scope, Expression source) + private static Expression ToList(Expression source, Type elementType) { return Expression.Call( typeof(Enumerable), nameof(Enumerable.ToList), [ - scope.RuntimeType + elementType ], source); } @@ -157,4 +173,19 @@ private static bool TryGetSetType( setType = null; return false; } + + private static Type? GetElementType(Type type) + { + if (type.IsArray) + { + return type.GetElementType(); + } + + if (type.IsGenericType) + { + return type.GetGenericArguments()[0]; + } + + return null; + } } diff --git a/src/HotChocolate/Data/test/Data.Tests/Issue5528ReproTests.cs b/src/HotChocolate/Data/test/Data.Tests/Issue5528ReproTests.cs new file mode 100644 index 00000000000..50a251cd27a --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Tests/Issue5528ReproTests.cs @@ -0,0 +1,117 @@ +using HotChocolate.Execution; +using HotChocolate.Types; +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.Data; + +public class Issue5528ReproTests +{ + [Fact] + public async Task List_Of_Union_With_Fluent_Api_And_Projection_Does_Not_Throw() + { + var executor = await new ServiceCollection() + .AddGraphQL() + .AddProjections() + .AddQueryType() + .AddType() + .AddType() + .AddType() + .AddType() + .ModifyRequestOptions(o => o.IncludeExceptionDetails = true) + .BuildRequestExecutorAsync(); + + var result = await executor.ExecuteAsync( + """ + { + tenants { + id + entries { + ... on FileEntry { + name + fileSize + } + ... on FolderEntry { + name + childCount + } + } + } + } + """); + + var operationResult = result.ExpectOperationResult(); + Assert.Empty(operationResult.Errors ?? []); + } + + public class Query + { + [UseProjection] + public IQueryable GetTenants() + => Data.AsQueryable(); + } + + public class Tenant + { + public int Id { get; set; } + + public List Entries { get; set; } = []; + } + + public abstract class Entry + { + public string Name { get; set; } = string.Empty; + } + + public class FileEntry : Entry + { + public int FileSize { get; set; } + } + + public class FolderEntry : Entry + { + public int ChildCount { get; set; } + } + + public class TenantType : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Entries).Type>(); + } + } + + public class FileEntryType : ObjectType; + + public class FolderEntryType : ObjectType; + + public class FileOrFolderUnionType : UnionType + { + protected override void Configure(IUnionTypeDescriptor descriptor) + { + descriptor.Name("FileOrFolder"); + descriptor.Type(); + descriptor.Type(); + } + } + + private static readonly Tenant[] Data = + [ + new() + { + Id = 1, + Entries = + [ + new FileEntry + { + Name = "README.md", + FileSize = 123 + }, + new FolderEntry + { + Name = "src", + ChildCount = 3 + } + ] + } + ]; +}