EricksonLopez.Pagination.Redis 0.0.0-alpha.0

This is a prerelease version of EricksonLopez.Pagination.Redis.
dotnet add package EricksonLopez.Pagination.Redis --version 0.0.0-alpha.0
                    
NuGet\Install-Package EricksonLopez.Pagination.Redis -Version 0.0.0-alpha.0
                    
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="EricksonLopez.Pagination.Redis" Version="0.0.0-alpha.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="EricksonLopez.Pagination.Redis" Version="0.0.0-alpha.0" />
                    
Directory.Packages.props
<PackageReference Include="EricksonLopez.Pagination.Redis" />
                    
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 EricksonLopez.Pagination.Redis --version 0.0.0-alpha.0
                    
#r "nuget: EricksonLopez.Pagination.Redis, 0.0.0-alpha.0"
                    
#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 EricksonLopez.Pagination.Redis@0.0.0-alpha.0
                    
#: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=EricksonLopez.Pagination.Redis&version=0.0.0-alpha.0&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=EricksonLopez.Pagination.Redis&version=0.0.0-alpha.0&prerelease
                    
Install as a Cake Tool

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.

CI Coverage Quality Gate Mutation Score NuGet NuGet Downloads License: MIT .NET NativeAOT


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

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:

  1. 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.
  2. 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.
  3. 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 HasNextPage by fetching PageSize + 1 records, completely eliminating COUNT(*) 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.Metrics counters 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 NuGet Core implementations, PagedList<T>, CursorPagedList<T>, encoders, and DI configuration
EricksonLopez.Pagination.Abstractions NuGet Zero-dependency contracts, IPagedList<T>, ICursorPagedList<T>, parameters, and attributes
EricksonLopez.Pagination.EntityFrameworkCore NuGet EF Core IQueryable<T> extensions, KeysetBuilder<T>, and dynamic filter/sort DSL
EricksonLopez.Pagination.Dapper NuGet High-performance raw SQL offset & keyset pagination for IDbConnection
EricksonLopez.Pagination.LinqToDB NuGet LinqToDB LINQ provider offset and keyset cursor pagination extensions
EricksonLopez.Pagination.MongoDB NuGet MongoDB official driver IFindFluent<T> offset and ObjectId cursor extensions
EricksonLopez.Pagination.Cosmos NuGet Azure Cosmos DB continuation token forward cursor pagination extensions
EricksonLopez.Pagination.Elasticsearch NuGet Elasticsearch 8.x client search_after cursor pagination and mapping
EricksonLopez.Pagination.Redis NuGet Distributed cursor nonce replay store backed by StackExchange.Redis
EricksonLopez.Pagination.Relay NuGet GraphQL Relay Cursor Connections specification (Connection<T>, Edge<T>, PageInfo)
EricksonLopez.Pagination.Result NuGet Railway-Oriented Programming integration with EricksonLopez.Result
EricksonLopez.Pagination.AspNetCore NuGet Minimal API / MVC model binders, validation filters, RFC 7232 ETags, and response envelopes
EricksonLopez.Pagination.Blazor NuGet Headless <PagedListPager> Razor component with Bootstrap preset and templating
EricksonLopez.Pagination.Grpc NuGet Google Protobuf message converters and gRPC pagination contracts
EricksonLopez.Pagination.OpenApi NuGet Swagger / OpenAPI schema transformers and parameter operation filters
EricksonLopez.Pagination.SourceGenerators NuGet Roslyn source generator emitting compile-time Native AOT cursor decoders
EricksonLopez.Pagination.Analyzers NuGet 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

πŸŽ“ Interactive Showcase & Recipes


πŸ“₯ 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 parameter Action<PaginationAspNetCoreOptions> for ASP.NET Core-specific settings. The primary setting is ModelBinderProviderInsertIndex, 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 .runsettings and Directory.Build.props exclude 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. CreatedAt or Price), 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). Analyzers PAG002 and PAG004 validate sorting invariants.

2. ExpiredPaginationCursorException (HTTP 410 Gone)

  • Symptom: Client requests fail with ExpiredPaginationCursorException.
  • Root Cause: The cursor's timestamp exceeds the configured timeToLive threshold.
  • Solution: Catch ExpiredPaginationCursorException in 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 InMemoryCursorReplayStore in a multi-pod Kubernetes deployment.
  • Solution: Install EricksonLopez.Pagination.Redis and register AddPaginationRedisReplayStore() in Program.cs to share the nonce store across all pods.

4. InvalidPaginationCursorException After Deployment

  • Symptom: Active users receive InvalidPaginationCursorException immediately 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 InvalidPaginationCursorException and 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 WHERE predicates 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

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 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. 
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.0.0-alpha.0 58 8/25/2026