Skip to content

Commit

Permalink
Moved paging primitives into separate library. (#7091)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelstaib authored May 6, 2024
1 parent 104e1da commit 4e3703f
Show file tree
Hide file tree
Showing 12 changed files with 92 additions and 53 deletions.
15 changes: 15 additions & 0 deletions src/HotChocolate/Core/HotChocolate.Core.sln
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Features", "sr
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Features.Tests", "test\Features.Tests\HotChocolate.Features.Tests.csproj", "{EA77D317-8767-4DDE-8038-820D582C52D6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Pagination.Core", "src\Pagination.Core\HotChocolate.Pagination.Core.csproj", "{4E7D749A-7172-411D-977C-E88500321FB0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -999,6 +1001,18 @@ Global
{EA77D317-8767-4DDE-8038-820D582C52D6}.Release|x64.Build.0 = Release|Any CPU
{EA77D317-8767-4DDE-8038-820D582C52D6}.Release|x86.ActiveCfg = Release|Any CPU
{EA77D317-8767-4DDE-8038-820D582C52D6}.Release|x86.Build.0 = Release|Any CPU
{4E7D749A-7172-411D-977C-E88500321FB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4E7D749A-7172-411D-977C-E88500321FB0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4E7D749A-7172-411D-977C-E88500321FB0}.Debug|x64.ActiveCfg = Debug|Any CPU
{4E7D749A-7172-411D-977C-E88500321FB0}.Debug|x64.Build.0 = Debug|Any CPU
{4E7D749A-7172-411D-977C-E88500321FB0}.Debug|x86.ActiveCfg = Debug|Any CPU
{4E7D749A-7172-411D-977C-E88500321FB0}.Debug|x86.Build.0 = Debug|Any CPU
{4E7D749A-7172-411D-977C-E88500321FB0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4E7D749A-7172-411D-977C-E88500321FB0}.Release|Any CPU.Build.0 = Release|Any CPU
{4E7D749A-7172-411D-977C-E88500321FB0}.Release|x64.ActiveCfg = Release|Any CPU
{4E7D749A-7172-411D-977C-E88500321FB0}.Release|x64.Build.0 = Release|Any CPU
{4E7D749A-7172-411D-977C-E88500321FB0}.Release|x86.ActiveCfg = Release|Any CPU
{4E7D749A-7172-411D-977C-E88500321FB0}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -1074,6 +1088,7 @@ Global
{AE9AF1C7-578A-46A5-84FD-9BBA8EB8DE22} = {7462D089-D350-44D6-8131-896D949A65B7}
{669FA147-3B41-4841-921A-55B019C3AF26} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2}
{EA77D317-8767-4DDE-8038-820D582C52D6} = {7462D089-D350-44D6-8131-896D949A65B7}
{4E7D749A-7172-411D-977C-E88500321FB0} = {37B9D3B1-CA34-4720-9A0B-CFF1E64F52C2}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E4D94C77-6657-4630-9D42-0A9AC5153A1B}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<PackageId>HotChocolate.Pagination.Core</PackageId>
<AssemblyName>HotChocolate.Pagination.Core</AssemblyName>
<RootNamespace>HotChocolate.Pagination</RootNamespace>
<Description>Contains primitives for implementing paging.</Description>

<TargetFrameworks>$(Library2TargetFrameworks)</TargetFrameworks>

<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Collections;

namespace HotChocolate.Data;
namespace HotChocolate.Pagination;

/// <summary>
/// Represents a page of a result set.
Expand All @@ -24,34 +24,34 @@ public readonly struct Page<T>(
IReadOnlyList<T> items,
bool hasNextPage,
bool hasPreviousPage,
Func<T, string> createCursor)
Func<T, string> createCursor)
: IEnumerable<T>
{
/// <summary>
/// Gets the items of this page.
/// </summary>
public IReadOnlyList<T> Items => items;

/// <summary>
/// Gets the first item of this page.
/// </summary>
public T? First => items.Count > 0 ? items[0] : default;

/// <summary>
/// Gets the last item of this page.
/// </summary>
public T? Last => items.Count > 0 ? items[^1] : default;

/// <summary>
/// Defines if there is a next page.
/// </summary>
public bool HasNextPage => hasNextPage;

/// <summary>
/// Defines if there is a previous page.
/// </summary>
public bool HasPreviousPage => hasPreviousPage;

/// <summary>
/// Creates a cursor for an item of this page.
/// </summary>
Expand All @@ -62,7 +62,7 @@ public readonly struct Page<T>(
/// Returns a cursor for the item.
/// </returns>
public string CreateCursor(T item) => createCursor(item);

/// <summary>
/// An empty page.
/// </summary>
Expand All @@ -74,7 +74,7 @@ public readonly struct Page<T>(
/// <returns></returns>
public IEnumerator<T> GetEnumerator()
=> items.GetEnumerator();

IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace HotChocolate.Data;
namespace HotChocolate.Pagination;

/// <summary>
/// The paging key can be used when offloading paging logic to a DataLoader.
Expand All @@ -12,4 +12,4 @@ namespace HotChocolate.Data;
/// <typeparam name="T">
/// The type of the parent key.
/// </typeparam>
public readonly record struct PageKey<T>(T Key, PagingArguments PagingArgs);
public readonly record struct PageKey<T>(T Key, PagingArguments PagingArgs);
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace HotChocolate.Data;
namespace HotChocolate.Pagination;

/// <summary>
/// The paging arguments are used to specify the paging behavior.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
<Using Include="Microsoft.EntityFrameworkCore" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\Core\src\Pagination.Core\HotChocolate.Pagination.Core.csproj" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net6.0'">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.0" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using System.Reflection;
using HotChocolate.Pagination;

namespace HotChocolate.Data;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.EntityFrameworkCore.Query;
using HotChocolate.Pagination;

namespace HotChocolate.Data;

Expand All @@ -13,7 +14,7 @@ public static class PagingQueryableExtensions
private static readonly MethodInfo _createAndConvert = typeof(PagingQueryableExtensions)
.GetMethod(nameof(CreateAndConvertParameter), BindingFlags.NonPublic | BindingFlags.Static)!;
private static readonly ConcurrentDictionary<Type, Func<object?, Expression>> _cachedConverters = new();

/// <summary>
/// Executes a query with paging and returns the selected page.
/// </summary>
Expand All @@ -37,27 +38,27 @@ public static class PagingQueryableExtensions
/// </exception>
public static async ValueTask<Page<T>> ToPageAsync<T>(
this IQueryable<T> source,
PagingArguments arguments,
PagingArguments arguments,
CancellationToken cancellationToken = default)
{
var keys = ParseDataSetKeys(source);

if(keys.Length == 0)
{
throw new ArgumentException(
"In order to use cursor pagination, you must specify at least on key using the `OrderBy` method.",
nameof(source));
}

if(arguments.Last is not null && arguments.First is not null)
{
throw new ArgumentException(
"You can specify either `first` or `last`, but not both as this can lead to unpredictable results.",
nameof(arguments));
}

var forward = arguments.Last is null;

if (arguments.After is not null)
{
var cursor = CursorParser.Parse(arguments.After, keys);
Expand All @@ -69,7 +70,7 @@ public static async ValueTask<Page<T>> ToPageAsync<T>(
var cursor = CursorParser.Parse(arguments.Before, keys);
source = source.Where(BuildWhereExpression<T>(keys, cursor, forward));
}

if (arguments.First is not null)
{
source = source.Take(arguments.First.Value);
Expand All @@ -81,7 +82,7 @@ public static async ValueTask<Page<T>> ToPageAsync<T>(
}

var result = await source.ToListAsync(cancellationToken);

if(result.Count == 0)
{
return Page<T>.Empty;
Expand All @@ -91,7 +92,7 @@ public static async ValueTask<Page<T>> ToPageAsync<T>(
{
result.Reverse();
}

return CreatePage(result, arguments, keys);
}

Expand Down Expand Up @@ -129,7 +130,7 @@ public static async ValueTask<Dictionary<PageKey<TKey>, Page<TProperty>>> ToBatc
this IIncludableQueryable<T, IOrderedEnumerable<TProperty>> source,
Func<T, TKey> keySelector,
PagingArguments arguments,
CancellationToken cancellationToken = default)
CancellationToken cancellationToken = default)
where TKey : notnull
{
var rewriter = new BatchQueryRewriter<TProperty>(arguments);
Expand All @@ -146,11 +147,11 @@ public static async ValueTask<Dictionary<PageKey<TKey>, Page<TProperty>>> ToBatc
case IReadOnlyList<TProperty> resultList:
result.Add(key, CreatePage(resultList, arguments, rewriter.Keys));
break;

case IEnumerable<TProperty> resultEnumerable:
result.Add(key, CreatePage(resultEnumerable.ToArray(), arguments, rewriter.Keys));
break;

default:
throw new InvalidOperationException(
"The result must be a list or an enumerable.");
Expand All @@ -162,11 +163,11 @@ public static async ValueTask<Dictionary<PageKey<TKey>, Page<TProperty>>> ToBatc

private static Page<T> CreatePage<T>(IReadOnlyList<T> items, PagingArguments arguments, DataSetKey[] keys)
{
var hasPrevious = arguments.First is not null && items.Count > 0 ||
var hasPrevious = arguments.First is not null && items.Count > 0 ||
arguments.Last is not null && items.Count > arguments.Last;
var hasNext = arguments.First is not null && items.Count > arguments.First ||
var hasNext = arguments.First is not null && items.Count > arguments.First ||
arguments.Last is not null && items.Count > 0;

return new Page<T>(items, hasNext, hasPrevious, item => CursorFormatter.Format(item, keys));
}

Expand All @@ -176,7 +177,7 @@ private static DataSetKey[] ParseDataSetKeys<T>(IQueryable<T> source)
parser.Visit(source.Expression);
return parser.Keys.ToArray();
}

internal static Expression<Func<T, bool>> BuildWhereExpression<T>(
DataSetKey[] keys,
object[] cursor,
Expand Down Expand Up @@ -223,7 +224,7 @@ internal static Expression<Func<T, bool>> BuildWhereExpression<T>(
for (var j = 0; j < handled.Count; j++)
{
var handledKey = handled[j];

keyExpr =
Expression.Equal(
Expression.Call(
Expand Down Expand Up @@ -280,10 +281,10 @@ private static Expression CreateParameter(object? value, Type type)

return converter(value);
}

private static Expression CreateAndConvertParameter<T>(object value)
{
Expression<Func<T>> lambda = () => (T)value;
return lambda.Body;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using HotChocolate;
using HotChocolate.Data;
using HotChocolate.Execution.Configuration;
using HotChocolate.Types.Pagination;
using HotChocolate.Pagination;

namespace Microsoft.Extensions.DependencyInjection;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using HotChocolate.Types.Pagination;
using HotChocolate.Pagination;

namespace HotChocolate.Data;

Expand All @@ -24,7 +25,7 @@ public static async Task<Connection<T>> ToConnectionAsync<T>(
var result = await resultPromise;
return CreateConnection(result);
}

/// <summary>
/// Converts a <see cref="Page{T}"/> to a <see cref="Connection{T}"/>.
/// </summary>
Expand Down Expand Up @@ -56,4 +57,4 @@ private static Connection<T> CreateConnection<T>(Page<T> page) where T : class

private static string? CreateCursor<T>(T? item, Func<T, string> createCursor) where T : class
=> item is null ? null : createCursor(item);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using HotChocolate.Execution;
using HotChocolate.Types;
using HotChocolate.Types.Pagination;
using HotChocolate.Pagination;
using Microsoft.Extensions.DependencyInjection;
using Squadron;

Expand All @@ -21,7 +22,7 @@ public async Task GetDefaultPage()
// Arrange
var connectionString = CreateConnectionString();
await SeedAsync(connectionString);

// Act
var result = await new ServiceCollection()
.AddScoped(_ => new CatalogContext(connectionString))
Expand Down Expand Up @@ -49,14 +50,14 @@ public async Task GetDefaultPage()
// Assert
result.MatchMarkdownSnapshot();
}

[Fact]
public async Task GetSecondPage_With_2_Items()
{
// Arrange
var connectionString = CreateConnectionString();
await SeedAsync(connectionString);

// Act
var result = await new ServiceCollection()
.AddScoped(_ => new CatalogContext(connectionString))
Expand Down Expand Up @@ -84,7 +85,7 @@ public async Task GetSecondPage_With_2_Items()
// Assert
result.MatchMarkdownSnapshot();
}

private static async Task SeedAsync(string connectionString)
{
await using var context = new CatalogContext(connectionString);
Expand Down Expand Up @@ -126,4 +127,4 @@ public async Task<Connection<Brand>> GetBrandsAsync(
.ToPageAsync(arguments, cancellationToken: ct)
.ToConnectionAsync();
}
}
}
Loading

0 comments on commit 4e3703f

Please sign in to comment.