Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ public static BatchExpression<TK, TV> BuildBatchExpression<TK, TV>(
var group = Expression.Parameter(typeof(IGrouping<TK, TV>), "g");
var groupKey = Expression.Property(group, "Key");
Expression source = group;
var applySelectorAfterPaging = arguments.After is not null || arguments.Before is not null;

for (var i = 0; i < orderExpressions.Length; i++)
{
Expand All @@ -181,8 +182,8 @@ public static BatchExpression<TK, TV> BuildBatchExpression<TK, TV>(
typedOrderExpression);
}

// apply the selector to each item in the grouping after ordering
if (selector is not null)
// keep the historical query shape unless cursor filtering is active.
if (!applySelectorAfterPaging && selector is not null)
{
var selectMethod = typeof(Enumerable)
.GetMethods(BindingFlags.Static | BindingFlags.Public)
Expand Down Expand Up @@ -276,6 +277,18 @@ public static BatchExpression<TK, TV> BuildBatchExpression<TK, TV>(
Expression.Constant(arguments.Last.Value + 1));
}

// apply the selector after cursor filtering and paging so cursor predicates
// run against the unprojected source when the selector shape is not SQL-translatable.
if (applySelectorAfterPaging && selector is not null)
{
var selectMethod = typeof(Enumerable)
.GetMethods(BindingFlags.Static | BindingFlags.Public)
.First(m => m.Name == nameof(Enumerable.Select) && m.GetParameters().Length == 2)
.MakeGenericMethod(typeof(TV), typeof(TV));

source = Expression.Call(selectMethod, source, selector);
}

source = Expression.Call(
typeof(Enumerable),
"ToList",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,18 @@ public static async ValueTask<Page<T>> ToPageAsync<T>(
ArgumentNullException.ThrowIfNull(source);

source = QueryHelpers.EnsureOrderPropsAreSelected(source);
Expression<Func<T, T>>? selector = null;
var applySelectorAfterPaging = arguments.After is not null || arguments.Before is not null;

if (applySelectorAfterPaging)
{
selector = QueryHelpers.ExtractCurrentSelector(source);

if (selector is not null)
{
source = QueryHelpers.RemoveSelector(source);
}
}

var keys = ParseDataSetKeys(source);

Expand Down Expand Up @@ -181,13 +193,16 @@ public static async ValueTask<Page<T>> ToPageAsync<T>(
}

source = source.Take(requestedCount + 1);
var pageQuery = selector is null
? source
: source.Select(selector);

var builder = ImmutableArray.CreateBuilder<T>();
var fetchCount = 0;

if (includeTotalCount)
{
var combinedQuery = source.Select(t => new { TotalCount = originalQuery.Count(), Item = t });
var combinedQuery = pageQuery.Select(t => new { TotalCount = originalQuery.Count(), Item = t });

TryGetQueryInterceptor()?.OnBeforeExecute(combinedQuery);

Expand All @@ -207,9 +222,9 @@ public static async ValueTask<Page<T>> ToPageAsync<T>(
}
else
{
TryGetQueryInterceptor()?.OnBeforeExecute(source);
TryGetQueryInterceptor()?.OnBeforeExecute(pageQuery);

await foreach (var item in source.AsAsyncEnumerable()
await foreach (var item in pageQuery.AsAsyncEnumerable()
.WithCancellation(cancellationToken).ConfigureAwait(false))
{
fetchCount++;
Expand Down Expand Up @@ -412,6 +427,19 @@ public static async ValueTask<Dictionary<TKey, Page<TValue>>> ToBatchPageAsync<T
CancellationToken cancellationToken = default)
where TKey : notnull
{
source = QueryHelpers.EnsureOrderPropsAreSelected(source);

// extract the selector before ensuring group props are selected,
// as we need to remove it before grouping and re-apply it after
var selector = QueryHelpers.ExtractCurrentSelector(source);

// if we have a selector, remove it before grouping
// we'll re-apply it to the grouped items later
if (selector is not null)
{
source = QueryHelpers.RemoveSelector(source);
}

var keys = ParseDataSetKeys(source);

if (keys.Length == 0)
Expand All @@ -435,19 +463,6 @@ public static async ValueTask<Dictionary<TKey, Page<TValue>>> ToBatchPageAsync<T
includeTotalCount = true;
}

source = QueryHelpers.EnsureOrderPropsAreSelected(source);

// extract the selector before ensuring group props are selected,
// as we need to remove it before grouping and re-apply it after
var selector = QueryHelpers.ExtractCurrentSelector(source);

// if we have a selector, remove it before grouping
// we'll re-apply it to the grouped items later
if (selector is not null)
{
source = QueryHelpers.RemoveSelector(source);
}

source = QueryHelpers.EnsureGroupPropsAreSelected(source, keySelector);

// we need to move the ordering into the select expression we are constructing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,78 @@ public async Task BatchPaging_With_TPC_Selector_And_Scalar_Property()
Assert.Equal(2, result.Count);
}

[Fact]
public async Task Paging_With_TPH_Selector_After_Cursor()
{
// arrange
var connectionString = CreateConnectionString();
await SeedAnimalsAsync(connectionString);

await using var context = new AnimalContext(connectionString);

var query = new QueryContext<Animal>(
Selector: e =>
e is Dog
? new Dog { Id = ((Dog)e).Id, Name = ((Dog)e).Name }
: e is Cat
? (Animal)new Cat { Id = ((Cat)e).Id, Name = ((Cat)e).Name }
: null!);

var arguments = new PagingArguments(2);

// act
var firstPage = await context.Pets
.With(query, sort => sort.AddDescending(e => e.Name))
.ToPageAsync(arguments);

var secondPage = await context.Pets
.With(query, sort => sort.AddDescending(e => e.Name))
.ToPageAsync(arguments with { After = firstPage.CreateCursor(firstPage.Last!) });

// assert
Assert.NotNull(secondPage);
Assert.Equal(2, secondPage.Items.Length);
}

[Fact]
public async Task BatchPaging_With_TPH_Selector_After_Cursor()
{
// arrange
var connectionString = CreateConnectionString();
await SeedAnimalsAsync(connectionString);

await using var context = new AnimalContext(connectionString);

var query = new QueryContext<Animal>(
Selector: e =>
e is Dog
? new Dog { Id = ((Dog)e).Id, Name = ((Dog)e).Name }
: e is Cat
? (Animal)new Cat { Id = ((Cat)e).Id, Name = ((Cat)e).Name }
: null!);

var arguments = new PagingArguments(2);

// act
var firstMap = await context.Pets
.With(query, sort => sort.AddDescending(e => e.Name))
.ToBatchPageAsync(e => e.OwnerId, arguments);

var firstPage = Assert.Single(firstMap).Value;

var secondMap = await context.Pets
.With(query, sort => sort.AddDescending(e => e.Name))
.ToBatchPageAsync(
e => e.OwnerId,
arguments with { After = firstPage.CreateCursor(firstPage.Last!) });

var secondPage = Assert.Single(secondMap).Value;

// assert
Assert.NotNull(secondPage);
Assert.Equal(2, secondPage.Items.Length);
}

private static async Task SeedFileSystemAsync(string connectionString)
{
await using var context = new FileSystemContext(connectionString);
Expand Down Expand Up @@ -102,4 +174,22 @@ private static async Task SeedFileSystemAsync(string connectionString)

await context.SaveChangesAsync();
}

private static async Task SeedAnimalsAsync(string connectionString)
{
await using var context = new AnimalContext(connectionString);
await context.Database.EnsureCreatedAsync();

var owner = new Owner { Id = 1, Name = "owner-1" };

context.Owners.Add(owner);
context.Pets.AddRange(
new Dog { Id = 1, Name = "zeta", OwnerId = owner.Id, IsBarking = true },
new Cat { Id = 2, Name = "epsilon", OwnerId = owner.Id, IsPurring = true },
new Dog { Id = 3, Name = "delta", OwnerId = owner.Id, IsBarking = false },
new Cat { Id = 4, Name = "gamma", OwnerId = owner.Id, IsPurring = false },
new Dog { Id = 5, Name = "beta", OwnerId = owner.Id, IsBarking = true });

await context.SaveChangesAsync();
}
}
Loading