EricksonLopez.Pagination.Redis
0.0.0-alpha.0
dotnet add package EricksonLopez.Pagination.Redis --version 0.0.0-alpha.0
NuGet\Install-Package EricksonLopez.Pagination.Redis -Version 0.0.0-alpha.0
<PackageReference Include="EricksonLopez.Pagination.Redis" Version="0.0.0-alpha.0" />
<PackageVersion Include="EricksonLopez.Pagination.Redis" Version="0.0.0-alpha.0" />
<PackageReference Include="EricksonLopez.Pagination.Redis" />
paket add EricksonLopez.Pagination.Redis --version 0.0.0-alpha.0
#r "nuget: EricksonLopez.Pagination.Redis, 0.0.0-alpha.0"
#:package EricksonLopez.Pagination.Redis@0.0.0-alpha.0
#addin nuget:?package=EricksonLopez.Pagination.Redis&version=0.0.0-alpha.0&prerelease
#tool nuget:?package=EricksonLopez.Pagination.Redis&version=0.0.0-alpha.0&prerelease
EricksonLopez.Pagination
High-performance, zero-allocation, AOT-first offset and keyset pagination ecosystem with cryptographic tamper-proof cursors and dynamic filter/sort DSL for modern .NET.
EricksonLopez.Pagination is an enterprise-grade pagination and data traversal ecosystem engineered for high-throughput .NET 8, .NET 9, and .NET 10 systems. It eliminates the severe $O(N)$ latency degradation and table-scan bottlenecks of traditional SQL OFFSET/FETCH queries by providing true $O(\log N)$ keyset (cursor) pagination, count-less lookahead probing, and memory-efficient streaming across Entity Framework Core, Dapper, LinqToDB, MongoDB, Azure Cosmos DB, Elasticsearch, gRPC, Blazor, and GraphQL Relay. With cryptographic HMAC-SHA256 tamper-proof cursors, distributed nonce replay protection, compile-time Roslyn diagnostics (PAG002βPAG008), and source-generated Native AOT decoders, it provides deterministic performance, zero-allocation hot paths, and multi-tenant security out of the box.
Table of Contents
- What Problem It Solves
- Key Features
- Ecosystem
- Documentation
- Installation
- Quick Start
- Core Use Cases
- Use Case 1: Clean Architecture / CQRS Handlers
- Use Case 2: Multi-Column Keyset Pagination with Dapper
- Use Case 3: Distributed Multi-Node Cursor Replay Protection with Redis
- Use Case 4: Zero-Downtime Rolling Deployments via Schema Fingerprints
- Use Case 5: Parallel Keyset Dataset Partitioning for ETL Workers
- Use Case 6: RFC 7232 HTTP Conditional Requests & Deterministic ETag Caching
- Configuration & Integrations
- Testing & Quality
- Performance Benchmarks
- Compatibility & Technical Matrix
- Architecture & Design Principles
- Best Practices & Anti-Patterns
- Troubleshooting & Common Pitfalls
- Part of the EricksonLopez Ecosystem
- Contributing
- License
π― What Problem It Solves
The Physics of SQL OFFSET Degradation at Scale
Traditional relational pagination relies on OFFSET N LIMIT M (or FETCH NEXT). In SQL engines (PostgreSQL, SQL Server, MySQL, SQLite, Oracle), OFFSET operates in linear $O(N)$ time complexity relative to page depth:
- Row Discard Penalty: To execute
OFFSET 999900 LIMIT 100, the database engine must traverse the index root, read and evaluate MVCC visibility for 999,900 preceding rows, and discard them all before returning the 100 target rows. - Buffer Pool Thrashing: Reading millions of rows into database buffer pools evicts hot cache pages, spiking disk I/O and CPU utilization across all tenants.
- Sort Buffer Spilling: Unindexed queries sort $N + M$ records in memory, causing disk spillage (
tempdb/ temporary files) and catastrophic tail latency.
The Full-Table Scan Overhead of COUNT(*)
Standard pagination libraries execute two database queries per page: COUNT(*) followed by SELECT ... OFFSET ... LIMIT .... On tables with millions of rows, COUNT(*) performs a full sequential scan or full index scan, holding shared locks and multiplying database CPU load on every single page navigation.
Opaque Cursor Tampering & Replay Vulnerabilities
NaΓ―ve cursor implementations serialize internal database identifiers into plain Base64 strings (e.g. eyJJZCI6MTA0Mn0=). This introduces major enterprise security flaws:
- Identifier Enumeration & Scraping: Attackers decode the cursor, alter primary keys, and iterate through restricted records.
- Replay Attacks: Stale cursors can be cached or replayed indefinitely to bypass pagination boundaries or consume server resources.
- Cross-Pod Schema Drift: Modifying keyset sort criteria during rolling deployments causes desynchronized cursor errors when older pods process new cursors or vice versa.
How EricksonLopez.Pagination Solves This
- $O(\log N)$ Keyset (Cursor) Seeks: Converts pagination into
WHERE (col1, col2) > (@c1, @c2)predicates that execute direct B-Tree index seeks. Latency remains flat (~1.69β2.23 ms) whether querying row 10, row 100,000, or row 10,000,000. - Index-Seek Bounding Conditions (ADR-0019): Injects single-column prefix bounds to force database query optimizers to choose index seeks instead of multi-column index scans.
- Count-Less $N+1$ Probing: Evaluates
HasNextPageby fetchingPageSize + 1records, completely eliminatingCOUNT(*)queries when exact totals are unnecessary. - Cryptographic Tamper-Proof Cursors (ADR-0017 & ADR-0020): Encodes cursors with HMAC-SHA256 signatures, constant-time verification (
CryptographicOperations.FixedTimeEquals), configurable TTL expiration, FNV-1a schema fingerprints (ADR-0007), and distributed nonce replay stores (ADR-0035). - AST-Compiled Dynamic Filtering & Sorting: Compiles URL filter expressions (
?filter=status=active,price>=100) into typed LINQ expression trees with a 25.5x speedup via AST caching. - Native AOT & Trimming Support: Provides source generators (
EricksonLopez.Pagination.SourceGenerators) that register typed cursor decoders at compile time via[ModuleInitializer], ensuring reflection-free execution.
β‘ Key Features
- β‘ Constant-Time Keyset Navigation: True $O(\log N)$ B-Tree index seek pagination maintaining sub-2.5 ms latency across 10M+ rows.
- π Cryptographic HMAC-SHA256 Cursors: Tamper-proof, constant-time verified cursors with microsecond codec overhead (< 0.4 ΞΌs).
- β±οΈ TTL & Replay Attack Defense: Built-in cursor expiration, clock-skew tolerance, and distributed nonce stores (
ICursorReplayStore) for Redis. - π Zero-Allocation Batch Streaming: Continuous ETL streaming of millions of records via
IAsyncEnumerable<T>with $O(1)$ memory consumption. - π‘οΈ Count-Less Offset Optimization: $N+1$ lookahead probing eliminating
COUNT(*)execution on high-throughput endpoints. - π Dynamic Filter & Sort DSL: URL filter expressions compiled to expression trees with field aliasing (
[Filterable(Name)]) and custom operators (IFilterOperatorProvider<T>). - ποΈ FNV-1a Schema Fingerprints: Cross-pod schema validation ensuring seamless zero-downtime rolling deployments (ADR-0007).
- π¦ Native AOT & Trimming Ready: Compile-time decoder generators and AOT-safe filter interfaces (
IFilterProvider<T>). - π Built-In OpenTelemetry Observability: Native
System.Diagnostics.Metricscounters and histograms for query volumes, page sizes, and cursor error rates. - π‘οΈ Compile-Time Roslyn Diagnostics: Custom analyzers (
PAG002βPAG008) preventing unsorted queries, sort conflicts, and insecure cursor encoders. - π Multi-Storage Uniformity: Consistent fluent API across EF Core, Dapper, LinqToDB, MongoDB, Azure Cosmos DB, Elasticsearch, gRPC, Blazor, and GraphQL Relay.
π¦ Ecosystem
| Package | Version | Description |
|---|---|---|
EricksonLopez.Pagination |
Core implementations, PagedList<T>, CursorPagedList<T>, encoders, and DI configuration |
|
EricksonLopez.Pagination.Abstractions |
Zero-dependency contracts, IPagedList<T>, ICursorPagedList<T>, parameters, and attributes |
|
EricksonLopez.Pagination.EntityFrameworkCore |
EF Core IQueryable<T> extensions, KeysetBuilder<T>, and dynamic filter/sort DSL |
|
EricksonLopez.Pagination.Dapper |
High-performance raw SQL offset & keyset pagination for IDbConnection |
|
EricksonLopez.Pagination.LinqToDB |
LinqToDB LINQ provider offset and keyset cursor pagination extensions | |
EricksonLopez.Pagination.MongoDB |
MongoDB official driver IFindFluent<T> offset and ObjectId cursor extensions |
|
EricksonLopez.Pagination.Cosmos |
Azure Cosmos DB continuation token forward cursor pagination extensions | |
EricksonLopez.Pagination.Elasticsearch |
Elasticsearch 8.x client search_after cursor pagination and mapping |
|
EricksonLopez.Pagination.Redis |
Distributed cursor nonce replay store backed by StackExchange.Redis | |
EricksonLopez.Pagination.Relay |
GraphQL Relay Cursor Connections specification (Connection<T>, Edge<T>, PageInfo) |
|
EricksonLopez.Pagination.Result |
Railway-Oriented Programming integration with EricksonLopez.Result |
|
EricksonLopez.Pagination.AspNetCore |
Minimal API / MVC model binders, validation filters, RFC 7232 ETags, and response envelopes | |
EricksonLopez.Pagination.Blazor |
Headless <PagedListPager> Razor component with Bootstrap preset and templating |
|
EricksonLopez.Pagination.Grpc |
Google Protobuf message converters and gRPC pagination contracts | |
EricksonLopez.Pagination.OpenApi |
Swagger / OpenAPI schema transformers and parameter operation filters | |
EricksonLopez.Pagination.SourceGenerators |
Roslyn source generator emitting compile-time Native AOT cursor decoders | |
EricksonLopez.Pagination.Analyzers |
Roslyn diagnostic analyzers (PAG002βPAG008) catching pagination errors at build time |
π Documentation
π Official Documentation Hub: https://github.com/ericksonlopezf/dotnet-pagination/tree/main/docs
π Technical Reference & Architecture Guides
- System Overview β Comprehensive technical overview, architectural layering, and design tenets.
- Architecture & Invariants β Deep architectural blueprint, component interactions, and dependency graphs.
- Deep Offset Degradation & Keyset Guide β Physical storage engine execution mechanics of
OFFSETvs B-Tree keyset seeks. - Architectural Decision Records (ADRs) β Official ADR catalog documenting technical rationale and rejected architectures.
- Benchmark Suite & Results β Multi-database performance benchmarks across 1M and 10M row datasets.
- Feature Matrix & Competitive Intelligence β In-depth feature comparison vs X.PagedList, Gridify, and Sieve.
- Quality Gates & Static Analysis β Roslyn, SonarQube, Coverlet code coverage, and Stryker.NET mutation testing policies.
- API Reference β Complete public API surface reference and member documentation.
- API Inventory β Granular index of all exported public types and extension methods.
- CI/CD Pipelines β GitHub Actions enterprise build, test, and automated release pipeline specifications.
- Migration Guide β Upgrade steps and contract migration paths between major versions.
π Interactive Showcase & Recipes
- Production Cookbook & Recipes β 25+ ready-to-use production recipes covering Minimal APIs, CQRS, Redis, Dapper, and Native AOT.
- Architecture Diagrams β Complete collection of Mermaid sequence diagrams, state machines, and execution pipelines.
- Frequently Asked Questions (FAQ) β Answers to architectural questions regarding keyset constraints, tiebreakers, and AOT.
π₯ Installation
Install the required packages based on your project layer and database provider:
1. Core & Abstractions
# Domain and Application layers (contracts only)
dotnet add package EricksonLopez.Pagination.Abstractions
# Core implementation and cryptographic encoders
dotnet add package EricksonLopez.Pagination
2. Data Access Providers
# Entity Framework Core
dotnet add package EricksonLopez.Pagination.EntityFrameworkCore
# Dapper Micro-ORM
dotnet add package EricksonLopez.Pagination.Dapper
# MongoDB
dotnet add package EricksonLopez.Pagination.MongoDB
# Azure Cosmos DB
dotnet add package EricksonLopez.Pagination.Cosmos
# Elasticsearch 8.x
dotnet add package EricksonLopez.Pagination.Elasticsearch
# LinqToDB
dotnet add package EricksonLopez.Pagination.LinqToDB
3. Web, API & Transport Integrations
# ASP.NET Core Minimal APIs & MVC
dotnet add package EricksonLopez.Pagination.AspNetCore
# Blazor UI Component
dotnet add package EricksonLopez.Pagination.Blazor
# Distributed Redis Replay Store
dotnet add package EricksonLopez.Pagination.Redis
# GraphQL Relay Connections
dotnet add package EricksonLopez.Pagination.Relay
# gRPC Protobuf Converters
dotnet add package EricksonLopez.Pagination.Grpc
# OpenAPI / Swagger Schema Transformer
dotnet add package EricksonLopez.Pagination.OpenApi
# Railway-Oriented Result Integration
dotnet add package EricksonLopez.Pagination.Result
4. Compile-Time Tooling & Diagnostics
# Native AOT Cursor Decoder Generator (Development dependency)
dotnet add package EricksonLopez.Pagination.SourceGenerators
# Roslyn Diagnostic Analyzers PAG002-PAG008 (Development dependency)
dotnet add package EricksonLopez.Pagination.Analyzers
π Quick Start
1. Service Registration & Cryptographic Configuration
Configure pagination services in Program.cs. AddPagination() uses HmacCursorEncoder by default β if no key is configured, a deterministic development key is used automatically with a startup warning. For production, supply your own key and TTL:
using EricksonLopez.Pagination;
using EricksonLopez.Pagination.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
// Register Core Pagination Services
// NOTE: HmacCursorEncoder is the default. The configuration below shows production-grade
// settings with an explicit secret key and TTL. Omitting it is safe for development
// (a startup warning is logged reminding you to set a key before deploying to production).
builder.Services.AddPagination(options =>
{
// Production: cryptographically sign cursors with HMAC-SHA256 and a 30-minute TTL
options.Cursor.Encoder = new HmacCursorEncoder(
secretKey: builder.Configuration["Pagination:SecretKey"]!, // min 32 bytes UTF-8
timeToLive: TimeSpan.FromMinutes(30),
clockSkewTolerance: TimeSpan.FromSeconds(30));
options.MaxPageSize = 100;
options.DefaultPageSize = 20;
});
var app = builder.Build();
Advanced:
AddPagination()accepts an optional second parameterAction<PaginationAspNetCoreOptions>for ASP.NET Core-specific settings. The primary setting isModelBinderProviderInsertIndex, which controls where the pagination model binder is inserted in the MVC binder pipeline (default:0β head of the pipeline). Only configure this if you have a custom model binder that must run before pagination binding:builder.Services.AddPagination( configure: options => { options.MaxPageSize = 100; }, configureAspNetCore: aspOptions => { aspOptions.ModelBinderProviderInsertIndex = 2; });
2. High-Performance Keyset (Cursor) Pagination
Implement constant $O(\log N)$ keyset pagination with composite tiebreakers:
using EricksonLopez.Pagination;
using EricksonLopez.Pagination.AspNetCore;
using EricksonLopez.Pagination.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc;
app.MapGet("/api/products/feed", async (
[AsParameters] CursorPaginationParameters cursor,
[FromServices] AppDbContext db,
CancellationToken ct) =>
{
var page = await db.Products
.Keyset(cursor)
.Descending(p => p.CreatedAt) // Primary sort key
.Ascending(p => p.Id) // Unique tiebreaker (mandatory for deterministic order)
.ToCursorPagedListAsync(cancellationToken: ct);
return Results.Ok(page.ToCursorPagedResponse(p => $"{p.CreatedAt:O}|{p.Id}"));
});
Cursor Request Query: GET /api/products/feed?first=20&after=ZXlKa...
3. Count-Less Fast Offset Pagination
Eliminate costly COUNT(*) database queries on high-traffic endpoints using $N+1$ probing:
app.MapGet("/api/products", async (
[AsParameters] PaginationParameters pagination,
[FromServices] AppDbContext db,
CancellationToken ct) =>
{
// Fetches PageSize + 1 records to evaluate HasNextPage in a single round-trip
var page = await db.Products
.OrderBy(p => p.Name)
.ToPagedListWithoutCountAsync(pagination, cancellationToken: ct);
return Results.Ok(page.ToPagedResponse());
});
4. Dynamic Filter & Sort DSL
Filter and sort queries safely via URL parameters without raw SQL string concatenation:
app.MapGet("/api/products/search", async (
[AsParameters] PaginationParameters pagination,
[AsParameters] FilterParameters filter,
[AsParameters] SortParameters sort,
[FromServices] AppDbContext db,
CancellationToken ct) =>
{
var page = await db.Products
.ApplyFilter(filter) // e.g. "name~=phone,price>=100"
.ApplySort(sort, defaultSort: p => p.Id) // e.g. "price desc, name"
.ToPagedListAsync(pagination, cancellationToken: ct);
return Results.Ok(page.ToPagedResponse());
});
Supported Filter Operators:
=: Equals (status=active)!=: Not equals (status!=inactive)>,>=,<,<=: Numeric and DateTime comparisons (price>=100,createdAt>2026-01-01)~=: String contains (name~=phone)^=: String starts with (name^=apple)$=: String ends with (name$=pro),: Logical AND (name~=phone,price>=100)|: Logical OR (status=active|status=pending)
5. Zero-Allocation Batch Streaming (IAsyncEnumerable)
Stream millions of database records continuously in background ETL jobs with constant $O(1)$ memory consumption:
app.MapPost("/api/products/export", async (
[FromServices] AppDbContext db,
CancellationToken ct) =>
{
var cursor = new CursorPaginationParameters { PageSize = 1000 };
await foreach (var product in db.Products
.Keyset(cursor)
.Ascending(p => p.Id)
.AsKeysetStreamAsync(cancellationToken: ct))
{
await ProcessProductAsync(product, ct);
}
return Results.Accepted();
});
π‘ Core Use Cases
Use Case 1: Clean Architecture / CQRS Handlers
Encapsulate paginated data access in CQRS Query Handlers using MediatR and explicit return types:
public sealed record GetProductsQuery(
CursorPaginationParameters Cursor,
FilterParameters Filter) : IRequest<CursorPagedResponse<ProductDto>>;
public sealed class GetProductsQueryHandler : IRequestHandler<GetProductsQuery, CursorPagedResponse<ProductDto>>
{
private readonly AppDbContext _db;
public GetProductsQueryHandler(AppDbContext db) => _db = db;
public async Task<CursorPagedResponse<ProductDto>> Handle(GetProductsQuery query, CancellationToken ct)
{
var page = await _db.Products
.AsNoTracking()
.ApplyFilter(query.Filter)
.Keyset(query.Cursor)
.Descending(p => p.CreatedAt)
.Ascending(p => p.Id)
.ToCursorPagedListAsync(ct);
return page.Map(p => new ProductDto(p.Id, p.Name, p.Price, p.CreatedAt))
.ToCursorPagedResponse(p => $"{p.CreatedAt:O}|{p.Id}");
}
}
Use Case 2: Multi-Column Keyset Pagination with Dapper
Execute raw SQL keyset queries with DapperKeysetBuilder<T> supporting multiple dialects and typed cursor encoding:
app.MapGet("/api/orders/dapper", async (
[AsParameters] CursorPaginationParameters cursor,
[FromServices] IDbConnection connection) =>
{
var page = await new DapperKeysetBuilder<OrderDto>(connection, cursor)
.Select("id AS Id, customer_id AS CustomerId, total_amount AS TotalAmount, created_at AS CreatedAt")
.From("orders")
.Where("status = @Status", new { Status = "Completed" })
.OrderBy("created_at", SortDirection.Descending)
.ThenBy("id", SortDirection.Ascending)
.WithCursorColumns(
o => o.CreatedAt.ToString("O"),
o => o.Id.ToString(CultureInfo.InvariantCulture))
.WithCursorDecoder(
parts => DateTimeOffset.Parse(parts[0], null, DateTimeStyles.RoundtripKind),
parts => long.Parse(parts[1], CultureInfo.InvariantCulture))
.UseDialect(DatabaseDialect.PostgreSql)
.ExecuteAsync();
return Results.Ok(page.ToCursorPagedResponse(o => $"{o.CreatedAt:O}|{o.Id}"));
});
Use Case 3: Distributed Multi-Node Cursor Replay Protection with Redis
Prevent cursor replay attacks in multi-pod Kubernetes environments by backing ICursorReplayStore with Redis:
// 1. Register distributed replay store in Program.cs
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
ConnectionMultiplexer.Connect(builder.Configuration.GetConnectionString("Redis")!));
builder.Services.AddPaginationRedisReplayStore();
// 2. Consume in endpoint with automatic rejection of consumed nonces
app.MapGet("/api/secure-transactions", async (
[AsParameters] CursorPaginationParameters cursor,
[FromServices] AppDbContext db,
CancellationToken ct) =>
{
try
{
var page = await db.Transactions
.Keyset(cursor)
.Ascending(t => t.Id)
.ToCursorPagedListAsync(ct);
return Results.Ok(page.ToCursorPagedResponse(t => t.Id));
}
catch (ReplayedPaginationCursorException ex)
{
return Results.Problem(
title: "Cursor Replay Detected",
detail: $"Cursor nonce '{ex.Nonce}' has already been processed.",
statusCode: StatusCodes.Status409Conflict);
}
});
Use Case 4: Zero-Downtime Rolling Deployments via Schema Fingerprints
Prevent runtime SQL corruption during canary or blue/green deployments when sort schemas evolve (ADR-0007):
app.MapGet("/api/v2/orders", async (
[AsParameters] CursorPaginationParameters cursor,
[FromServices] AppDbContext db,
CancellationToken ct) =>
{
try
{
// 3-column keyset schema fingerprint computed automatically via FNV-1a
var page = await db.Orders
.Keyset(cursor)
.Ascending(o => o.TenantId)
.Descending(o => o.CreatedAt)
.Ascending(o => o.Id)
.ToCursorPagedListAsync(ct);
return Results.Ok(page.ToCursorPagedResponse(o => $"{o.TenantId}|{o.CreatedAt:O}|{o.Id}"));
}
catch (InvalidPaginationCursorException)
{
// Return structured 400 Bad Request instructing client to reset pagination
return Results.Problem(
title: "Pagination Schema Outdated",
detail: "The pagination keyset schema has evolved. Please restart pagination from the first page.",
statusCode: StatusCodes.Status400BadRequest);
}
});
Use Case 5: Parallel Keyset Dataset Partitioning for ETL Workers
Split a dataset across $N$ parallel background workers without row overlap or lock contention (ADR-0036):
public async Task ProcessOrdersInParallelAsync(
IServiceProvider serviceProvider,
int workerCount,
CancellationToken ct)
{
using var scope = serviceProvider.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// 1. Partition the keyset space into balanced primary key ranges
var partitions = await db.Orders
.PartitionByKeysetAsync(
keySelector: o => o.Id,
partitionCount: workerCount,
cancellationToken: ct);
// 2. Execute parallel worker streams across independent DbContext scopes
await Parallel.ForEachAsync(partitions, new ParallelOptions { MaxDegreeOfParallelism = workerCount, CancellationToken = ct },
async (partition, token) =>
{
using var workerScope = serviceProvider.CreateScope();
var workerDb = workerScope.ServiceProvider.GetRequiredService<AppDbContext>();
var workerCursor = new CursorPaginationParameters { First = 500 };
bool hasMore = true;
while (hasMore)
{
var page = await workerDb.Orders
.Where(o => o.Id >= partition.MinKey && o.Id <= partition.MaxKey)
.Keyset(workerCursor)
.Ascending(o => o.Id)
.ToCursorPagedListAsync(token);
// ICursorPagedList<T> implements IReadOnlyList<T> β enumerate directly
await ProcessBatchAsync(page, token);
hasMore = page.HasNextPage;
// CursorPaginationParameters is a readonly record struct β use 'with' to advance the cursor
workerCursor = workerCursor with { After = page.EndCursor };
}
});
}
Use Case 6: RFC 7232 HTTP Conditional Requests & Deterministic ETag Caching
Save network bandwidth and server CPU by serving 304 Not Modified responses for unchanged pages:
app.MapGet("/api/catalog", async (
[AsParameters] PaginationParameters pagination,
HttpContext httpContext,
[FromServices] AppDbContext db,
CancellationToken ct) =>
{
var page = await db.Products
.OrderBy(p => p.Id)
.ToPagedListAsync(pagination, cancellationToken: ct);
var response = page.ToPagedResponse();
// Generates deterministic ETag based on page content and evaluates If-None-Match header
bool notModified = response.ApplyETagHeaders(httpContext, maxAge: TimeSpan.FromMinutes(5));
if (notModified)
{
return Results.StatusCode(StatusCodes.Status304NotModified);
}
return Results.Ok(response);
});
π Configuration & Integrations
ASP.NET Core Minimal APIs & RouteGroup Validation
Enforce strict page size limits across entire route groups with a single declaration:
var api = app.MapGroup("/api/v1")
.AddPaginationValidation(); // Protects all endpoints within this group
api.MapGet("/customers", async (
[AsParameters] PaginationParameters pagination,
AppDbContext db,
CancellationToken ct) =>
{
var page = await db.Customers.OrderBy(c => c.Id).ToPagedListAsync(pagination, ct);
return Results.Ok(page.ToPagedResponse());
});
OpenAPI & Swagger Integration
Automatically document pagination parameters and response schemas in Swagger UI and OpenAPI v3 documents:
// Program.cs
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
// Injects ?page, ?pageSize, ?first, ?after, ?filter, ?sort query parameters into OpenAPI docs
options.OperationFilter<PaginationOperationFilter>();
});
OpenTelemetry Metrics & Observability
EricksonLopez.Pagination exports built-in metrics under the EricksonLopez.Pagination meter:
| Metric Name | Instrument | Unit | Description |
|---|---|---|---|
pagination.queries.total |
Counter | {queries} |
Total number of paginated queries executed by provider |
pagination.page.size |
Histogram | rows |
Distribution of requested page sizes |
pagination.page.depth |
Histogram | {pages} |
Distribution of requested offset page depths |
pagination.cursor.errors |
Counter | {errors} |
Count of invalid, expired, or replayed cursor attempts |
pagination.legacy_cursor_accepted |
Counter | {cursors} |
Number of legacy v1 (unsigned) cursors accepted during transition window |
// Program.cs OpenTelemetry Configuration
// Requires: OpenTelemetry.Exporter.Prometheus.AspNetCore
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics =>
{
metrics.AddMeter("EricksonLopez.Pagination")
.AddPrometheusExporter();
});
JSON Serialization & Native AOT Support
For trimmed and Native AOT applications, register the generated PaginationJsonSerializerContext:
[JsonSerializable(typeof(PagedResponse<ProductDto>))]
[JsonSerializable(typeof(CursorPagedResponse<ProductDto>))]
internal partial class AppJsonSerializerContext : JsonSerializerContext
{
}
// Program.cs
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
});
GraphQL Relay Connection Compliance
Transform keyset results into official GraphQL Relay Connection schemas:
app.MapGet("/graphql/products", async (
[AsParameters] CursorPaginationParameters cursor,
AppDbContext db,
CancellationToken ct) =>
{
var pagedList = await db.Products
.Keyset(cursor)
.Ascending(p => p.Id)
.ToCursorPagedListAsync(ct);
// Yields Connection<Product> containing Edges, PageInfo (HasNextPage, EndCursor), and Nodes
Connection<Product> connection = pagedList.ToRelayConnection(p => p.Id.ToString(CultureInfo.InvariantCulture));
return Results.Ok(connection);
});
Roslyn Diagnostic Analyzers
The EricksonLopez.Pagination.Analyzers package enforces pagination invariants at compile time:
None of these diagnostics provide an automatic Roslyn CodeFix (quick-fix lightbulb). All resolutions listed below are manual code changes the developer must apply.
| Diagnostic ID | Severity | Category | Description | Manual Resolution |
|---|---|---|---|---|
| PAG002 | Warning | Usage | Missing OrderBy before ToPagedListAsync causing non-deterministic results |
Add .OrderBy(x => x.Id) before calling ToPagedListAsync |
| PAG003 | Warning | Usage | Unexpected OrderBy before ToCursorPagedListAsync |
Remove OrderBy and use .Keyset().Ascending() |
| PAG004 | Warning | Usage | Calling OrderBy before Keyset() causes duplicate SQL ORDER BY clauses |
Remove preceding OrderBy calls |
| PAG005 | Info | Performance | Runtime cursor reflection detected in Native AOT project | Install EricksonLopez.Pagination.SourceGenerators |
| PAG006 | Warning | Security | ToCursorPagedListAsync called β verify that HmacCursorEncoder is configured in AddPagination() (suppress if already configured) |
Configure HmacCursorEncoder with a secret key in AddPagination() or suppress with #pragma warning disable PAG006 |
| PAG007 | Warning | Performance | KeysetBuilder<T> chain has more than 5 columns |
Consolidate composite keys or use a single unique tiebreaker column |
| PAG008 | Warning | Security | Explicit instantiation of insecure Base64CursorEncoder |
Replace with HmacCursorEncoder |
π§ͺ Testing & Quality
Multi-Paradigm Testing Architecture
The test suite enforces mathematical correctness, concurrency invariants, and cryptographic guarantees across all layers:
- Unit & Integration Tests: xUnit with AwesomeAssertions fluent assertions.
- Property-Based Testing: FsCheck.Xunit verifying mathematical invariants across cursor encoding, AST parsing, and boundary conditions.
- Component Testing: bUnit verifying Blazor
<PagedListPager>DOM rendering and interaction. - Realistic Integration Testing: Testcontainers (PostgreSQL 16, SQL Server 2022, MongoDB 7) running real database engines.
Code Coverage & Exclusions
Code coverage is collected in Cobertura format via Coverlet and uploaded to Codecov:
- Minimum coverage target: $\ge 98%$.
- Exclusions configured in
.runsettingsandDirectory.Build.propsexclude compiler-generated code, Roslyn source generator assemblies, and regex static caches.
# Run test suite with full coverage collection locally
dotnet test --no-build -c Release --collect:"XPlat Code Coverage" --settings .runsettings
Mutation Testing with Stryker.NET
Code robustness is validated with Stryker.NET mutation testing across the entire solution:
# Restore local tools and execute mutation testing
dotnet tool restore
dotnet stryker
| Threshold | Score Target | Policy |
|---|---|---|
| High (Target) | 100% | Exceptional test assertion depth |
| Low (Warning) | 98% | Requires review of uncovered mutation survivors |
| Break (Gate) | 95% | Fails CI build pipeline; blocks pull request merge |
β‘ Performance Benchmarks
Environment: AMD Ryzen 7 9800X3D (8C/8T @ 4.70 GHz), Windows 11 25H2, .NET 10.0.10 Release, BenchmarkDotNet v0.15.8
1. Deep Pagination Scaling (1,000,000 Rows in PostgreSQL 16)
Fetching PageSize = 100 at depths of Page 1, Page 100, and Page 10,000:
| Method | Target Page (Rows Skipped) | Mean Latency | Median Latency | Allocated | Speedup vs Offset |
|---|---|---|---|---|---|
| Offset Baseline | Page 1 (0 rows) | 17.07 ms | 15.21 ms | 46.61 KB | 1.0x (Baseline) |
| Keyset (EricksonLopez) | Page 1 (0 rows) | 8.85 ms | 7.18 ms | 54.61 KB | 2.1x faster |
| Offset Baseline | Page 100 (9,900 rows) | 13.68 ms | 13.16 ms | 47.55 KB | 1.0x (Baseline) |
| Keyset (EricksonLopez) | Page 100 (9,900 rows) | 2.34 ms | 1.88 ms | 57.05 KB | 7.0x faster |
| Offset Baseline | Page 10,000 (999,900 rows) | 93.76 ms | 82.02 ms | 47.39 KB | 1.0x (Baseline) |
| Keyset (EricksonLopez) | Page 10,000 (999,900 rows) | 12.03 ms | 9.72 ms | 52.61 KB | 8.4x faster |
Note: Keyset measurements include end-to-end HMAC-SHA256 signature verification, constant-time validation, query compilation, entity materialization, and cursor token generation.
2. Keyset Scalability up to 10 Million Rows
Benchmarking keyset seeks across database datasets scaling up to 10,000,000 rows in PostgreSQL:
| Benchmark Operation | Dataset Depth (Rows Skipped) | Mean Latency | Median Latency | Allocated | Asymptotic Scaling |
|---|---|---|---|---|---|
Keyset_Page1 |
0 | 3.75 ms | 2.12 ms | 48.57 KB | $O(\log N)$ |
Keyset_Depth_100K |
100,000 | 2.05 ms | 1.76 ms | 21.43 KB | Flat $O(\log N)$ |
Keyset_Depth_1M |
1,000,000 | 2.23 ms | 1.70 ms | 21.39 KB | Flat $O(\log N)$ |
Keyset_Depth_10M |
10,000,000 | 1.69 ms | 1.61 ms | 21.57 KB | Flat $O(\log N)$ |
Offset_Depth_100K |
100,000 | 5.94 ms | 5.44 ms | 18.75 KB | $O(N)$ Degraded |
Offset_Depth_1M |
1,000,000 | 6.04 ms | 5.52 ms | 18.69 KB | $O(N)$ Degraded |
3. Cryptographic Cursor Codec Benchmark (< 0.4 ΞΌs)
In-memory microbenchmark isolating cursor signing, decoding, and tamper detection:
| Operation | Unsecured Base64 | HMAC-SHA256 Signed | HMAC + TTL Expiration | Allocated | Security Guarantee |
|---|---|---|---|---|---|
| Single-Column Encode | 327.1 ns | 259.9 ns | 347.9 ns | 448β520 B | π HMAC-SHA256 Authenticated |
| Single-Column Decode | 445.0 ns | 364.6 ns | 394.6 ns | 208β304 B | π Signature & TTL Verified |
| Multi-Column Encode | 347.0 ns | 272.9 ns | 358.1 ns | 680 B | π Multi-Column Authenticated |
| Multi-Column Decode | 441.9 ns | 379.7 ns | 402.3 ns | 416 B | π Multi-Column Verified |
4. Dynamic Filter AST Compilation & Cache Benchmark
Measuring FilterExpression.Build<T>() parsing DSL strings (name~=phone,price>=100):
| Method | Mean Latency | Gen0 / 1k Ops | Allocated | Speedup |
|---|---|---|---|---|
| Cached Execution (Warm) | 32.64 ns | 0.0035 | 176 B | 25.5x faster |
| First Compilation (Cold) | 833.34 ns | 0.0381 | 2,064 B | Baseline |
5. Provider Comparison vs Alternatives
| Feature / Metric | EricksonLopez.Pagination | MR.EFCore.Keyset | Gridify | X.PagedList | Sieve (Abandoned) |
|---|---|---|---|---|---|
| Keyset (Cursor) Pagination | β Native | β Native | β None | β None | β None |
| Cryptographic HMAC Cursors | β Native | β None | β None | β None | β None |
| TTL & Replay Attack Defense | β Native | β None | β None | β None | β None |
| Composite Keyset Keys | β Up to 16 cols | β Up to 16 cols | β None | β None | β None |
| Count-less Offset ($N+1$) | β Native | β None | β None | β None | β None |
| Dynamic Filter/Sort DSL | β Native AST | β None | β Native | β None | β String |
| Dapper / Raw SQL Keyset | β Native | β None | β None | β None | β None |
| Native AOT & Trimming | β Full (SrcGen) | β οΈ Partial | β οΈ Partial | β None | β None |
| Blazor Component | β Native | β None | β None | β HTML/Tag | β None |
| Roslyn Analyzers | β PAG002βPAG008 | β None | β None | β None | β None |
π Compatibility & Technical Matrix
Target Frameworks & Native AOT Compatibility
| Package | .NET 8.0 LTS | .NET 9.0 STS | .NET 10.0 | netstandard2.0 | Native AOT | Trimmable |
|---|---|---|---|---|---|---|
EricksonLopez.Pagination.Abstractions |
β | β | β | β | β Fully AOT | β |
EricksonLopez.Pagination (Core) |
β | β | β | β | β Fully AOT | β |
EricksonLopez.Pagination.EntityFrameworkCore |
β | β | β | β | β οΈ With SrcGen | β |
EricksonLopez.Pagination.AspNetCore |
β | β | β | β | β Fully AOT | β |
EricksonLopez.Pagination.Dapper |
β | β | β | β | β οΈ Direct SQL | β οΈ |
EricksonLopez.Pagination.LinqToDB |
β | β | β | β | β Reflection | β |
EricksonLopez.Pagination.MongoDB |
β | β | β | β | β οΈ Direct Driver | β οΈ |
EricksonLopez.Pagination.Cosmos |
β | β | β | β | β οΈ Direct Driver | β οΈ |
EricksonLopez.Pagination.Elasticsearch |
β | β | β | β | β οΈ Direct Driver | β οΈ |
EricksonLopez.Pagination.Redis |
β | β | β | β | β Fully AOT | β |
EricksonLopez.Pagination.Relay |
β | β | β | β | β Fully AOT | β |
EricksonLopez.Pagination.Result |
β | β | β | β | β Fully AOT | β |
EricksonLopez.Pagination.Blazor |
β | β | β | β | β Fully AOT | β |
EricksonLopez.Pagination.Grpc |
β | β | β | β | β Fully AOT | β |
EricksonLopez.Pagination.OpenApi |
β | β | β | β | β οΈ Reflection | β οΈ |
EricksonLopez.Pagination.SourceGenerators |
β | β | β | β | N/A (Build-time) | N/A |
EricksonLopez.Pagination.Analyzers |
β | β | β | β | N/A (Build-time) | N/A |
Database Dialects & Storage Provider Support
| Storage Engine | Offset Pagination | Keyset (Cursor) Pagination | Approximate Count | Multi-Column Keyset | Keyset Streaming |
|---|---|---|---|---|---|
| PostgreSQL 14+ | β
OFFSET LIMIT |
β B-Tree Index Seek | β
pg_class stats |
β Up to 16 cols | β
IAsyncEnumerable |
| SQL Server 2016+ | β
OFFSET FETCH |
β Clustered Index Seek | β
sys.partitions |
β Up to 16 cols | β
IAsyncEnumerable |
| MySQL 8.0+ / MariaDB | β
LIMIT OFFSET |
β Index Seek | β Fallback exact | β Up to 16 cols | β
IAsyncEnumerable |
| SQLite 3.30+ | β
LIMIT OFFSET |
β B-Tree Index Seek | β Fallback exact | β Up to 16 cols | β
IAsyncEnumerable |
| Oracle 12c+ | β
OFFSET ROWS |
β Index Seek | β Fallback exact | β Up to 16 cols | β
IAsyncEnumerable |
| MongoDB 6.0+ | β
Skip().Limit() |
β
ObjectId / Field Seek |
β Exact count | β Composite Sort | β
IAsyncEnumerable |
| Azure Cosmos DB | β Continuation | β Continuation Tokens | β Exact count | β Single Token | β Change Feed |
| Elasticsearch 8.x | β
from / size |
β
search_after |
β Approximate hits | β Multi-field Sort | β Scroll / Search |
ποΈ Architecture & Design Principles
Execution Pipeline Architecture
flowchart TD
Client["HTTP / gRPC / UI Client"] --> Filter["PaginationEndpointFilter\n(AddPaginationValidation)"]
Filter --> Binder["Model Binders\n(PaginationParameters / CursorParameters)"]
Binder --> DSL["Dynamic Filter & Sort DSL\n(ApplyFilter / ApplySort)"]
DSL --> ProviderChoice{"Select Data Provider"}
ProviderChoice -->|EF Core| EFCorePipeline["KeysetBuilder<T> / IQueryable\n(Index-Seek Bounding)"]
ProviderChoice -->|Dapper| DapperPipeline["DapperKeysetBuilder<T>\n(Parameterized SQL Dialect)"]
ProviderChoice -->|MongoDB| MongoPipeline["IFindFluent<T>\n(ObjectId Cursor Seek)"]
ProviderChoice -->|Cosmos / Elastic| OtherPipeline["Continuation / search_after"]
EFCorePipeline --> Storage[("Database Storage Engine\n(PostgreSQL, SQL Server, MySQL, SQLite)")]
DapperPipeline --> Storage
MongoPipeline --> Storage
OtherPipeline --> Storage
Storage --> Materialize["Result Materialization\n(CountedPagedList / CursorPagedList)"]
Materialize --> Codec["ICursorEncoder\n(HMAC-SHA256 Signing + TTL + FNV-1a)"]
Codec --> ResponseFormat["Response Transformation\n(ToPagedResponse / ToRelayConnection / ETag 304)"]
ResponseFormat --> Client
Keyset Index Seek vs. Offset Scan Mechanism
flowchart LR
subgraph OffsetMechanism["Traditional OFFSET O(N) Degradation"]
direction TB
O_Req["OFFSET 999900 LIMIT 100"] --> O_Root["1. Locate Index Root"]
O_Root --> O_Scan["2. Scan & Evaluate 999,900 Rows"]
O_Scan --> O_Discard["β οΈ DISCARD 999,900 Rows\n(Buffer Pool Churn & MVCC Overhead)"]
O_Discard --> O_Fetch["3. Materialize Next 100 Rows"]
end
subgraph KeysetMechanism["Keyset B-Tree Index Seek O(log N)"]
direction TB
K_Req["WHERE (CreatedAt, Id) > (@c1, @c2) LIMIT 100"] --> K_Root["1. Locate Index Root"]
K_Root --> K_Seek["2. B-Tree Seek Direct to Target Tuple\n(O(log N) Steps)"]
K_Seek --> K_Fetch["3. Read Consecutive 100 Index Leaf Nodes\n(Total Touched: exactly 100!)"]
end
Cryptographic Cursor Lifecycle & State Machine
stateDiagram-v2
[*] --> InboundToken: Client passes ?after=<cursor>
InboundToken --> Base64UrlSplit: Split payload and signature components
Base64UrlSplit --> HmacVerify: Compute HMAC-SHA256 with SecretKey
HmacVerify --> ConstantTimeCheck: CryptographicOperations.FixedTimeEquals()
ConstantTimeCheck --> InvalidCursor: Signatures mismatch
InvalidCursor --> [*]: Throw InvalidPaginationCursorException (HTTP 400)
ConstantTimeCheck --> TTLVerification: Signature valid
TTLVerification --> ExpiredCursor: Timestamp > TTL + ClockSkew
ExpiredCursor --> [*]: Throw ExpiredPaginationCursorException (HTTP 410)
TTLVerification --> ReplayVerification: Timestamp within TTL
ReplayVerification --> ReplayCheck: ICursorReplayStore.TryAcquireNonceAsync(nonce, ttl)
ReplayCheck --> ReplayedCursor: Nonce already consumed
ReplayedCursor --> [*]: Throw ReplayedPaginationCursorException (HTTP 409)
ReplayCheck --> SchemaVerification: Nonce acquired
SchemaVerification --> SchemaMismatch: FNV-1a Fingerprint != Current Keyset Schema
SchemaMismatch --> [*]: Throw InvalidPaginationCursorException (Schema evolved)
SchemaVerification --> ExecuteQuery: Schema valid
ExecuteQuery --> EmitNewCursor: Query executed; Generate NextCursor token
EmitNewCursor --> [*]: HTTP 200 OK Response
π‘οΈ Best Practices & Anti-Patterns
| Scenario | β Avoid | β Recommended |
|---|---|---|
| Large Datasets (> 100K rows) | Using OFFSET / FETCH for deep pages |
Using Keyset pagination (.Keyset().Ascending()) for flat $O(\log N)$ seeks |
| Keyset Determinism | Omitting a unique tiebreaker column in keyset definitions | Always appending a strictly unique column (e.g. .Ascending(x => x.Id)) as the final keyset key |
| Cursor Security | Using plain Base64 cursor encoders in public APIs | Configuring HmacCursorEncoder with a minimum 32-byte secret key and TTL |
| Index Alignment | Using keyset pagination on unindexed database columns | Creating composite B-Tree database indexes matching keyset column order and directions |
| Hot-Path Allocations | Capturing local variables in lambda closures inside hot loops | Using static lambdas and TState state parameters |
| Total Count Overhead | Executing COUNT(*) on high-frequency list feeds |
Using ToPagedListWithoutCountAsync() ($N+1$ lookahead probing) |
| Batch Processing & ETL | Loading entire tables into memory with ToListAsync() |
Using AsKeysetStreamAsync() or ToPagedListBatchedAsync() with $O(1)$ memory |
| Kubernetes Deployments | Using InMemoryCursorReplayStore across multi-pod clusters |
Using EricksonLopez.Pagination.Redis (RedisCursorReplayStore) |
| Native AOT Publishing | Relying on runtime reflection for cursor decoding | Adding EricksonLopez.Pagination.SourceGenerators for compile-time decoders |
β οΈ Troubleshooting & Common Pitfalls
Applying keyset pagination without an underlying database index will force the database storage engine to perform a full table scan, eliminating all performance benefits. Ensure composite indexes match the keyset column definitions exactly.
1. Duplicate or Skipped Records Across Keyset Pages
- Symptom: Items appear duplicated or missing as the client traverses pages.
- Root Cause: The keyset ordering lacks a unique tiebreaker. When sorting on non-unique fields (e.g.
CreatedAtorPrice), identical values create ambiguous seek boundaries. - Solution: Always append a unique identifier (e.g.
Id) as the last column in your keyset chain:.Ascending(p => p.CreatedAt).Ascending(p => p.Id). AnalyzersPAG002andPAG004validate sorting invariants.
2. ExpiredPaginationCursorException (HTTP 410 Gone)
- Symptom: Client requests fail with
ExpiredPaginationCursorException. - Root Cause: The cursor's timestamp exceeds the configured
timeToLivethreshold. - Solution: Catch
ExpiredPaginationCursorExceptionin your endpoint or global exception handler and return RFC 7807 ProblemDetails with HTTP 410 Gone, instructing the client to restart pagination.
3. ReplayedPaginationCursorException in Clustered Environments
- Symptom: Cursors pass on one node but trigger false replay conflicts on other pods.
- Root Cause: Using
InMemoryCursorReplayStorein a multi-pod Kubernetes deployment. - Solution: Install
EricksonLopez.Pagination.Redisand registerAddPaginationRedisReplayStore()inProgram.csto share the nonce store across all pods.
4. InvalidPaginationCursorException After Deployment
- Symptom: Active users receive
InvalidPaginationCursorExceptionimmediately following a rolling deployment. - Root Cause: Keyset sort columns were altered in code. The FNV-1a schema fingerprint detected a mismatch between the cursor token and the new schema definition.
- Solution: Catch
InvalidPaginationCursorExceptionand return HTTP 400 Bad Request prompting the client to refresh its feed.
5. Excessive Keyset Columns Warning (PAG007)
- Symptom: Compiler emits warning
PAG007: KeysetBuilder has too many columns. - Root Cause: Defining more than 5 sort columns generates deeply nested
WHEREpredicates that overwhelm query optimizers on databases lacking row-value syntax. - Solution: Restrict keyset sorting to 1β3 business columns followed by a single unique tiebreaker.
π Part of the EricksonLopez Ecosystem
- π§± EricksonLopez.SharedKernel β Foundational Domain Primitives, Specifications, and Domain Events for enterprise .NET.
- β‘ EricksonLopez.Result β High-performance, struct-based Result Pattern and Railway-Oriented Programming ecosystem.
- π EricksonLopez.Specification β Composable, AOT-first Specification Pattern for LINQ and query execution.
- π’ EricksonLopez.MultiTenancy β Multi-tenant isolation, tenant resolution, and PostgreSQL Row-Level Security (RLS).
- π οΈ EricksonLopez.SqlBuilder β Zero-allocation AST-based SQL query builder for high-performance micro-ORMs.
π€ Contributing
Contributions are welcome! Follow these steps to set up your local development environment:
Prerequisites
- .NET 8.0, .NET 9.0, and .NET 10.0 SDKs
- Docker Desktop (required for Testcontainers integration tests)
dotnet-strykerlocal tool (installed viadotnet tool restore)
Build and Test
# 1. Clone the repository
git clone https://github.com/ericksonlopezf/dotnet-pagination.git
cd dotnet-pagination
# 2. Restore local tools and dependencies
dotnet tool restore
dotnet restore EricksonLopez.Pagination.slnx
# 3. Build solution with code style and analyzer enforcement
dotnet build EricksonLopez.Pagination.slnx -c Release
# 4. Run test suite with Coverlet code coverage
dotnet test EricksonLopez.Pagination.slnx -c Release --settings .runsettings
# 5. Run Stryker mutation testing
dotnet stryker
Please review our Contributing Guidelines, Code of Conduct, Security Policy, Governance Model, and Roadmap before submitting a pull request.
π License
Distributed under the MIT License. Copyright Β© 2026 Erickson Lopez.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. 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. |
-
net10.0
- EricksonLopez.Pagination.Abstractions (>= 0.0.0-alpha.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Options (>= 8.0.2)
- StackExchange.Redis (>= 2.8.24)
-
net8.0
- EricksonLopez.Pagination.Abstractions (>= 0.0.0-alpha.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Options (>= 8.0.2)
- StackExchange.Redis (>= 2.8.24)
-
net9.0
- EricksonLopez.Pagination.Abstractions (>= 0.0.0-alpha.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Options (>= 8.0.2)
- StackExchange.Redis (>= 2.8.24)
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.0.0-alpha.0 | 58 | 8/25/2026 |