Skip to content

Commit 6c393d3

Browse files
authored
Fixed: Preserve ordering when executing batched paging queries. (#7994)
1 parent b8fcc1c commit 6c393d3

18 files changed

+191
-30
lines changed

src/GreenDonut/src/GreenDonut.Data.EntityFramework/Expressions/ExpressionHelpers.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -441,11 +441,16 @@ protected override Expression VisitMethodCall(MethodCallExpression node)
441441
// we are not interested in nested order by calls
442442
if (node.Method.DeclaringType == typeof(Queryable) && node.Method.Name == nameof(Queryable.Select))
443443
{
444+
// We first visit our parent. When we visit an expression
445+
// like "OrderBy().Select()", we want to visit the OrderBy first.
446+
var source = Visit(node.Arguments[0]);
447+
444448
var previousState = _insideSelectProjection;
445449
_insideSelectProjection = true;
446-
var result = base.VisitMethodCall(node);
450+
var projection = Visit(node.Arguments[1]);
447451
_insideSelectProjection = previousState;
448-
return result;
452+
453+
return node.Update(null, [source, projection]);
449454
}
450455

451456
if (node.Method.DeclaringType == typeof(Queryable)

src/HotChocolate/Data/test/Data.PostgreSQL.Tests/IntegrationTests.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,34 @@ ... on Brand {
211211
MatchSnapshot(result, interceptor);
212212
}
213213

214+
[Fact]
215+
public async Task Query_Products_First_2_And_Brand()
216+
{
217+
// arrange
218+
using var interceptor = new TestQueryInterceptor();
219+
220+
// act
221+
var result = await ExecuteAsync(
222+
"""
223+
{
224+
products(first: 2) {
225+
nodes {
226+
name
227+
brand {
228+
name
229+
}
230+
}
231+
}
232+
}
233+
234+
""",
235+
interceptor);
236+
237+
// assert
238+
MatchSnapshot(result, interceptor);
239+
}
240+
241+
214242
private static ServiceProvider CreateServer(string connectionString)
215243
{
216244
var services = new ServiceCollection();

src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Services/ProductService.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
using GreenDonut.Data;
2+
using HotChocolate.Data.Data;
23
using HotChocolate.Data.Models;
34

45
namespace HotChocolate.Data.Services;
56

6-
public class ProductService(IProductBatchingContext batchingContext)
7+
public class ProductService(CatalogContext context, IProductBatchingContext batchingContext)
78
{
89
public async Task<Product?> GetProductByIdAsync(
910
int id,
@@ -13,6 +14,12 @@ public class ProductService(IProductBatchingContext batchingContext)
1314
.With(query)
1415
.LoadAsync(id, cancellationToken);
1516

17+
public async Task<Page<Product>> GetProductsAsync(
18+
PagingArguments pagingArgs,
19+
QueryContext<Product>? query = null,
20+
CancellationToken cancellationToken = default)
21+
=> await context.Products.With(query, DefaultOrder).ToPageAsync(pagingArgs, cancellationToken);
22+
1623
public async Task<Page<Product>> GetProductsByBrandAsync(
1724
int brandId,
1825
PagingArguments pagingArgs,
@@ -22,4 +29,7 @@ public async Task<Page<Product>> GetProductsByBrandAsync(
2229
.With(pagingArgs, query)
2330
.LoadAsync(brandId, cancellationToken)
2431
?? Page<Product>.Empty;
32+
33+
private static SortDefinition<Product> DefaultOrder(SortDefinition<Product> sort)
34+
=> sort.IfEmpty(o => o.AddDescending(t => t.Name)).AddAscending(t => t.Id);
2535
}

src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Brands/BrandNode.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
using HotChocolate.Types;
55
using HotChocolate.Types.Pagination;
66

7-
namespace HotChocolate.Data.Types;
7+
namespace HotChocolate.Data.Types.Brands;
88

99
[ObjectType<Brand>]
1010
public static partial class BrandNode

src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Brands/BrandQueries.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
using HotChocolate.Types.Pagination;
66
using HotChocolate.Types.Relay;
77

8-
namespace HotChocolate.Data.Types;
8+
namespace HotChocolate.Data.Types.Brands;
99

1010
[QueryType]
1111
public static class BrandQueries
@@ -20,7 +20,7 @@ public static async Task<Connection<Brand>> GetBrandsAsync(
2020
=> await brandService.GetBrandsAsync(pagingArgs, query, cancellationToken).ToConnectionAsync();
2121

2222
[NodeResolver]
23-
public static async Task<Brand?> GetBrandAsync(
23+
public static async Task<Brand?> GetBrandByIdAsync(
2424
int id,
2525
QueryContext<Brand> query,
2626
BrandService brandService,

src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Brands/BrandSortInputType.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
using HotChocolate.Data.Models;
22
using HotChocolate.Data.Sorting;
33

4-
namespace HotChocolate.Data.Types;
4+
namespace HotChocolate.Data.Types.Brands;
55

66
public sealed class BrandSortInputType : SortInputType<Brand>
77
{

src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Products/ProductNode.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
using GreenDonut.Data;
22
using HotChocolate.Data.Models;
33
using HotChocolate.Data.Services;
4+
using HotChocolate.Execution.Processing;
45
using HotChocolate.Types;
56
using HotChocolate.Types.Relay;
67

7-
namespace HotChocolate.Data.Types;
8+
namespace HotChocolate.Data.Types.Products;
89

910
[ObjectType<Product>]
1011
public static partial class ProductNode
1112
{
13+
// [BindMember(nameof(Product.Brand))]
1214
public static async Task<Brand?> GetBrandAsync(
13-
[Parent(requires: nameof(Product.BrandId))] Product product,
14-
BrandService brandService,
15+
[Parent(requires: nameof(Product.BrandId))] Product product,
1516
QueryContext<Brand> query,
17+
ISelection selection,
18+
BrandService brandService,
1619
CancellationToken cancellationToken)
1720
=> await brandService.GetBrandByIdAsync(product.BrandId, query, cancellationToken);
1821

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using GreenDonut.Data;
2+
using HotChocolate.Data.Models;
3+
using HotChocolate.Data.Services;
4+
using HotChocolate.Types;
5+
using HotChocolate.Types.Pagination;
6+
7+
namespace HotChocolate.Data.Types.Products;
8+
9+
10+
[QueryType]
11+
public static partial class ProductQueries
12+
{
13+
[UsePaging]
14+
[UseFiltering]
15+
[UseSorting]
16+
public static async Task<Connection<Product>> GetProductsAsync(
17+
PagingArguments pagingArgs,
18+
QueryContext<Product> query,
19+
ProductService productService,
20+
CancellationToken cancellationToken)
21+
=> await productService.GetProductsAsync(pagingArgs, query, cancellationToken).ToConnectionAsync();
22+
}

src/HotChocolate/Data/test/Data.PostgreSQL.Tests/Types/Products/ProductSortInputType.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
using HotChocolate.Data.Models;
22
using HotChocolate.Data.Sorting;
33

4-
namespace HotChocolate.Data.Types;
4+
namespace HotChocolate.Data.Types.Products;
55

66
public sealed class ProductSortInputType : SortInputType<Product>
77
{

src/HotChocolate/Data/test/Data.PostgreSQL.Tests/__snapshots__/IntegrationTests.CreateSchema.graphql

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,9 @@ type Query {
9090
node("ID of the object." id: ID!): Node @cost(weight: "10")
9191
"Lookup nodes by a list of IDs."
9292
nodes("The list of node IDs." ids: [ID!]!): [Node]! @cost(weight: "10")
93+
products("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String where: ProductFilterInput @cost(weight: "10") order: [ProductSortInput!] @cost(weight: "10")): ProductsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10")
9394
brands("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String where: BrandFilterInput @cost(weight: "10")): BrandsConnection @listSize(assumedSize: 50, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 10, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10")
94-
brand(id: ID!): Brand @cost(weight: "10")
95+
brandById(id: ID!): Brand @cost(weight: "10")
9596
}
9697

9798
input BooleanOperationFilterInput {

0 commit comments

Comments
 (0)