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
4 changes: 2 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@
</PropertyGroup>

<PropertyGroup>
<EntityFrameworkCoreVersion>10.0.3</EntityFrameworkCoreVersion>
<EntityFrameworkCoreVersion>10.0.11</EntityFrameworkCoreVersion>
</PropertyGroup>

<PropertyGroup Condition="'$(IsPackable)' != 'false'">
<Version>3.0.3</Version>
<Version>3.1.0</Version>
<Authors>DrSkillIssue</Authors>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/DrSkillIssue/EFPagination</PackageProjectUrl>
Expand Down
14 changes: 7 additions & 7 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="$(EntityFrameworkCoreVersion)" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="$(EntityFrameworkCoreVersion)" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="$(EntityFrameworkCoreVersion)" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.201" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.400" />
</ItemGroup>

<!-- Analyzers/Generators (Roslyn) -->
<ItemGroup>
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.3.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.6.0" />
</ItemGroup>

<!-- Benchmark -->
Expand All @@ -26,13 +26,13 @@

<!-- Test -->
<ItemGroup>
<PackageVersion Include="coverlet.collector" Version="8.0.0" />
<PackageVersion Include="FluentAssertions" Version="8.8.0" />
<PackageVersion Include="GitHubActionsTestLogger" Version="3.0.1" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageVersion Include="coverlet.collector" Version="10.0.1" />
<PackageVersion Include="FluentAssertions" Version="8.10.0" />
<PackageVersion Include="GitHubActionsTestLogger" Version="3.0.5" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.9.0" />
<PackageVersion Include="Moq" Version="4.20.72" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="xunit.runner.visualstudio" Version="4.0.0" />
</ItemGroup>

</Project>
12 changes: 6 additions & 6 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,8 @@ public sealed class PaginationSortRegistry<T>

Behavior:

- `Resolve` uses the default definition when `sortBy` is empty or unknown.
- `TryResolve` returns `false` when `sortBy` does not match any registered field (empty `sortBy` returns the default and `true`).
- Selects the descending variant only when `sortDir` equals `"desc"` (case-insensitive).
- Uses ascending for all other direction values.
- `Resolve` uses the default definition when `sortBy` is empty or unknown, selects the descending variant when `sortDir` equals `"desc"` (case-insensitive), and uses ascending for all other direction values.
- `TryResolve` returns `false` when `sortBy` does not match any registered field or `sortDir` is outside empty, `"asc"`, or `"desc"` (case-insensitive). Empty `sortBy` returns the default and `true`.

## SortField\<T\> / SortField

Expand Down Expand Up @@ -536,7 +534,7 @@ KeysetQueryBuilder<T> FromRequest<T>(

### Keyset (Registry + Request)

Creates a builder from a sort registry and pagination request, resolving the definition from the request's sort parameters and applying cursors:
Creates a builder from a sort registry and pagination request, resolving the definition from the request's sort parameters and applying cursors. Throws `BadHttpRequestException` for an unknown sort field or an unsupported direction:

```cs
KeysetQueryBuilder<T> Keyset<T>(
Expand Down Expand Up @@ -597,7 +595,7 @@ Task<PaginatedResponse<TOut>> PaginateAsync<T, TOut>(
CancellationToken ct = default) where T : class;
```

Both have a `PaginationSortRegistry<T>` overload that resolves the definition from `request.SortBy`/`request.SortDir`:
Both have a `PaginationSortRegistry<T>` overload that resolves the definition from `request.SortBy`/`request.SortDir`, throwing `BadHttpRequestException` for an unknown sort field or an unsupported direction:

```cs
Task<PaginatedResponse<TOut>> PaginateAsync<T, TOut>(
Expand All @@ -621,6 +619,8 @@ Task<PaginatedResponse<TOut>> PaginateAsync<T, TOut>(

The `Expression<Func<T, TOut>>` overloads run the projection inside the SQL statement that applies the keyset `ORDER BY`. Subqueries inside the selector stay server-side and the SELECT list materializes only the projected columns plus the keyset key columns. The pagination definition must have 1–8 key columns; outside that range, the overload throws `NotSupportedException`.

Every `PaginateAsync` overload throws `BadHttpRequestException` (status 400) for an invalid or expired cursor and for a `request.PageSize` of zero or less. ASP.NET Core's exception handler middleware maps its status code to the response.

See [Server-Side Projection](patterns.md#server-side-projection) for end-to-end examples.

## Exceptions
Expand Down
2 changes: 1 addition & 1 deletion docs/patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ app.MapGet("/api/users", async (
});
```

The `Keyset(registry, request)` overload resolves the definition from `request.SortBy`/`request.SortDir`, applies the cursor from `request.After` or `request.Before`, and embeds the sort key in cursor tokens so sort context is preserved across pages.
The `Keyset(registry, request)` overload resolves the definition from `request.SortBy`/`request.SortDir`, applies the cursor from `request.After` or `request.Before`, and embeds the sort key in cursor tokens so sort context is preserved across pages. It throws `BadHttpRequestException` (status 400) for an unknown sort field or an unsupported direction.

## Manual Cursor Encode/Decode

Expand Down
2 changes: 1 addition & 1 deletion docs/prebuilt-definitions.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ var page = await dbContext.Users

`Resolve` is case-insensitive for both field names and `desc`, and falls back to the default definition when the request asks for an unknown sort.

`TryResolve` returns `false` instead of falling back, allowing you to reject invalid sort field names:
`TryResolve` returns `false` for an unknown sort field or an unsupported direction, allowing you to reject invalid sort requests:

```cs
if (!Sorts.TryResolve(sortBy, sortDir, out var definition))
Expand Down
86 changes: 43 additions & 43 deletions samples/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,105 +4,105 @@
"net10.0": {
"Microsoft.EntityFrameworkCore": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "CDEImwD4A7BseABJMCpLZnhfFjmPY/bHwhhS70elc6gLI/bYUEOhxWt7PmaNGYGhIEzOnStlCy5QcVb+8dod5Q==",
"requested": "[10.0.11, )",
"resolved": "10.0.11",
"contentHash": "VOSGU8en6HZJs8t7UMFN+9vGcRgVOOn6fA44Ngcg2NyvJ3P1KE94iAb0XzaVaGhXGtt+qaM/VtEn0/hzluQJeg==",
"dependencies": {
"Microsoft.EntityFrameworkCore.Abstractions": "10.0.3",
"Microsoft.EntityFrameworkCore.Analyzers": "10.0.3"
"Microsoft.EntityFrameworkCore.Abstractions": "10.0.11",
"Microsoft.EntityFrameworkCore.Analyzers": "10.0.11"
}
},
"Microsoft.EntityFrameworkCore.Sqlite": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "pXP9l00ZzsZuS6ajUJFLHi5vt3vVxHaxQXCwRs73tyqNbjPX/4ac114wyfAepMSmUKq0roFFoTZ7h+1K1+iQjQ==",
"requested": "[10.0.11, )",
"resolved": "10.0.11",
"contentHash": "jc7iVrhQyInR3loraMESfEFaFOtQOB1mRKHjX6QYC9o7YDbfMNbAPnIwlpffnFwhXd6/27FKaaV+sWSoLd4F1g==",
"dependencies": {
"Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.3",
"Microsoft.Extensions.DependencyModel": "10.0.3",
"SQLitePCLRaw.bundle_e_sqlite3": "2.1.11",
"SQLitePCLRaw.core": "2.1.11"
"Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.11",
"Microsoft.Extensions.DependencyModel": "10.0.11",
"SQLitePCLRaw.bundle_e_sqlite3": "2.1.12",
"SQLitePCLRaw.core": "2.1.12"
}
},
"Microsoft.Data.Sqlite.Core": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "onD94qHlvteS2S9eg1T7Huehm3x92no4nU1AyDWjSmT6jDBhY8QF0UapwNhFA+1dArzxM10fV7s6uI1EIK2+dw==",
"resolved": "10.0.11",
"contentHash": "hubA20AGenQ4Sx0ElWaPpB8DISjXpdx463+1zOGRslsT0e/t/06ITv+pHsop8CcJ0d8PZLfgnT7juCDVD79Dkw==",
"dependencies": {
"SQLitePCLRaw.core": "2.1.11"
"SQLitePCLRaw.core": "2.1.12"
}
},
"Microsoft.EntityFrameworkCore.Abstractions": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "c7Uoz381xnMHNBRB8eHRhGgzUtXbgddlbODhwZRrTSzZWDharp3RkJsFwhxyESbeXhCqmML7VdvjMQ7uu+HreA=="
"resolved": "10.0.11",
"contentHash": "6auJR+9+9VunznKfH7WGrHMrnrmA0F7JZ22EXzwXvVhjfnbu9Xq7NSIWaOf3KJsOanM2qf5ajJ2JR5TlcPZTLA=="
},
"Microsoft.EntityFrameworkCore.Analyzers": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "lVABgJTyTUNE7Bi0bSu4dWHiCHIXEGzTh/kLh0N07IgU/tIKwTeBPp8tgV4x2iKj4h7iPLo8oXzyHmLDGtAE1g=="
"resolved": "10.0.11",
"contentHash": "Bv7X4wSSnzCQED9WYXKJ8fwgyvKwf0xZM1GO8xkf6CF9zl+UBnvjxmcPnokJRy0JKjc1SlHSzzhx1HcL4jitTQ=="
},
"Microsoft.EntityFrameworkCore.Relational": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "Pmh60OK9neVr/M0FJwm9hlzm2bD4Kd65SID8E6SP5c90tExNgXwORrlEWl0oGU/ig9ifpNN4PSpIrnHNozlT5w==",
"resolved": "10.0.11",
"contentHash": "grznnTJgEYxaWpdKAsTzg6j+89jHgCXWYp+QGtlX5O92+w/VuhWM6JLPYb+uw8M9VhGUvOTsO76dYOy9vNPd5Q==",
"dependencies": {
"Microsoft.EntityFrameworkCore": "10.0.3"
"Microsoft.EntityFrameworkCore": "10.0.11"
}
},
"Microsoft.EntityFrameworkCore.Sqlite.Core": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "bUkOkSwPqvhNlsMMg5dA/PR9S4cVehiHEIZklakRH6JZCFsEBNvUz8kBRGK8Hi6mcaPKUAmVZQkN3moZsBYZLA==",
"resolved": "10.0.11",
"contentHash": "ywTQKt32xnVhCzjEQAqFufpEyXkOUfvW/EC/s4xnS8Xaor2xXE+TMUyzhgACqXtZEU5IR95y94RDzHto55Fx7w==",
"dependencies": {
"Microsoft.Data.Sqlite.Core": "10.0.3",
"Microsoft.EntityFrameworkCore.Relational": "10.0.3",
"Microsoft.Extensions.DependencyModel": "10.0.3",
"SQLitePCLRaw.core": "2.1.11"
"Microsoft.Data.Sqlite.Core": "10.0.11",
"Microsoft.EntityFrameworkCore.Relational": "10.0.11",
"Microsoft.Extensions.DependencyModel": "10.0.11",
"SQLitePCLRaw.core": "2.1.12"
}
},
"Microsoft.Extensions.DependencyModel": {
"type": "Transitive",
"resolved": "10.0.3",
"contentHash": "31kRjr1fgdJO1UZ/AsjL2noqwht+juHMQ8c/oh8CEsDhlM2+0zwVZVsZjxSfOFiPtn5+6kRGuvSbLAufAPT0kA=="
"resolved": "10.0.11",
"contentHash": "PJPtFYsZ+r+uz9qqXWUTEyKeJ1EiBGIJtqavkg9ZXijjGSFAk4Fgi5sqIxj+uAyLZwEKgexDUQXhWhvU6l3+og=="
},
"SQLitePCLRaw.bundle_e_sqlite3": {
"type": "Transitive",
"resolved": "2.1.11",
"contentHash": "DC4nA7yWnf4UZdgJDF+9Mus4/cb0Y3Sfgi3gDnAoKNAIBwzkskNAbNbyu+u4atT0ruVlZNJfwZmwiEwE5oz9LQ==",
"resolved": "2.1.12",
"contentHash": "mAgscpQMLw5/nfA1Q5oJVAT29yROUo1ifZGbbTpx/lwZpSxMUGoYbKfmvdm8oXER+RzxqBmmQzeBEVKfeHv2nw==",
"dependencies": {
"SQLitePCLRaw.lib.e_sqlite3": "2.1.11",
"SQLitePCLRaw.provider.e_sqlite3": "2.1.11"
"SQLitePCLRaw.lib.e_sqlite3": "2.1.12",
"SQLitePCLRaw.provider.e_sqlite3": "2.1.12"
}
},
"SQLitePCLRaw.core": {
"type": "Transitive",
"resolved": "2.1.11",
"contentHash": "PK0GLFkfhZzLQeR3PJf71FmhtHox+U3vcY6ZtswoMjrefkB9k6ErNJEnwXqc5KgXDSjige2XXrezqS39gkpQKA=="
"resolved": "2.1.12",
"contentHash": "ETpNw9DY3ckWLgRRAeCHj+GKOuPi61aeczkXhgHexUvqoZBAYg8RYESE2J7O1M7+o6QbdSEZwrw9bfqztUVWXg=="
},
"SQLitePCLRaw.lib.e_sqlite3": {
"type": "Transitive",
"resolved": "2.1.11",
"contentHash": "Ev2ytaXiOlWZ4b3R67GZBsemTINslLD1DCJr2xiacpn4tbapu0Q4dHEzSvZSMnVWeE5nlObU3VZN2p81q3XOYQ=="
"resolved": "2.1.12",
"contentHash": "fWi8Dbknuhgg72fWinIdjXVaqO1hHL4YBBwVLnr7e1c9TAZwJ0QE38j9syW1hwx6HaqEVTwI+O07WPdZn8Rp0w=="
},
"SQLitePCLRaw.provider.e_sqlite3": {
"type": "Transitive",
"resolved": "2.1.11",
"contentHash": "Y/0ZkR+r0Cg3DQFuCl1RBnv/tmxpIZRU3HUvelPw6MVaKHwYYR8YNvgs0vuNuXCMvlyJ+Fh88U1D4tah1tt6qw==",
"resolved": "2.1.12",
"contentHash": "W3oH4XIfCzFrgUSDKHhN6N+dgzA5YHOR2VxX8GB6Qy7CyrJJgxPEG8NirgYWlPQC5P2jz2knSsexWu4tDUL33g==",
"dependencies": {
"SQLitePCLRaw.core": "2.1.11"
"SQLitePCLRaw.core": "2.1.12"
}
},
"efpagination": {
"type": "Project",
"dependencies": {
"Microsoft.EntityFrameworkCore": "[10.0.3, )"
"Microsoft.EntityFrameworkCore": "[10.0.11, )"
}
},
"efpagination.aspnetcore": {
"type": "Project",
"dependencies": {
"EFPagination": "[1.1.3, )"
"EFPagination": "[3.0.3, )"
}
}
}
Expand Down
Loading