Amazon DynamoDB · RetrieveMultiple

Page through results

Page through a partition with paging cookies, in three scenarios: the first page, the next pages from the cookie, and a page number, which DynamoDB can't jump to.

Set up for this page

The scenarios build on the sample application: SampleContext.cs (the dbContext every sample is given); SampleOutput.cs (Step and Show, which print); SampleContext.Application.cs (dbContext.Service, the application's ARX service); SampleModel.cs (the DynamoDB layout and the schema catalog); SampleContext.Direct.cs (dbContext.Direct); DynamoDirectHelper.cs (the checks behind dbContext.Direct).

dbContext.Direct is the samples' own checking code on the plain driver, not part of ARX: leave it out of your application.

Before scenario 1 the sample runs this, so the service and the rows the scenarios use are there:

Set up
var orders = Enumerable.Range(1, 12).Select(i =>
{
    var order = new ArxEntity("sales_order");
    order["customer_id"] = "C-D421";
    order["order_number"] = $"SO-D42{i:00}";
    order["status"] = "OPEN";
    order["ordered_on"] = "2026-11-08";
    return order;
}).ToList();
await service.CreateMultipleAsync(orders);

1 The first page

Five orders, and the cookie for the rest.

Already in scope: service (ArxDynamoDbService)

C#
var page = await service.RetrieveMultipleAsync(Query(new ArxPagingInfo { Count = 5 }));

Console.WriteLine($"{string.Join(", ", page.Entities.Select(x => x["order_number"]))}; more: {page.HasNextPage}");

return page;
Used above

Defined once in this sample.

private static ArxQueryExpression Query(ArxPagingInfo page)
{
    var query = new ArxQueryExpression("sales_order");
    query.ColumnSet.AddColumns("order_number");
    query.Criteria.AddCondition("customer_id", ArxConditionOperator.Equal, "C-D421");
    query.PageInfo = page;
    return query;
}

Check it in the database

PartiQL

A PartiQL statement: run it in NoSQL Workbench, or with aws dynamodb execute-statement --statement, after the code has run.

SELECT "order_number" FROM "crm" WHERE "PK" = 'CUSTOMER#C-D421' AND begins_with("SK", 'ORDER#')

Expect: 12 items; the first page holds SO-D4201 to SO-D4205.

2 The next pages

Each page from the last one's cookie, until there is none.

Already in scope: service (ArxDynamoDbService), page (ArxEntityCollection)

C#
while (page.PagingCookie != null)
{
    page = await service.RetrieveMultipleAsync(Query(new ArxPagingInfo { Count = 5, PagingCookie = page.PagingCookie }));
    Console.WriteLine($"{string.Join(", ", page.Entities.Select(x => x["order_number"]))}; more: {page.HasNextPage}");
}
Used above

Defined once in this sample.

private static ArxQueryExpression Query(ArxPagingInfo page)
{
    var query = new ArxQueryExpression("sales_order");
    query.ColumnSet.AddColumns("order_number");
    query.Criteria.AddCondition("customer_id", ArxConditionOperator.Equal, "C-D421");
    query.PageInfo = page;
    return query;
}

Check it in the database

PartiQL

A PartiQL statement: run it in NoSQL Workbench, or with aws dynamodb execute-statement --statement, after the code has run.

SELECT "order_number" FROM "crm" WHERE "PK" = 'CUSTOMER#C-D421' AND "SK" > 'ORDER#SO-D4205'

Expect: 7 items, SO-D4206 to SO-D4212.

3 A page number

Page 3 straight away: ARX8001. Page with cookies.

Already in scope: service (ArxDynamoDbService)

C#
try
{
    await service.RetrieveMultipleAsync(Query(new ArxPagingInfo { Count = 5, PageNumber = 3 }));
}
catch (ArxNotSupportedException exception)
{
    Console.WriteLine(exception.Message);
}
Used above

Defined once in this sample.

private static ArxQueryExpression Query(ArxPagingInfo page)
{
    var query = new ArxQueryExpression("sales_order");
    query.ColumnSet.AddColumns("order_number");
    query.Criteria.AddCondition("customer_id", ArxConditionOperator.Equal, "C-D421");
    query.PageInfo = page;
    return query;
}

Check it in the database

PartiQL

A PartiQL statement: run it in NoSQL Workbench, or with aws dynamodb execute-statement --statement, after the code has run.

SELECT "order_number" FROM "crm" WHERE "PK" = 'CUSTOMER#C-D421' AND begins_with("SK", 'ORDER#')

Expect: 12 items (PartiQL pages by its own NextToken, never by number).

Good to know

A page continues from where the last one ended: pass back the PagingCookie the page returned, with the same query. HasNextPage is exact, and the cookie is null on the last page. DynamoDB can't count or skip, so a page number above 1 and a total count are ARX8001.

Run the complete sample

The code above comes from one program, built in a fresh project and run against the database. To run it yourself, here is every file and step.

The program, its files and how to run it

Run it locally

  1. .NET SDK: .NET SDK 10.0.100 or later 10.0.x (target net10.0).
  2. Create the project:
    Terminal
    dotnet new console -n ArxSample -f net10.0
    cd ArxSample
  3. Install the packages:
    Terminal
    dotnet add package ARX.SDK.DynamoDb --version 1.0.0-rc.1
  4. Database: a Amazon DynamoDB, or DynamoDB Local for development (tested with DynamoDB Local 2.5.2) database with the sample schema.
  5. Configuration: set ARX_SAMPLES_DYNAMODB to its connection string. Nothing is hard-coded.
  6. Source code: add the files of the sample application this sample uses (SampleContext.cs, SampleOutput.cs, SampleContext.Application.cs, SampleModel.cs, SampleContext.Direct.cs, DynamoDirectHelper.cs), then replace Program.cs and add the other file below, with the file names shown.
  7. Run it:
    Terminal
    dotnet run
  8. Expected output: below.

The files

Program.cs

Runs the sample. Generated for this page; it reads the connection string from the environment and first remakes the sample database from sample-schema.json.

using ArxSamples.DynamoDb;
using ArxSamples.DynamoDb.Operations.RetrieveMultiple.Basics;

// The connection string comes from the environment, never from the code (see "Configure the connection").
var connectionString = Environment.GetEnvironmentVariable("ARX_SAMPLES_DYNAMODB")
    ?? throw new InvalidOperationException("Set the ARX_SAMPLES_DYNAMODB environment variable to your Amazon DynamoDB connection string.");

var dbContext = new SampleContext(connectionString);

// Every run starts from the sample data: this deletes and makes again what sample-schema.json lists
// (save the file in the project folder, see Get started).
await dbContext.Direct.ResetAsync();

await new Basic2_PageThroughResults().RunAsync(dbContext);
Basic2_PageThroughResults.cs

The sample, as it is in the repository.

using ARX.SDK;
using ARX.SDK.DynamoDb.Services;
using ARX.SDK.Query;
using ARX.SDK.Services;

using static ArxSamples.DynamoDb.SampleOutput;

namespace ArxSamples.DynamoDb.Operations.RetrieveMultiple.Basics;

/// <summary>
/// Page through a partition with paging cookies, in three scenarios: the first page, the next pages from the cookie, and
/// a page number, which DynamoDB can't jump to.
/// </summary>
/// <remarks>
/// A page continues from where the last one ended: pass back the <c>PagingCookie</c> the page returned, with the same
/// query. <c>HasNextPage</c> is exact, and the cookie is null on the last page. DynamoDB can't count or skip, so a page
/// number above 1 and a total count are ARX8001.
/// </remarks>
public sealed class Basic2_PageThroughResults : ISample
{
    public string Name => "retrieve-multiple-basics-2-page-through-results";

    public string Title => "Page through results";

    public async Task RunAsync(SampleContext dbContext)
    {
        var orders = Enumerable.Range(1, 12).Select(i =>
        {
            var order = new ArxEntity("sales_order");
            order["customer_id"] = "C-D421";
            order["order_number"] = $"SO-D42{i:00}";
            order["status"] = "OPEN";
            order["ordered_on"] = "2026-11-08";
            return order;
        }).ToList();
        await dbContext.Service.CreateMultipleAsync(orders);

        var first = await Scenario1_TheFirstPageAsync(dbContext.Service);

        await Scenario2_TheNextPagesAsync(dbContext.Service, first);

        await Scenario3_APageNumberAsync(dbContext.Service);
    }

    private static ArxQueryExpression Query(ArxPagingInfo page)
    {
        var query = new ArxQueryExpression("sales_order");
        query.ColumnSet.AddColumns("order_number");
        query.Criteria.AddCondition("customer_id", ArxConditionOperator.Equal, "C-D421");
        query.PageInfo = page;
        return query;
    }

    /// <summary>1. Five orders, and the cookie for the rest.</summary>
    private static async Task<ArxEntityCollection> Scenario1_TheFirstPageAsync(ArxDynamoDbService service)
    {
        Step("1. The first page");

        var page = await service.RetrieveMultipleAsync(Query(new ArxPagingInfo { Count = 5 }));

        Show($"{string.Join(", ", page.Entities.Select(x => x["order_number"]))}; more: {page.HasNextPage}");

        /* ===========================================================
         * [DIRECT QUERY CHECK]
         * -----------------------------------------------------------
         * SELECT "order_number" FROM "crm" WHERE "PK" = 'CUSTOMER#C-D421' AND begins_with("SK", 'ORDER#')
         * -- Expect: 12 items; the first page holds SO-D4201 to SO-D4205.
         * ===========================================================
         */
        return page;
    }

    /// <summary>2. Each page from the last one's cookie, until there is none.</summary>
    private static async Task Scenario2_TheNextPagesAsync(ArxDynamoDbService service, ArxEntityCollection page)
    {
        Step("2. The next pages");

        while (page.PagingCookie != null)
        {
            page = await service.RetrieveMultipleAsync(Query(new ArxPagingInfo { Count = 5, PagingCookie = page.PagingCookie }));
            Show($"{string.Join(", ", page.Entities.Select(x => x["order_number"]))}; more: {page.HasNextPage}");
        }

        /* ===========================================================
         * [DIRECT QUERY CHECK]
         * -----------------------------------------------------------
         * SELECT "order_number" FROM "crm" WHERE "PK" = 'CUSTOMER#C-D421' AND "SK" > 'ORDER#SO-D4205'
         * -- Expect: 7 items, SO-D4206 to SO-D4212.
         * ===========================================================
         */
    }

    /// <summary>3. Page 3 straight away: ARX8001. Page with cookies.</summary>
    private static async Task Scenario3_APageNumberAsync(ArxDynamoDbService service)
    {
        Step("3. A page number");

        try
        {
            await service.RetrieveMultipleAsync(Query(new ArxPagingInfo { Count = 5, PageNumber = 3 }));
        }
        catch (ArxNotSupportedException exception)
        {
            ShowError(exception);
        }

        /* ===========================================================
         * [DIRECT QUERY CHECK]
         * -----------------------------------------------------------
         * SELECT "order_number" FROM "crm" WHERE "PK" = 'CUSTOMER#C-D421' AND begins_with("SK", 'ORDER#')
         * -- Expect: 12 items (PartiQL pages by its own NextToken, never by number).
         * ===========================================================
         */
    }
}

Expected output

What dotnet run printed when the sample was verified, on a freshly created sample schema. Ids differ when the tables already hold rows.

Output
  > 1. The first page
      SO-D4201, SO-D4202, SO-D4203, SO-D4204, SO-D4205; more: True
  > 2. The next pages
      SO-D4206, SO-D4207, SO-D4208, SO-D4209, SO-D4210; more: True
      SO-D4211, SO-D4212; more: False
  > 3. A page number
      caught ArxNotSupportedException: ARX8001 NotSupported: DynamoDB can't jump to page 3: it reads on from where the last page ended. Pass the previous page's PagingCookie (PageNumber 1).