Xpandables.AspNetCore.AsyncPaged 10.0.2

dotnet add package Xpandables.AspNetCore.AsyncPaged --version 10.0.2
                    
NuGet\Install-Package Xpandables.AspNetCore.AsyncPaged -Version 10.0.2
                    
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="Xpandables.AspNetCore.AsyncPaged" Version="10.0.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Xpandables.AspNetCore.AsyncPaged" Version="10.0.2" />
                    
Directory.Packages.props
<PackageReference Include="Xpandables.AspNetCore.AsyncPaged" />
                    
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 Xpandables.AspNetCore.AsyncPaged --version 10.0.2
                    
#r "nuget: Xpandables.AspNetCore.AsyncPaged, 10.0.2"
                    
#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 Xpandables.AspNetCore.AsyncPaged@10.0.2
                    
#: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=Xpandables.AspNetCore.AsyncPaged&version=10.0.2
                    
Install as a Cake Addin
#tool nuget:?package=Xpandables.AspNetCore.AsyncPaged&version=10.0.2
                    
Install as a Cake Tool

🌐 AspNetCore.AsyncPaged

NuGet Version NuGet Downloads .NET License

ASP.NET Core Integration for Async Paged Enumerables — Stream paged JSON responses with automatic pagination metadata, minimal API filters, MVC output formatters, and efficient serialization.


📋 Overview

AspNetCore.AsyncPaged provides seamless ASP.NET Core integration for IAsyncPagedEnumerable<T>, enabling efficient streaming of paginated data directly to HTTP responses. The library automatically wraps paged data with pagination metadata, supports both minimal APIs and MVC controllers, and leverages System.Text.Json for high-performance JSON serialization.

Built for .NET 10 with C# 14 extension members, this package bridges the gap between your data layer and HTTP responses with zero boilerplate.

✨ Key Features

  • 🔄 IAsyncPagedEnumerable<T> to IResult — Direct conversion from paged enumerables to HTTP results
  • 🛡️ Endpoint Filters — Automatic transformation of paged responses via WithXAsyncPagedFilter()
  • 📝 MVC Output Formatter — Custom TextOutputFormatter for controller-based APIs
  • Streaming JSON Serialization — Memory-efficient serialization via PipeWriter and Stream
  • 🎯 Structured Response Format — Consistent { "pagination": {...}, "items": [...] } output
  • 🚀 AOT Compatible — Source-generated JSON serialization with PaginationJsonContext
  • 🔧 Flexible Configuration — Custom JsonSerializerOptions and JsonTypeInfo<T> support

📦 Installation

dotnet add package AspNetCore.AsyncPaged

Or via NuGet Package Manager:

Install-Package AspNetCore.AsyncPaged

🚀 Quick Start

Minimal API with Endpoint Filter

using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// Apply the async paged filter to automatically transform IAsyncPagedEnumerable responses
app.MapGet("/api/products", async (ProductService productService, CancellationToken ct) =>
{
    IAsyncPagedEnumerable<Product> products = productService.GetProductsAsync(pageNumber: 1, pageSize: 20);
    return products; // Filter transforms this to structured JSON response
})
.WithXAsyncPagedFilter();

app.Run();

Response Output:

{
  "pagination": {
    "totalCount": 150,
    "pageSize": 20,
    "currentPage": 1,
    "continuationToken": null
  },
  "items": [
    { "id": 1, "name": "Product A", "price": 29.99 },
    { "id": 2, "name": "Product B", "price": 49.99 }
  ]
}

Manual Result Conversion

app.MapGet("/api/orders", async (OrderService orderService, CancellationToken ct) =>
{
    IAsyncPagedEnumerable<Order> orders = orderService.GetOrdersAsync(pageNumber: 2, pageSize: 50);
    
    // Explicitly convert to IResult for full control
    return orders.ToResult();
});

With Custom JSON Options

app.MapGet("/api/customers", async (CustomerService customerService, CancellationToken ct) =>
{
    IAsyncPagedEnumerable<Customer> customers = customerService.GetCustomersAsync();
    
    var jsonOptions = new JsonSerializerOptions
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        WriteIndented = true
    };
    
    return customers.ToResult(jsonOptions);
});

🏗️ MVC Controller Support

Register MVC Options

using Microsoft.Extensions.DependencyInjection;

var builder = WebApplication.CreateBuilder(args);

// Add MVC with async paged output formatter
builder.Services.AddControllers();
builder.Services.AddXControllerAsyncPagedMvcOptions();

var app = builder.Build();
app.MapControllers();
app.Run();

Controller Implementation

using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class ProductsController(ProductService productService) : ControllerBase
{
    [HttpGet]
    public IAsyncPagedEnumerable<Product> GetProducts(
        [FromQuery] int page = 1,
        [FromQuery] int pageSize = 20)
    {
        // Return IAsyncPagedEnumerable directly - formatter handles serialization
        return productService.GetProductsAsync(page, pageSize);
    }

    [HttpGet("category/{categoryId}")]
    public IAsyncPagedEnumerable<Product> GetByCategory(
        int categoryId,
        [FromQuery] int page = 1,
        [FromQuery] int pageSize = 20)
    {
        return productService.GetByCategoryAsync(categoryId, page, pageSize);
    }
}

The ControllerResultAsyncPagedOutputFormatter automatically:

  • Detects IAsyncPagedEnumerable<T> return types
  • Streams JSON with pagination metadata wrapper
  • Supports UTF-8 and transcoding for other encodings
  • Handles cancellation gracefully

🔧 Extension Methods

IAsyncPagedEnumerable<T> Extensions

using Microsoft.AspNetCore.Http;

IAsyncPagedEnumerable<Product> products = GetProductsAsync();

// Convert to IResult (uses default JSON options from DI)
IResult result1 = products.ToResult();

// Convert with custom JsonSerializerOptions
IResult result2 = products.ToResult(new JsonSerializerOptions { WriteIndented = true });

// Convert with source-generated JsonTypeInfo for AOT
IResult result3 = products.ToResult(ProductJsonContext.Default.Product);

Endpoint Convention Builder Extensions

// Apply async paged filter to transform responses
app.MapGet("/api/items", GetItems)
    .WithXAsyncPagedFilter();

// Works with route groups
var apiGroup = app.MapGroup("/api")
    .WithXAsyncPagedFilter(); // Applied to all endpoints in group

apiGroup.MapGet("/products", GetProducts);
apiGroup.MapGet("/orders", GetOrders);

📊 JSON Response Structure

All paged responses follow a consistent structure:

{
  "pagination": {
    "totalCount": 500,
    "pageSize": 25,
    "currentPage": 2,
    "continuationToken": "offset:50"
  },
  "items": [
    // Array of serialized items
  ]
}

Pagination Properties

Property Type Description
totalCount int? Total items across all pages (null if unknown)
pageSize int Number of items per page
currentPage int Current page number (1-based)
continuationToken string? Token for cursor-based pagination

⚡ Streaming Serialization

The library uses efficient streaming serialization that:

  1. Computes pagination metadata first via GetPaginationAsync()
  2. Writes the opening structure ({ "pagination": {...}, "items": [)
  3. Streams items incrementally as they're enumerated
  4. Adaptive flushing based on dataset size and memory pressure

Flush Strategy

Dataset Size Batch Size Description
Unknown 100 items Default for streaming sources
< 1,000 200 items Small datasets - less frequent flushing
< 10,000 100 items Medium datasets
< 100,000 50 items Large datasets - more frequent flushing
≥ 100,000 25 items Very large datasets - maximum responsiveness

Additionally, flushing occurs when pending bytes exceed 32KB regardless of item count.


🔄 Integration with Data Layer

Entity Framework Core Example

public class ProductService(AppDbContext context)
{
    public IAsyncPagedEnumerable<Product> GetProductsAsync(
        int pageNumber = 1,
        int pageSize = 20)
    {
        return context.Products
            .Where(p => p.IsActive)
            .OrderBy(p => p.Name)
            .Skip((pageNumber - 1) * pageSize)
            .Take(pageSize)
            .ToAsyncPagedEnumerable(); // Automatically extracts pagination from Skip/Take
    }

    public IAsyncPagedEnumerable<Product> GetByCategoryAsync(
        int categoryId,
        int pageNumber = 1,
        int pageSize = 20)
    {
        var query = context.Products
            .Where(p => p.CategoryId == categoryId && p.IsActive)
            .OrderByDescending(p => p.CreatedAt);

        return query
            .Skip((pageNumber - 1) * pageSize)
            .Take(pageSize)
            .ToAsyncPagedEnumerable(async ct =>
            {
                // Custom pagination factory for accurate total count
                int total = await context.Products
                    .CountAsync(p => p.CategoryId == categoryId && p.IsActive, ct);
                
                return Pagination.Create(
                    pageSize: pageSize,
                    currentPage: pageNumber,
                    totalCount: total);
            });
    }
}

Cursor-Based Pagination Example

public class ActivityService(AppDbContext context)
{
    public IAsyncPagedEnumerable<Activity> GetActivitiesAsync(
        string? continuationToken = null,
        int pageSize = 50)
    {
        var query = context.Activities.AsQueryable();

        if (TryDecodeCursor(continuationToken, out DateTime cursor))
        {
            query = query.Where(a => a.Timestamp < cursor);
        }

        var items = query
            .OrderByDescending(a => a.Timestamp)
            .Take(pageSize);

        return items.ToAsyncPagedEnumerable(async ct =>
        {
            var activities = await items.ToListAsync(ct);
            string? nextToken = activities.Count == pageSize
                ? EncodeCursor(activities[^1].Timestamp)
                : null;

            return Pagination.Create(
                pageSize: pageSize,
                currentPage: 1,
                continuationToken: nextToken,
                totalCount: null); // Unknown for cursor-based
        });
    }

    private static bool TryDecodeCursor(string? token, out DateTime cursor)
    {
        cursor = default;
        if (string.IsNullOrEmpty(token)) return false;
        return DateTime.TryParse(token, out cursor);
    }

    private static string EncodeCursor(DateTime timestamp) => 
        timestamp.ToString("O");
}

🛡️ Endpoint Filter Details

The AsyncPagedEnpointFilter automatically:

  1. Intercepts endpoint results after handler execution
  2. Detects IAsyncPagedEnumerable types (including wrapped in ObjectResult)
  3. Creates ResultAsyncPaged<T> with proper generic type resolution
  4. Returns the structured result for serialization
// The filter transforms this:
app.MapGet("/api/data", () => GetDataAsync().ToAsyncPagedEnumerable());

// Into this effective behavior:
app.MapGet("/api/data", () => 
{
    var paged = GetDataAsync().ToAsyncPagedEnumerable();
    return new ResultAsyncPaged<DataItem>(paged);
});

📝 MVC Output Formatter Configuration

The ControllerAsyncPagedMvcOptions configures MVC with:

public void Configure(MvcOptions options)
{
    options.EnableEndpointRouting = false;
    options.RespectBrowserAcceptHeader = true;
    options.ReturnHttpNotAcceptable = true;

    // Insert formatter at position 0 for priority
    options.OutputFormatters.Insert(0, 
        new ControllerResultAsyncPagedOutputFormatter(jsonSerializerOptions));
}

Supported Media Types

  • application/json
  • text/json
  • application/*+json

Supported Encodings

  • UTF-8 (optimized path via PipeWriter)
  • Unicode (transcoding stream fallback)

✅ Best Practices

✅ Do

  • Use WithXAsyncPagedFilter() for minimal APIs returning IAsyncPagedEnumerable<T>
  • Register AddXControllerAsyncPagedMvcOptions() for MVC controller support
  • Provide custom pagination factories for accurate total counts with complex queries
  • Use source-generated JsonTypeInfo<T> for AOT-compatible serialization
  • Return IAsyncPagedEnumerable<T> directly from handlers - let the framework handle conversion

❌ Don't

  • Materialize entire collections before returning - leverage streaming
  • Ignore cancellation tokens - pass them through to data layer operations
  • Mix manual ToResult() with endpoint filter - choose one approach per endpoint
  • Forget to compute pagination - call GetPaginationAsync() before serialization

🔧 Advanced: Custom Result Implementation

For full control over serialization:

app.MapGet("/api/custom", async (DataService service, HttpContext context) =>
{
    var paged = service.GetDataAsync();
    var pagination = await paged.GetPaginationAsync(context.RequestAborted);
    
    // Access pagination for custom headers
    context.Response.Headers["X-Total-Count"] = pagination.TotalCount?.ToString();
    context.Response.Headers["X-Page-Size"] = pagination.PageSize.ToString();
    
    return paged.ToResult();
});

Package Description
System.AsyncPaged Core IAsyncPagedEnumerable<T> and Pagination types
System.AsyncPaged.Json JSON serialization extensions for paged enumerables
System.AsyncPaged.Linq LINQ operators (SelectManyPaged, transformations)

📄 License

Apache License 2.0 - Copyright © Kamersoft 2025

Contributions welcome at Xpandables.Net on GitHub.

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

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
10.0.2 79 3/15/2026
10.0.1 85 2/20/2026
10.0.0 104 1/9/2026