Wiaoj.Querying.Pagination.EntityFrameworkCore 0.2.0-alpha.3

This is a prerelease version of Wiaoj.Querying.Pagination.EntityFrameworkCore.
dotnet add package Wiaoj.Querying.Pagination.EntityFrameworkCore --version 0.2.0-alpha.3
                    
NuGet\Install-Package Wiaoj.Querying.Pagination.EntityFrameworkCore -Version 0.2.0-alpha.3
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Wiaoj.Querying.Pagination.EntityFrameworkCore" Version="0.2.0-alpha.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Wiaoj.Querying.Pagination.EntityFrameworkCore" Version="0.2.0-alpha.3" />
                    
Directory.Packages.props
<PackageReference Include="Wiaoj.Querying.Pagination.EntityFrameworkCore" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Wiaoj.Querying.Pagination.EntityFrameworkCore --version 0.2.0-alpha.3
                    
#r "nuget: Wiaoj.Querying.Pagination.EntityFrameworkCore, 0.2.0-alpha.3"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Wiaoj.Querying.Pagination.EntityFrameworkCore@0.2.0-alpha.3
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Wiaoj.Querying.Pagination.EntityFrameworkCore&version=0.2.0-alpha.3&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Wiaoj.Querying.Pagination.EntityFrameworkCore&version=0.2.0-alpha.3&prerelease
                    
Install as a Cake Tool

Wiaoj.Querying.Pagination.EntityFrameworkCore

Pages the result of a query contract in a single call, using Entity Framework Core. The call validates the request, applies the filters, search and sort, projects each row to the response type, and pages the result, with offset or keyset pages.

It connects Wiaoj.Querying and Wiaoj.Pagination.EntityFrameworkCore. Because the connection lives in this package, Wiaoj.Querying itself does not depend on EF Core.

Installation

dotnet add package Wiaoj.Querying.Pagination.EntityFrameworkCore

Offset pages

Declare the endpoint's contract as a QuerySchema<TEntity, TResponse> with a tie-breaker:

public sealed class PublicAssetSchema : QuerySchema<Asset, AssetSummaryResponse> {
    public PublicAssetSchema() {
        Project(a => new AssetSummaryResponse(a.Id.Encode(), a.FileName, a.FileSize));
        AllowFilter(a => a.FileName);
        AllowSort(a => a.FileSize);
        DefaultSort(a => a.FileSize, SortDirection.Descending);
        TieBreaker(a => a.Id);
    }
}

Then page with one call:

app.MapGet("/assets", async (Query<Asset> query, [AsParameters] PageRequest page, PublicAssetSchema schema, AppDbContext db, CancellationToken ct) =>
        TypedResults.Ok(await db.Assets
            .Where(a => a.ApplicationId == appId)
            .ToPagedResultAsync(query, schema, page, ct)))
   .WithQueryValidation<Asset, PublicAssetSchema>()
   .WithPagination();

The call does the following:

  • Validates the request. A misspelt filter throws QueryValidationException. It is never silently skipped, which would return a wider result.
  • Orders the rows. It applies the caller's sort if there is one, otherwise the schema's default sort, and always ends with the tie-breaker. Without a tie-breaker, rows that share a sort value can appear on two pages and be missing from another, so a schema without one throws.
  • Projects in SQL. Only the columns the projection uses are read.
  • Returns the response shape. The result is PagedResult<TResponse> with its metadata. There is no mapping step where the metadata could be dropped.

Any ordering you apply to the source before the call is replaced, because the contract decides the order.

Keyset pages

A cursor-paged endpoint lets the caller pick the sort, and the cursor seeks on that sort:

public sealed class AssetFeedSchema : QuerySchema<Asset, AssetSummaryResponse> {
    public AssetFeedSchema() {
        Project(a => new AssetSummaryResponse(a.Id.Encode(), a.FileName, a.FileSize));
        AllowFilter(a => a.FileName);
        Property(a => a.CreatedAt).AsCursor();      // sortable, and pageable by cursor
        Property(a => a.FileName).AsCursor();
        AllowSort(a => a.FileSize);                 // sortable on offset endpoints only
        DefaultSort(a => a.CreatedAt, SortDirection.Descending);
        TieBreaker(a => a.Id, id => id.Value.ToString(CultureInfo.InvariantCulture),
                              text => new AssetId(long.Parse(text, CultureInfo.InvariantCulture)));
    }
}

CursorResult<AssetSummaryResponse> page = await db.Assets
    .Where(a => a.ApplicationId == appId)
    .ToCursorResultAsync(query, schema, cursorRequest, ct);

Why this combination is safe. Applying a client-chosen sort and then paging with a cursor fixed on another key loses rows without any error (#73). Here the cursor is built from the sort keys followed by the tie-breaker, so the seek continues exactly where the ordering stopped.

What the call enforces:

Situation Result
sort= names a field that is not AsCursor() QueryValidationException (FieldNotCursorSortable) → 400
A cursor is sent back with a different sort, including only a changed direction QueryValidationException (CursorSortChanged) → 400
The cursor cannot be read QueryValidationException (InvalidCursor) → 400
No sort= The schema's default sort, whose fields must be AsCursor(). With no default sort, the tie-breaker alone.
The schema has no tie-breaker, or no codec for it InvalidOperationException: a server configuration error
A cursor key is null on a page boundary InvalidOperationException: a keyset seek cannot move past NULL

Behind WithQueryValidation, a QueryValidationException thrown from the handler becomes the same 400 ValidationProblem that an invalid query string gets.

Cursor keys. Built-in codecs cover strings, integers, decimal, floating point, bool, Guid, DateTime, DateTimeOffset, DateOnly, TimeOnly, TimeSpan and enums. A strongly-typed id needs a codec. A nullable value type cannot be a cursor key.

The cursor itself. It records the sort it was issued for as a fingerprint. That fingerprint detects a changed sort; it is not a signature. If callers must not be able to forge a cursor, sign it with SignedCursorToken.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.0-alpha.3 46 9/24/2026
0.2.0-alpha.2 40 9/24/2026
0.2.0-alpha.1 45 9/24/2026
0.1.0-alpha.9 50 9/21/2026
0.1.0-alpha.8 44 9/21/2026
0.1.0-alpha.7 56 9/18/2026
0.1.0-alpha.6 47 9/16/2026
0.1.0-alpha.5 53 9/16/2026
0.1.0-alpha.4 51 9/16/2026
0.1.0-alpha.3 47 9/15/2026
0.1.0-alpha.2 49 9/15/2026
0.1.0-alpha.1 65 9/14/2026
0.0.1-alpha.112-preview 53 9/13/2026
0.0.1-alpha.111-preview 51 9/13/2026