From 3c7d02d90de0db690b94d88a83c1b719ffb7c832 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Tue, 29 Sep 2026 08:17:47 +1000 Subject: [PATCH] Remove query splitting when the includes fallback loads no collection #1422 removed a resolver's AsSplitQuery only when the projection could be built as a select. When it cannot, as for a type with derived types, ApplyProjection falls back to includes and left the split in place, so a connection over such a type that selected no collection still threw under Verify.EntityFramework 16. The includes come from the same projection, so the check now runs before either path. --- .../IncludeAppender.cs | 10 +++-- ...ived_types_without_collection.verified.txt | 38 +++++++++++++++++++ .../IntegrationTests.SchemaPrint.verified.txt | 12 ++++++ ...IntegrationTests_connection_split_query.cs | 30 +++++++++++++++ src/Tests/IntegrationTests/Query.cs | 7 ++++ 5 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 src/Tests/IntegrationTests/IntegrationTests.Connection_split_query_with_derived_types_without_collection.verified.txt diff --git a/src/GraphQL.EntityFramework/IncludeAppender.cs b/src/GraphQL.EntityFramework/IncludeAppender.cs index 6ef4169f..4c16eb35 100644 --- a/src/GraphQL.EntityFramework/IncludeAppender.cs +++ b/src/GraphQL.EntityFramework/IncludeAppender.cs @@ -34,14 +34,16 @@ public IQueryable ApplyProjection( projection = MergeFilterFieldsIntoProjection(projection, filters, type); } - if (!SelectExpressionBuilder.TryBuild(projection, keyNames, derivedTypes, out var expression, out var includePaths, out var argumentFields)) + // Includes are built from the same projection, so without a collection in it neither the + // select nor the includes fallback loads one + if (!LoadsCollection(projection)) { - return AddIncludesFromProjection(query, projection); + query = QuerySplitting.Remove(query); } - if (!LoadsCollection(projection)) + if (!SelectExpressionBuilder.TryBuild(projection, keyNames, derivedTypes, out var expression, out var includePaths, out var argumentFields)) { - query = QuerySplitting.Remove(query); + return AddIncludesFromProjection(query, projection); } foreach (var includePath in includePaths) diff --git a/src/Tests/IntegrationTests/IntegrationTests.Connection_split_query_with_derived_types_without_collection.verified.txt b/src/Tests/IntegrationTests/IntegrationTests.Connection_split_query_with_derived_types_without_collection.verified.txt new file mode 100644 index 00000000..131c19f6 --- /dev/null +++ b/src/Tests/IntegrationTests/IntegrationTests.Connection_split_query_with_derived_types_without_collection.verified.txt @@ -0,0 +1,38 @@ +{ + target: { + Data: { + splitInterfaceGraphConnection: { + totalCount: 2, + items: [ + { + property: Value1 + }, + { + property: Value2 + } + ] + } + } + }, + sql: [ + { + Text: +select COUNT(*) +from BaseEntities as b + }, + { + Text: +select b.Id, + b.Discriminator, + b.Property, + b.Status +from BaseEntities as b +order by b.Property +offset @p rows fetch next @p1 rows only, + Parameters: { + @p: 0, + @p1: 10 + } + } + ] +} \ No newline at end of file diff --git a/src/Tests/IntegrationTests/IntegrationTests.SchemaPrint.verified.txt b/src/Tests/IntegrationTests/IntegrationTests.SchemaPrint.verified.txt index d6cdbf11..52437ad2 100644 --- a/src/Tests/IntegrationTests/IntegrationTests.SchemaPrint.verified.txt +++ b/src/Tests/IntegrationTests/IntegrationTests.SchemaPrint.verified.txt @@ -106,6 +106,18 @@ where: BaseEntityWhere, orderBy: [BaseEntityOrderBy!], ids: [ID!]): BaseConnection! + splitInterfaceGraphConnection( + "Only return edges after the specified cursor." + after: String, + "Specifies the maximum number of edges to return, starting after the cursor specified by 'after', or the first number of edges if 'after' is not specified." + first: Int, + "Only return edges prior to the specified cursor." + before: String, + "Specifies the maximum number of edges to return, starting prior to the cursor specified by 'before', or the last number of edges if 'before' is not specified." + last: Int, + where: BaseEntityWhere, + orderBy: [BaseEntityOrderBy!], + ids: [ID!]): BaseConnection! baseEntities(id: ID, ids: [ID!], where: BaseEntityWhere, orderBy: [BaseEntityOrderBy!], skip: Int, take: Int): [BaseEntity]! derivedEntities(id: ID, ids: [ID!], where: DerivedEntityWhere, orderBy: [DerivedEntityOrderBy!], skip: Int, take: Int): [Derived!]! derivedEntity(id: ID, ids: [ID!], where: DerivedEntityWhere): Derived! diff --git a/src/Tests/IntegrationTests/IntegrationTests_connection_split_query.cs b/src/Tests/IntegrationTests/IntegrationTests_connection_split_query.cs index 4d2e32e4..2a53ba56 100644 --- a/src/Tests/IntegrationTests/IntegrationTests_connection_split_query.cs +++ b/src/Tests/IntegrationTests/IntegrationTests_connection_split_query.cs @@ -22,6 +22,36 @@ public async Task Connection_split_query_without_collection() await RunQuery(database, query, null, null, false, entities); } + // The resolver's AsSplitQuery is removed from a connection over a type with derived types, + // since no collection is selected + [Fact] + public async Task Connection_split_query_with_derived_types_without_collection() + { + var query = + """ + { + splitInterfaceGraphConnection(first: 10) { + totalCount + items { + property + } + } + } + """; + + var derived1 = new DerivedEntity + { + Property = "Value1" + }; + var derived2 = new DerivedWithNavigationEntity + { + Property = "Value2" + }; + + await using var database = await sqlInstance.Build(); + await RunQuery(database, query, null, null, false, [derived1, derived2]); + } + // The resolver's AsSplitQuery is kept, since the children are selected [Fact] public async Task Connection_split_query_with_collection() diff --git a/src/Tests/IntegrationTests/Query.cs b/src/Tests/IntegrationTests/Query.cs index a4de5f85..485508d8 100644 --- a/src/Tests/IntegrationTests/Query.cs +++ b/src/Tests/IntegrationTests/Query.cs @@ -172,6 +172,13 @@ public Query(IEfGraphQLService efGraphQlService) name: "interfaceGraphConnection", resolve: _ => _.DbContext.BaseEntities.OrderBy(_ => _.Property)); + // Split for the children a request might select, over a type with derived types + efGraphQlService.AddQueryConnectionField( + this, + itemGraphType: typeof(BaseGraphType), + name: "splitInterfaceGraphConnection", + resolve: _ => _.DbContext.BaseEntities.AsSplitQuery().OrderBy(_ => _.Property)); + AddQueryField( name: "baseEntities", graphType: typeof(BaseGraphType),