Skip to content

Commit 02a5cc1

Browse files
authored
Do not inline total count by default into paging queries. (#7942)
1 parent af52f4b commit 02a5cc1

File tree

12 files changed

+164
-70
lines changed

12 files changed

+164
-70
lines changed

src/HotChocolate/Core/src/Abstractions/DefaultQueryableExecutable.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public IQueryableExecutable<TQuery> WithSource<TQuery>(IQueryable<TQuery> src)
3838
return await enumerator.MoveNextAsync().ConfigureAwait(false) ? enumerator.Current : default;
3939
}
4040

41-
return await Task.Run(() => source.FirstOrDefault<T>(), cancellationToken).WaitAsync(cancellationToken);
41+
return await Task.Run(source.FirstOrDefault, cancellationToken).WaitAsync(cancellationToken);
4242
}
4343

4444
public override ValueTask<T?> SingleOrDefaultAsync(CancellationToken cancellationToken = default)
@@ -127,7 +127,7 @@ private async ValueTask<List<T>> ToListFromDataSourceAsync(CancellationToken can
127127
return result;
128128
}
129129

130-
return await Task.Run(() => source.ToList(), cancellationToken).WaitAsync(cancellationToken);
130+
return await Task.Run(source.ToList, cancellationToken).WaitAsync(cancellationToken);
131131
}
132132

133133
public override async IAsyncEnumerable<T> ToAsyncEnumerable(

src/HotChocolate/Core/src/Abstractions/IQueryableExecutable.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ public interface IQueryableExecutable<T> : IExecutable<T>
2323
/// <returns>The new instance of an enumerable executable</returns>
2424
IQueryableExecutable<T> WithSource(IQueryable<T> source);
2525

26-
2726
/// <summary>
2827
/// Returns a new executable with the provided source
2928
/// </summary>

src/HotChocolate/Core/src/Types.CursorPagination/Extensions/CursorPagingRequestExecutorBuilderExtension.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ public static class CursorPagingRequestExecutorBuilderExtension
2222
/// <param name="defaultProvider">
2323
/// Defines if the registered provider shall be registered as the default provider.
2424
/// </param>
25+
/// <param name="inlineTotalCount">
26+
/// Specifies that the paging provider shall inline the total count query into the
27+
/// sliced query in order to have a single database call.
28+
/// Some database providers might not support this feature.
29+
/// </param>
2530
/// <returns>
2631
/// The request executor builder.
2732
/// </returns>
@@ -31,9 +36,11 @@ public static class CursorPagingRequestExecutorBuilderExtension
3136
public static IRequestExecutorBuilder AddQueryableCursorPagingProvider(
3237
this IRequestExecutorBuilder builder,
3338
string? providerName = null,
34-
bool defaultProvider = false)
39+
bool defaultProvider = false,
40+
bool? inlineTotalCount = null)
3541
=> AddCursorPagingProvider<QueryableCursorPagingProvider>(
3642
builder,
43+
_ => new QueryableCursorPagingProvider(inlineTotalCount),
3744
providerName,
3845
defaultProvider);
3946

src/HotChocolate/Core/src/Types.CursorPagination/QueryableCursorPagingHandler.cs

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,19 @@
33

44
namespace HotChocolate.Types.Pagination;
55

6-
internal sealed class QueryableCursorPagingHandler<TEntity>(PagingOptions options)
7-
: CursorPagingHandler<IQueryable<TEntity>, TEntity>(options)
6+
internal sealed class QueryableCursorPagingHandler<TEntity> : CursorPagingHandler<IQueryable<TEntity>, TEntity>
87
{
8+
private readonly bool? _inlineTotalCount;
9+
10+
public QueryableCursorPagingHandler(PagingOptions options) : this(options, false)
11+
{
12+
}
13+
14+
public QueryableCursorPagingHandler(PagingOptions options, bool? inlineTotalCount) : base(options)
15+
{
16+
_inlineTotalCount = inlineTotalCount;
17+
}
18+
919
private static readonly QueryableCursorPaginationAlgorithm<TEntity> _paginationAlgorithm =
1020
QueryableCursorPaginationAlgorithm<TEntity>.Instance;
1121

@@ -18,7 +28,7 @@ public ValueTask<Connection<TEntity>> SliceAsync(
1828
source.Source,
1929
arguments,
2030
_paginationAlgorithm,
21-
new QueryExecutor(source),
31+
new QueryExecutor(source, _inlineTotalCount),
2232
context.RequestAborted);
2333

2434
protected override ValueTask<Connection> SliceAsync(
@@ -44,11 +54,11 @@ private async ValueTask<Connection> SliceAsyncInternal(
4454
source.Source,
4555
arguments,
4656
_paginationAlgorithm,
47-
new QueryExecutor(source),
57+
new QueryExecutor(source, _inlineTotalCount),
4858
context.RequestAborted)
4959
.ConfigureAwait(false);
5060

51-
private sealed class QueryExecutor(IQueryableExecutable<TEntity> executable)
61+
private sealed class QueryExecutor(IQueryableExecutable<TEntity> executable, bool? allowInlining)
5262
: ICursorPaginationQueryExecutor<IQueryable<TEntity>, TEntity>
5363
{
5464
public ValueTask<int> CountAsync(
@@ -68,17 +78,33 @@ public async ValueTask<CursorPaginationData<TEntity>> QueryAsync(
6878

6979
if (includeTotalCount)
7080
{
71-
var combinedQuery = slicedQuery.Select(t => new { TotalCount = originalQuery.Count(), Item = t });
72-
totalCount = 0;
81+
if (allowInlining ?? false)
82+
{
83+
var combinedQuery = slicedQuery.Select(t => new { TotalCount = originalQuery.Count(), Item = t });
84+
totalCount = 0;
7385

74-
var index = offset;
75-
await foreach (var item in executable
76-
.WithSource(combinedQuery)
77-
.ToAsyncEnumerable(cancellationToken)
78-
.ConfigureAwait(false))
86+
var index = offset;
87+
await foreach (var item in executable
88+
.WithSource(combinedQuery)
89+
.ToAsyncEnumerable(cancellationToken)
90+
.ConfigureAwait(false))
91+
{
92+
edges.Add(IndexEdge<TEntity>.Create(item.Item, index++));
93+
totalCount = item.TotalCount;
94+
}
95+
}
96+
else
7997
{
80-
edges.Add(IndexEdge<TEntity>.Create(item.Item, index++));
81-
totalCount = item.TotalCount;
98+
var index = offset;
99+
await foreach (var item in executable
100+
.WithSource(slicedQuery)
101+
.ToAsyncEnumerable(cancellationToken)
102+
.ConfigureAwait(false))
103+
{
104+
edges.Add(IndexEdge<TEntity>.Create(item, index++));
105+
}
106+
107+
totalCount = await executable.CountAsync(cancellationToken).ConfigureAwait(false);
82108
}
83109
}
84110
else

src/HotChocolate/Core/src/Types.CursorPagination/QueryableCursorPagingProvider.cs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Collections.Concurrent;
12
using System.Reflection;
23
using HotChocolate.Internal;
34

@@ -8,11 +9,21 @@ namespace HotChocolate.Types.Pagination;
89
/// </summary>
910
public class QueryableCursorPagingProvider : CursorPagingProvider
1011
{
12+
private static readonly ConcurrentDictionary<Type, MethodInfo> _factoryCache = new();
13+
private readonly bool? _inlineTotalCount;
14+
1115
private static readonly MethodInfo _createHandler =
1216
typeof(QueryableCursorPagingProvider).GetMethod(
1317
nameof(CreateHandlerInternal),
1418
BindingFlags.Static | BindingFlags.NonPublic)!;
1519

20+
public QueryableCursorPagingProvider() { }
21+
22+
public QueryableCursorPagingProvider(bool? inlineTotalCount)
23+
{
24+
_inlineTotalCount = inlineTotalCount;
25+
}
26+
1627
/// <inheritdoc />
1728
public override bool CanHandle(IExtendedType source)
1829
{
@@ -34,12 +45,12 @@ protected override CursorPagingHandler CreateHandler(
3445
throw new ArgumentNullException(nameof(source));
3546
}
3647

37-
return (CursorPagingHandler)_createHandler
38-
.MakeGenericMethod(source.ElementType?.Source ?? source.Source)
39-
.Invoke(null, [options,])!;
48+
var key = source.ElementType?.Source ?? source.Source;
49+
var factory = _factoryCache.GetOrAdd(key, static type => _createHandler.MakeGenericMethod(type));
50+
return (CursorPagingHandler)factory.Invoke(null, [options, _inlineTotalCount])!;
4051
}
4152

4253
private static QueryableCursorPagingHandler<TEntity> CreateHandlerInternal<TEntity>(
43-
PagingOptions options) =>
44-
new(options);
54+
PagingOptions options, bool? inlineTotalCount)
55+
=> new(options, inlineTotalCount);
4556
}

src/HotChocolate/Data/test/Data.EntityFramework.Tests/SqlLiteCursorTestBase.cs

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@ private DatabaseContext<TResult> BuildContext<TResult>(
3232
protected IRequestExecutor CreateSchema<TEntity>(TEntity[] entities)
3333
where TEntity : class
3434
{
35-
var builder = SchemaBuilder.New()
35+
return new ServiceCollection()
36+
.AddDbContextPool<DatabaseContext<TEntity>>(
37+
b => b.UseSqlite($"Data Source={Guid.NewGuid():N}.db"))
38+
.AddGraphQL()
3639
.AddQueryType(
3740
c =>
3841
{
@@ -64,10 +67,7 @@ protected IRequestExecutor CreateSchema<TEntity>(TEntity[] entities)
6467
}
6568
})
6669
.UsePaging<ObjectType<TEntity>>(
67-
options: new()
68-
{
69-
IncludeTotalCount = true,
70-
});
70+
options: new() { IncludeTotalCount = true, });
7171

7272
c.Field("root1")
7373
.Resolve(
@@ -94,17 +94,8 @@ protected IRequestExecutor CreateSchema<TEntity>(TEntity[] entities)
9494
}
9595
}
9696
});
97-
});
98-
99-
var schema = builder.Create();
100-
101-
return new ServiceCollection()
102-
.Configure<RequestExecutorSetup>(
103-
Schema.DefaultName,
104-
o => o.Schema = schema)
105-
.AddDbContextPool<DatabaseContext<TEntity>>(
106-
b => b.UseSqlite($"Data Source={Guid.NewGuid():N}.db"))
107-
.AddGraphQL()
97+
})
98+
.AddQueryableCursorPagingProvider(inlineTotalCount: true)
10899
.UseDefaultPipeline()
109100
.Services
110101
.BuildServiceProvider()

0 commit comments

Comments
 (0)