Skip to content

Commit

Permalink
Add support for prebuilt keyset query definitions
Browse files Browse the repository at this point in the history
  • Loading branch information
mrahhal committed May 13, 2023
1 parent 8ce4de7 commit 5a49207
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 8 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## Unreleased

### Added

- Add support for prebuilt keyset query definitions

### Other

- Update MR.EntityFrameworkCore.KeysetPagination package version to v1.3.0

[**Full diff**](https://github.com/mrahhal/MR.EntityFrameworkCore.KeysetPagination/compare/v2.0.1...HEAD)

## 2.0.1 - 2023-02-10

### Fixed
Expand Down
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,30 @@ Do a keyset pagination:
```cs
var usersPaginationResult = await _paginationService.KeysetPaginateAsync(
_dbContext.Users,
b => b.Descending(x => x.Created),
b => b.Descending(x => x.Created).Descending(x => x.Id),
async id => await _dbContext.Users.FindAsync(int.Parse(id)));
```

`id` above will always be a string, so make sure to parse it to your entity's id type.

**Note:** Check [MR.EntityFrameworkCore.KeysetPagination](https://github.com/mrahhal/MR.EntityFrameworkCore.KeysetPagination) for more info about keyset pagination.
Also note that the keyset should be

Check the readme for [MR.EntityFrameworkCore.KeysetPagination](https://github.com/mrahhal/MR.EntityFrameworkCore.KeysetPagination) for more info about keyset pagination.

Prebuilt keyset query definitions from [MR.EntityFrameworkCore.KeysetPagination](https://github.com/mrahhal/MR.EntityFrameworkCore.KeysetPagination) are also supported:

```cs
// In the ctor or someplace similar, set this to a static field for example.
_usersKeysetQuery = KeysetQuery.Build<User>(b => b.Descending(x => x.Created).Descending(x => x.Id));

// Then when calling KeysetPaginateAsync, we use the prebuilt definition.
var usersPaginationResult = await _paginationService.KeysetPaginateAsync(
_dbContext.Users,
_usersKeysetQuery,
async id => await _dbContext.Users.FindAsync(int.Parse(id)));
```

Prebuilt keyset query definitions are the recommended way, but the examples here just build the keyset in `KeysetPaginateAsync` for brevity.

Do a keyset pagination and map to dto:

Expand Down
6 changes: 5 additions & 1 deletion samples/Basic/Pages/Index.cshtml.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
using Basic.Models;
using Microsoft.AspNetCore.Mvc.RazorPages;
using MR.AspNetCore.Pagination;
using MR.EntityFrameworkCore.KeysetPagination;

namespace Basic.Pages
{
public class IndexModel : PageModel
{
private static readonly KeysetQueryDefinition<User> _usersKeysetQuery =
KeysetQuery.Build<User>(b => b.Descending(x => x.Created));

private readonly AppDbContext _dbContext;
private readonly IPaginationService _paginationService;

Expand All @@ -26,7 +30,7 @@ public async Task OnGet()

Users = await _paginationService.KeysetPaginateAsync(
query,
b => b.Descending(x => x.Created),
_usersKeysetQuery,
async id => await _dbContext.Users.FindAsync(int.Parse(id)));
}
}
Expand Down
146 changes: 141 additions & 5 deletions src/MR.AspNetCore.Pagination/PaginationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,26 @@ namespace MR.AspNetCore.Pagination;
/// </summary>
public interface IPaginationService
{
/// <summary>
/// Paginates data using keyset pagination.
/// </summary>
/// <typeparam name="T">The type of the entity.</typeparam>
/// <typeparam name="TOut">The type of the transformed object.</typeparam>
/// <param name="source">The queryable source.</param>
/// <param name="keysetQueryDefinition">The prebuilt keyset query definition.</param>
/// <param name="getReferenceAsync">A func to load the reference from its id.</param>
/// <param name="map">A map func to convert <typeparamref name="T"/> to <typeparamref name="TOut"/>.</param>
/// <param name="queryModel">The pagination query model.</param>
/// <returns>The keyset pagination result.</returns>
Task<KeysetPaginationResult<TOut>> KeysetPaginateAsync<T, TOut>(
IQueryable<T> source,
KeysetQueryDefinition<T> keysetQueryDefinition,
Func<string, Task<T?>> getReferenceAsync,
Func<IQueryable<T>, IQueryable<TOut>> map,
KeysetQueryModel queryModel)
where T : class
where TOut : class;

/// <summary>
/// Paginates data using keyset pagination.
/// </summary>
Expand All @@ -30,6 +50,26 @@ Task<KeysetPaginationResult<TOut>> KeysetPaginateAsync<T, TOut>(
where T : class
where TOut : class;

/// <summary>
/// Paginates data using keyset pagination with a model parsed from the request's query.
/// </summary>
/// <typeparam name="T">The type of the entity.</typeparam>
/// <typeparam name="TOut">The type of the transformed object.</typeparam>
/// <param name="source">The queryable source.</param>
/// <param name="keysetQueryDefinition">The prebuilt keyset query definition.</param>
/// <param name="getReferenceAsync">A func to load the reference from its id.</param>
/// <param name="map">A map func to convert <typeparamref name="T"/> to <typeparamref name="TOut"/>.</param>
/// <param name="pageSize">The page size. This takes priority over all other sources.</param>
/// <returns>The keyset pagination result.</returns>
Task<KeysetPaginationResult<TOut>> KeysetPaginateAsync<T, TOut>(
IQueryable<T> source,
KeysetQueryDefinition<T> keysetQueryDefinition,
Func<string, Task<T?>> getReferenceAsync,
Func<IQueryable<T>, IQueryable<TOut>> map,
int? pageSize = null)
where T : class
where TOut : class;

/// <summary>
/// Paginates data using keyset pagination with a model parsed from the request's query.
/// </summary>
Expand Down Expand Up @@ -120,6 +160,25 @@ OffsetPaginationResult<TOut> OffsetPaginate<T, TOut>(
/// </summary>
public static class PaginationServiceExtensions
{
/// <summary>
/// Paginates data using keyset pagination.
/// </summary>
/// <typeparam name="T">The type of the entity.</typeparam>
/// <param name="this">The <see cref="IPaginationService"/> instance.</param>
/// <param name="source">The queryable source.</param>
/// <param name="keysetQueryDefinition">The prebuilt keyset query definition.</param>
/// <param name="getReferenceAsync">A func to load the reference from its id.</param>
/// <param name="queryModel">The pagination query model.</param>
/// <returns>The keyset pagination result.</returns>
public static Task<KeysetPaginationResult<T>> KeysetPaginateAsync<T>(
this IPaginationService @this,
IQueryable<T> source,
KeysetQueryDefinition<T> keysetQueryDefinition,
Func<string, Task<T?>> getReferenceAsync,
KeysetQueryModel queryModel)
where T : class
=> @this.KeysetPaginateAsync(source, keysetQueryDefinition, getReferenceAsync, query => query, queryModel);

/// <summary>
/// Paginates data using keyset pagination.
/// </summary>
Expand All @@ -139,6 +198,25 @@ public static Task<KeysetPaginationResult<T>> KeysetPaginateAsync<T>(
where T : class
=> @this.KeysetPaginateAsync(source, builderAction, getReferenceAsync, query => query, queryModel);

/// <summary>
/// Paginates data using keyset pagination with a model parsed from the request's query.
/// </summary>
/// <typeparam name="T">The type of the entity.</typeparam>
/// <param name="this">The <see cref="IPaginationService"/> instance.</param>
/// <param name="source">The queryable source.</param>
/// <param name="keysetQueryDefinition">The prebuilt keyset query definition.</param>
/// <param name="getReferenceAsync">A func to load the reference from its id.</param>
/// <param name="pageSize">The page size. This takes priority over all other sources.</param>
/// <returns>The keyset pagination result.</returns>
public static Task<KeysetPaginationResult<T>> KeysetPaginateAsync<T>(
this IPaginationService @this,
IQueryable<T> source,
KeysetQueryDefinition<T> keysetQueryDefinition,
Func<string, Task<T?>> getReferenceAsync,
int? pageSize = null)
where T : class
=> @this.KeysetPaginateAsync(source, keysetQueryDefinition, getReferenceAsync, query => query, pageSize);

/// <summary>
/// Paginates data using keyset pagination with a model parsed from the request's query.
/// </summary>
Expand Down Expand Up @@ -239,7 +317,31 @@ public PaginationService(
}

/// <inheritdoc/>
public async Task<KeysetPaginationResult<TOut>> KeysetPaginateAsync<T, TOut>(
public Task<KeysetPaginationResult<TOut>> KeysetPaginateAsync<T, TOut>(
IQueryable<T> source,
KeysetQueryDefinition<T> keysetQueryDefinition,
Func<string, Task<T?>> getReferenceAsync,
Func<IQueryable<T>, IQueryable<TOut>> map,
KeysetQueryModel queryModel)
where T : class
where TOut : class
{
if (source == null) throw new ArgumentNullException(nameof(source));
if (keysetQueryDefinition == null) throw new ArgumentNullException(nameof(keysetQueryDefinition));
if (getReferenceAsync == null) throw new ArgumentNullException(nameof(getReferenceAsync));
if (map == null) throw new ArgumentNullException(nameof(map));
if (queryModel == null) throw new ArgumentNullException(nameof(queryModel));

return KeysetPaginateAsync(
source,
(query, direction, reference) => query.KeysetPaginate(keysetQueryDefinition, direction, reference),
getReferenceAsync,
map,
queryModel);
}

/// <inheritdoc/>
public Task<KeysetPaginationResult<TOut>> KeysetPaginateAsync<T, TOut>(
IQueryable<T> source,
Action<KeysetPaginationBuilder<T>> builderAction,
Func<string, Task<T?>> getReferenceAsync,
Expand All @@ -252,7 +354,25 @@ public async Task<KeysetPaginationResult<TOut>> KeysetPaginateAsync<T, TOut>(
if (builderAction == null) throw new ArgumentNullException(nameof(builderAction));
if (getReferenceAsync == null) throw new ArgumentNullException(nameof(getReferenceAsync));
if (map == null) throw new ArgumentNullException(nameof(map));
if (queryModel == null) throw new ArgumentNullException(nameof(queryModel));

return KeysetPaginateAsync(
source,
(query, direction, reference) => query.KeysetPaginate(builderAction, direction, reference),
getReferenceAsync,
map,
queryModel);
}

private async Task<KeysetPaginationResult<TOut>> KeysetPaginateAsync<T, TOut>(
IQueryable<T> source,
Func<IQueryable<T>, KeysetPaginationDirection, object?, KeysetPaginationContext<T>> callKeysetPaginate,
Func<string, Task<T?>> getReferenceAsync,
Func<IQueryable<T>, IQueryable<TOut>> map,
KeysetQueryModel queryModel)
where T : class
where TOut : class
{
var query = source;
var pageSize = queryModel.Size ?? _options.DefaultSize;

Expand All @@ -263,12 +383,12 @@ public async Task<KeysetPaginationResult<TOut>> KeysetPaginateAsync<T, TOut>(

if (queryModel.Last)
{
keysetContext = query.KeysetPaginate(builderAction, KeysetPaginationDirection.Backward);
keysetContext = callKeysetPaginate(query, KeysetPaginationDirection.Backward, null);
}
else if (queryModel.After != null)
{
var reference = await getReferenceAsync(queryModel.After);
keysetContext = query.KeysetPaginate(builderAction, KeysetPaginationDirection.Forward, reference);
keysetContext = callKeysetPaginate(query, KeysetPaginationDirection.Forward, reference);
}
else if (queryModel.Before != null)
{
Expand All @@ -277,12 +397,12 @@ public async Task<KeysetPaginationResult<TOut>> KeysetPaginateAsync<T, TOut>(
// If reference is null (maybe the entity was deleted from the database), we want to
// always return the first page, so enforce a Forward direction.
var direction = reference != null ? KeysetPaginationDirection.Backward : KeysetPaginationDirection.Forward;
keysetContext = query.KeysetPaginate(builderAction, direction, reference);
keysetContext = callKeysetPaginate(query, direction, reference);
}
else
{
// First page
keysetContext = query.KeysetPaginate(builderAction, KeysetPaginationDirection.Forward);
keysetContext = callKeysetPaginate(query, KeysetPaginationDirection.Forward, null);
}

data = await keysetContext.Query
Expand All @@ -298,6 +418,22 @@ public async Task<KeysetPaginationResult<TOut>> KeysetPaginateAsync<T, TOut>(
return new KeysetPaginationResult<TOut>(data, totalCount, pageSize, hasPrevious, hasNext);
}

/// <inheritdoc/>
public Task<KeysetPaginationResult<TOut>> KeysetPaginateAsync<T, TOut>(
IQueryable<T> source,
KeysetQueryDefinition<T> keysetQueryDefinition,
Func<string, Task<T?>> getReferenceAsync,
Func<IQueryable<T>, IQueryable<TOut>> map,
int? pageSize = null)
where T : class
where TOut : class
{
var queryModel = ParseKeysetQueryModel(
_httpContext.Request.Query,
pageSize);
return KeysetPaginateAsync(source, keysetQueryDefinition, getReferenceAsync, map, queryModel);
}

/// <inheritdoc/>
public Task<KeysetPaginationResult<TOut>> KeysetPaginateAsync<T, TOut>(
IQueryable<T> source,
Expand Down
16 changes: 16 additions & 0 deletions test/MR.AspNetCore.Pagination.Tests/PaginationServiceTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Microsoft.Extensions.Primitives;
using Moq;
using MR.AspNetCore.Pagination.TestModels;
using MR.EntityFrameworkCore.KeysetPagination;
using Xunit;

namespace MR.AspNetCore.Pagination.Tests;
Expand Down Expand Up @@ -62,6 +63,21 @@ public async Task KeysetPaginateAsync()
result.Data.Should().HaveCount(defaultPageSize);
}

[Fact]
public async Task KeysetPaginateAsync_Prebuilt()
{
var query = DbContext.Orders;
var keysetQuery = KeysetQuery.Build<Order>(b => b.Ascending(o => o.Id));

var result = await Service.KeysetPaginateAsync(
query,
keysetQuery,
async id => await DbContext.Orders.FindAsync(int.Parse(id)));

var defaultPageSize = new PaginationOptions().DefaultSize;
result.Data.Should().HaveCount(defaultPageSize);
}

[Fact]
public async Task OffsetPaginateAsync()
{
Expand Down

0 comments on commit 5a49207

Please sign in to comment.