Skip to content

Commit

Permalink
feat: support paged query
Browse files Browse the repository at this point in the history
  • Loading branch information
skarllot committed Sep 30, 2023
1 parent 8a8a10f commit 2a3ab25
Show file tree
Hide file tree
Showing 12 changed files with 223 additions and 5 deletions.
25 changes: 25 additions & 0 deletions src/Expressions.Database/Queries/PagedQuery.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace Raiqub.Expressions.Queries;

internal static class PagedQuery
{
public static IQueryable<T> PrepareQueryForPaging<T>(this IQueryable<T> queryable, int pageNumber, int pageSize)
{
if (pageNumber < 1)
{
throw new ArgumentOutOfRangeException(
nameof(pageNumber),
$"Page number must be greater than or equals to 1, but it is {pageNumber}");
}

if (pageSize < 1)
{
throw new ArgumentOutOfRangeException(
nameof(pageSize),
$"Page size must be greater than or equals to 1, but it is {pageSize}");
}

return pageNumber != 1
? queryable.Skip((pageNumber - 1) * pageSize).Take(pageSize)
: queryable.Take(pageSize);
}
}
6 changes: 6 additions & 0 deletions src/Expressions.Database/Queries/QueryLog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,15 @@ internal static class QueryLog
5,
"Error trying to enumerate found elements");

private static readonly Action<ILogger, Exception?> s_pagedListErrorCallback = LoggerMessage.Define(
LogLevel.Error,
6,
"Error trying to query a page of found elements");

public static void AnyError(ILogger logger, Exception exception) => s_anyErrorCallback(logger, exception);
public static void CountError(ILogger logger, Exception exception) => s_countErrorCallback(logger, exception);
public static void FirstError(ILogger logger, Exception exception) => s_firstErrorCallback(logger, exception);
public static void PagedListError(ILogger logger, Exception exception) => s_pagedListErrorCallback(logger, exception);
public static void ListError(ILogger logger, Exception exception) => s_listErrorCallback(logger, exception);
public static void SingleError(ILogger logger, Exception exception) => s_singleErrorCallback(logger, exception);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<TargetFrameworks>net6.0;netstandard2.1</TargetFrameworks>
<Description>Provides implementation of sessions and factories using Entity Framework Core.</Description>
<PackageTags>
spec;specification;ddd;domain;ef;efcore;core;query;querymodel;command;expressions;lambda;db;database;relational
spec;specification;ddd;domain;ef;efcore;core;query;querymodel;command;expressions;lambda;db;database;relational;paging;pagination;page
</PackageTags>
</PropertyGroup>

Expand Down
33 changes: 33 additions & 0 deletions src/Expressions.EntityFrameworkCore/Queries/EfDbQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Raiqub.Expressions.Queries;
using Raiqub.Expressions.Queries.Internal;

namespace Raiqub.Expressions.EntityFrameworkCore.Queries;

Expand Down Expand Up @@ -83,6 +84,38 @@ and not InvalidOperationException
}
}

public async Task<PagedResult<TResult>> ToPagedListAsync(
int pageNumber,
int pageSize,
CancellationToken cancellationToken = default)
{
var pagedQuery = _dataSource.PrepareQueryForPaging(pageNumber, pageSize);

try
{
long totalCount = await _dataSource
.LongCountAsync(cancellationToken)
.ConfigureAwait(false);

if (!Paging.PageNumberExists(pageNumber, pageSize, totalCount))
{
return new PagedResult<TResult>(pageNumber, pageSize, Array.Empty<TResult>(), 0L);
}

var items = await pagedQuery
.ToListAsync(cancellationToken)
.ConfigureAwait(false);

return new PagedResult<TResult>(pageNumber, pageSize, items, totalCount);
}
catch (Exception exception) when (exception is not ArgumentNullException
and not OperationCanceledException)
{
QueryLog.ListError(_logger, exception);
throw;
}
}

public async Task<IReadOnlyList<TResult>> ToListAsync(CancellationToken cancellationToken = default)
{
try
Expand Down
2 changes: 1 addition & 1 deletion src/Expressions.Marten/Expressions.Marten.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<TargetFramework>net6.0</TargetFramework>
<Description>Provides implementation of sessions and factories using Marten library.</Description>
<PackageTags>
spec;specification;ddd;domain;marten;postgres;postgresql;core;query;querymodel;command;expressions;lambda;db;database;json;document
spec;specification;ddd;domain;marten;postgres;postgresql;core;query;querymodel;command;expressions;lambda;db;database;json;document;paging;pagination;page
</PackageTags>
</PropertyGroup>

Expand Down
29 changes: 28 additions & 1 deletion src/Expressions.Marten/Queries/MartenDbQuery.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Marten;
using JasperFx.Core.Reflection;
using Marten;
using Marten.Linq;
using Microsoft.Extensions.Logging;
using Raiqub.Expressions.Queries;

Expand Down Expand Up @@ -82,6 +84,31 @@ and not InvalidOperationException
}
}

public async Task<PagedResult<TResult>> ToPagedListAsync(
int pageNumber,
int pageSize,
CancellationToken cancellationToken = default)
{
var queryable = _dataSource.As<IMartenQueryable<TResult>>()
.Stats(out QueryStatistics stats)
.PrepareQueryForPaging(pageNumber, pageSize);

try
{
var items = await queryable
.ToListAsync(cancellationToken)
.ConfigureAwait(false);

return new PagedResult<TResult>(pageNumber, pageSize, items, stats.TotalResults);
}
catch (Exception exception) when (exception is not ArgumentNullException
and not OperationCanceledException)
{
QueryLog.PagedListError(_logger, exception);
throw;
}
}

public async Task<IReadOnlyList<TResult>> ToListAsync(CancellationToken cancellationToken = default)
{
try
Expand Down
2 changes: 1 addition & 1 deletion src/Expressions.Reading/Expressions.Reading.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<RootNamespace>Raiqub.Expressions</RootNamespace>
<Description>Provides abstractions for creating query strategies, query sessions and querying from database (defines IDbQuerySessionFactory and IDbQuerySession interfaces).</Description>
<PackageTags>
query;querystrategy;strategy;session;ddd;domain;core;repository;expressions;lambda;db;database
query;querystrategy;strategy;session;ddd;domain;core;repository;expressions;lambda;db;database;paging;pagination;page
</PackageTags>
</PropertyGroup>

Expand Down
8 changes: 8 additions & 0 deletions src/Expressions.Reading/Queries/IDbQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ public interface IDbQuery<T>
/// <returns>A task that represents the asynchronous operation. The task result contains the first element of the query result, or <see langword="null"/> if the query result contains no elements.</returns>
Task<T?> FirstOrDefaultAsync(CancellationToken cancellationToken = default);

/// <summary>Returns a page from the available elements in the query result.</summary>
/// <param name="pageNumber">The one-based page number to retrieve.</param>
/// <param name="pageSize">The maximum number of elements to return.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken" /> to observe while waiting for the task to complete.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the elements of the page and related information to help pagination.</returns>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="pageNumber"/> or <paramref name="pageSize"/> is less than 1.</exception>
Task<PagedResult<T>> ToPagedListAsync(int pageNumber, int pageSize, CancellationToken cancellationToken = default);

/// <summary>Returns a read-only list of the elements in the query result.</summary>
/// <param name="cancellationToken">A <see cref="CancellationToken" /> to observe while waiting for the task to complete.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a read-only list of the elements in the query result.</returns>
Expand Down
15 changes: 15 additions & 0 deletions src/Expressions.Reading/Queries/Internal/Paging.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Raiqub.Expressions.Queries.Internal;

public static class Paging
{
public static bool PageNumberExists(int pageNumber, int pageSize, long totalCount)
{
long pageCount = GetPageCount(pageSize, totalCount);
return pageNumber <= pageCount;
}

public static long GetPageCount(int pageSize, long totalCount)
{
return totalCount > 0L ? (long)Math.Ceiling(totalCount / (double)pageSize) : 0L;
}
}
62 changes: 62 additions & 0 deletions src/Expressions.Reading/Queries/PagedResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using Raiqub.Expressions.Queries.Internal;

namespace Raiqub.Expressions.Queries;

/// <summary>Represents the result of paged query.</summary>
/// <typeparam name="TResult">The type of items returned by the query.</typeparam>
public sealed class PagedResult<TResult>
{
public PagedResult(long pageNumber, int pageSize, IReadOnlyList<TResult> items, long totalCount)
{
PageNumber = pageNumber;
PageSize = pageSize;
Items = items;
TotalCount = totalCount;

PageCount = Paging.GetPageCount(pageSize, totalCount);
}

/// <summary>Gets current page number (one-based).</summary>
public long PageNumber { get; }

/// <summary>Gets page size.</summary>
public int PageSize { get; }

/// <summary>Gets the records found for the paged query.</summary>
public IReadOnlyList<TResult> Items { get; }

/// <summary>Gets the total number of records.</summary>
public long TotalCount { get; }

/// <summary>Gets number of available pages.</summary>
public long PageCount { get; }

/// <summary>Gets a value indicating whether there is a previous page.</summary>
public bool HasPreviousPage => PageCount > 0 && PageNumber > 1;

/// <summary>Gets a value indicating whether there is a next page.</summary>
public bool HasNextPage => PageNumber < PageCount;

/// <summary>Gets a value indicating whether the current page is the first page.</summary>
public bool IsFirstPage => PageCount > 0 && PageNumber == 1;

/// <summary>Gets a value indicating whether the current page is the last page.</summary>
public bool IsLastPage => PageCount > 0 && PageNumber == PageCount;

/// <summary>Gets a value indicating whether the current page exists.</summary>
public bool IsValidPage => PageNumber <= PageCount;

/// <summary>Gets one-based index of first item in current page.</summary>
public long FirstItemOnPage => IsValidPage ? (PageNumber - 1L) * PageSize + 1L : 0L;

/// <summary>Gets one-based index of last item in current page.</summary>
public long LastItemOnPage
{
get
{
if (!IsValidPage) return 0L;
long num = FirstItemOnPage + PageSize - 1L;
return num > TotalCount ? TotalCount : num;
}
}
}
42 changes: 42 additions & 0 deletions tests/Common.Tests/Queries/QueryTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,48 @@ public async Task FirstOrDefaultShouldReturnExpected(string name, string? expect
post?.Title.Should().Be(expected);
}

[Fact]
public async Task ToPagedListShouldReturnPage()
{
await AddBlogs(GetBlogs());
await using var session = CreateSession();
var query = session.Query<Blog>();

var pagedResult1 = await query.ToPagedListAsync(1, 10);
var pagedResult2 = await query.ToPagedListAsync(2, 2);
var pagedResult3 = await query.ToPagedListAsync(3, 2);

pagedResult1.TotalCount.Should().Be(3);
pagedResult1.Items.Should().HaveCount(3);
pagedResult1.IsFirstPage.Should().BeTrue();
pagedResult1.IsLastPage.Should().BeTrue();
pagedResult1.HasNextPage.Should().BeFalse();
pagedResult1.HasPreviousPage.Should().BeFalse();
pagedResult1.PageCount.Should().Be(1);
pagedResult1.FirstItemOnPage.Should().Be(1);
pagedResult1.LastItemOnPage.Should().Be(3);

pagedResult2.TotalCount.Should().Be(3);
pagedResult2.Items.Should().HaveCount(1);
pagedResult2.IsFirstPage.Should().BeFalse();
pagedResult2.IsLastPage.Should().BeTrue();
pagedResult2.HasNextPage.Should().BeFalse();
pagedResult2.HasPreviousPage.Should().BeTrue();
pagedResult2.PageCount.Should().Be(2);
pagedResult2.FirstItemOnPage.Should().Be(3);
pagedResult2.LastItemOnPage.Should().Be(3);

pagedResult3.TotalCount.Should().Be(0);
pagedResult3.Items.Should().BeEmpty();
pagedResult3.IsFirstPage.Should().BeFalse();
pagedResult3.IsLastPage.Should().BeFalse();
pagedResult3.HasNextPage.Should().BeFalse();
pagedResult3.HasPreviousPage.Should().BeFalse();
pagedResult3.PageCount.Should().Be(0);
pagedResult3.FirstItemOnPage.Should().Be(0);
pagedResult3.LastItemOnPage.Should().Be(0);
}

[Fact]
public async Task ToListShouldReturnAll()
{
Expand Down
2 changes: 1 addition & 1 deletion version.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
"version": "2.0",
"version": "2.1",
"publicReleaseRefSpec": [
"^refs/tags/v\\\\d+\\\\.\\\\d+"
],
Expand Down

0 comments on commit 2a3ab25

Please sign in to comment.