Skip to content
Open
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
75 changes: 75 additions & 0 deletions QueryBuilder.Tests/PaginationResultTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System.Linq;
using SqlKata.Execution;
using Xunit;

namespace SqlKata.Tests
{
public class PaginationResultTests
{
[Fact]
public void ReportsPageStateFromCountAndPageSize()
{
var result = new PaginationResult<int>
{
Count = 25,
Page = 2,
PerPage = 10,
};

Assert.Equal(3, result.TotalPages);
Assert.False(result.IsFirst);
Assert.False(result.IsLast);
Assert.True(result.HasNext);
Assert.True(result.HasPrevious);
}

[Fact]
public void BuildsQueriesForAdjacentPages()
{
var nextResult = new PaginationResult<int>
{
Query = new Query("Posts"),
Page = 2,
PerPage = 10,
};

var nextQuery = nextResult.NextQuery();

Assert.Same(nextResult.Query, nextQuery);
Assert.Equal(20, nextQuery.GetOffset());
Assert.Equal(10, nextQuery.GetLimit());

var previousResult = new PaginationResult<int>
{
Query = new Query("Posts"),
Page = 2,
PerPage = 10,
};

var previousQuery = previousResult.PreviousQuery();

Assert.Same(previousResult.Query, previousQuery);
Assert.Equal(0, previousQuery.GetOffset());
Assert.Equal(10, previousQuery.GetLimit());
}

[Fact]
public void EachYieldsTheCurrentPageWhenThereIsNoNextPage()
{
var result = new PaginationResult<int>
{
Count = 2,
List = new[] { 1, 2 },
Page = 1,
PerPage = 10,
};

var iterator = result.Each;
var pages = iterator.ToList();

Assert.Single(pages);
Assert.Same(result, pages[0]);
Assert.Same(result, iterator.CurrentPage);
}
}
}
16 changes: 16 additions & 0 deletions SqlKata.Execution/PaginationIterator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,27 @@

namespace SqlKata.Execution
{
/// <summary>
/// Enumerates paginated results, beginning with a supplied first page and
/// loading each subsequent page as enumeration advances.
/// </summary>
/// <typeparam name="T">The type of each item in the paginated results.</typeparam>
public class PaginationIterator<T> : IEnumerable<PaginationResult<T>>
{
/// <summary>
/// Gets or sets the page from which enumeration starts.
/// </summary>
public PaginationResult<T> FirstPage { get; set; }

/// <summary>
/// Gets or sets the page most recently returned by the iterator.
/// </summary>
public PaginationResult<T> CurrentPage { get; set; }

/// <summary>
/// Returns an enumerator that yields the first page and loads later pages on demand.
/// </summary>
/// <returns>An enumerator over the available pages.</returns>
public IEnumerator<Execution.PaginationResult<T>> GetEnumerator()
{
CurrentPage = FirstPage;
Expand Down
77 changes: 77 additions & 0 deletions SqlKata.Execution/PaginationResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,41 @@

namespace SqlKata.Execution
{
/// <summary>
/// Represents one page of query results together with pagination metadata
/// and helpers for loading adjacent pages.
/// </summary>
/// <typeparam name="T">The type of each item in the result set.</typeparam>
public class PaginationResult<T>
{
/// <summary>
/// Gets or sets the query used to produce the paginated results.
/// </summary>
public Query Query { get; set; }

/// <summary>
/// Gets or sets the total number of matching records across all pages.
/// </summary>
public long Count { get; set; }

/// <summary>
/// Gets or sets the records in the current page.
/// </summary>
public IEnumerable<T> List { get; set; }

/// <summary>
/// Gets or sets the one-based current page number.
/// </summary>
public int Page { get; set; }

/// <summary>
/// Gets or sets the maximum number of records per page.
/// </summary>
public int PerPage { get; set; }

/// <summary>
/// Gets the total number of pages, rounded up from <see cref="Count"/>.
/// </summary>
public int TotalPages
{
get
Expand All @@ -30,6 +58,9 @@ public int TotalPages
}
}

/// <summary>
/// Gets a value indicating whether this is the first page.
/// </summary>
public bool IsFirst
{
get
Expand All @@ -38,6 +69,9 @@ public bool IsFirst
}
}

/// <summary>
/// Gets a value indicating whether this is the last page.
/// </summary>
public bool IsLast
{
get
Expand All @@ -46,6 +80,9 @@ public bool IsLast
}
}

/// <summary>
/// Gets a value indicating whether a page follows the current page.
/// </summary>
public bool HasNext
{
get
Expand All @@ -54,6 +91,9 @@ public bool HasNext
}
}

/// <summary>
/// Gets a value indicating whether a page precedes the current page.
/// </summary>
public bool HasPrevious
{
get
Expand All @@ -62,36 +102,73 @@ public bool HasPrevious
}
}

/// <summary>
/// Applies the next page's limit and offset to <see cref="Query"/>.
/// </summary>
/// <returns>The query configured for the next page.</returns>
public Query NextQuery()
{
return this.Query.ForPage(Page + 1, PerPage);
}

/// <summary>
/// Executes the query for the next page.
/// </summary>
/// <param name="transaction">The transaction to use, or <see langword="null"/> for none.</param>
/// <param name="timeout">The command timeout in seconds, or <see langword="null"/> to use the configured default.</param>
/// <returns>The next page of results.</returns>
public PaginationResult<T> Next(IDbTransaction transaction = null, int? timeout = null)
{
return this.Query.Paginate<T>(Page + 1, PerPage, transaction, timeout);
}

/// <summary>
/// Asynchronously executes the query for the next page.
/// </summary>
/// <param name="transaction">The transaction to use, or <see langword="null"/> for none.</param>
/// <param name="timeout">The command timeout in seconds, or <see langword="null"/> to use the configured default.</param>
/// <param name="cancellationToken">The token used to cancel the operation.</param>
/// <returns>A task containing the next page of results.</returns>
public async Task<PaginationResult<T>> NextAsync(IDbTransaction transaction = null, int? timeout = null, CancellationToken cancellationToken = default)
{
return await this.Query.PaginateAsync<T>(Page + 1, PerPage, transaction, timeout, cancellationToken);
}

/// <summary>
/// Applies the previous page's limit and offset to <see cref="Query"/>.
/// </summary>
/// <returns>The query configured for the previous page.</returns>
public Query PreviousQuery()
{
return this.Query.ForPage(Page - 1, PerPage);
}

/// <summary>
/// Executes the query for the previous page.
/// </summary>
/// <param name="transaction">The transaction to use, or <see langword="null"/> for none.</param>
/// <param name="timeout">The command timeout in seconds, or <see langword="null"/> to use the configured default.</param>
/// <returns>The previous page of results.</returns>
public PaginationResult<T> Previous(IDbTransaction transaction = null, int? timeout = null)
{
return this.Query.Paginate<T>(Page - 1, PerPage, transaction, timeout);
}

/// <summary>
/// Asynchronously executes the query for the previous page.
/// </summary>
/// <param name="transaction">The transaction to use, or <see langword="null"/> for none.</param>
/// <param name="timeout">The command timeout in seconds, or <see langword="null"/> to use the configured default.</param>
/// <param name="cancellationToken">The token used to cancel the operation.</param>
/// <returns>A task containing the previous page of results.</returns>
public async Task<PaginationResult<T>> PreviousAsync(IDbTransaction transaction = null, int? timeout = null, CancellationToken cancellationToken = default)
{
return await this.Query.PaginateAsync<T>(Page - 1, PerPage, transaction, timeout, cancellationToken);
}

/// <summary>
/// Gets an iterator that starts with this page and loads each remaining page.
/// </summary>
public PaginationIterator<T> Each
{
get
Expand Down
Loading