Skip to content

Cosmos: ToPageAsync silently returns full result set with null continuation token when query contains ORDER BY (against the Cosmos Emulator vnext-preview) #38163

@jhulbertpmn

Description

@jhulbertpmn

Cosmos: ToPageAsync silently returns full result set with null continuation token when query contains ORDER BY (against the Cosmos Emulator)

Summary

When ToPageAsync(pageSize, continuationToken) is called on a Cosmos-backed query
that includes an OrderBy(...) / OrderByDescending(...) clause, the call
returns the entire result set (capped only by the partition size) with a
null continuation token, instead of returning at most pageSize items with a
resumable token.

Removing the ORDER BY from the same query makes ToPageAsync behave as
documented (returns pageSize items + a non-null continuation token).

The base query is fully translatable — query.Take(pageSize).ToListAsync() on
the same IQueryable returns the requested number of rows correctly.

The failure is silent: no exception, no warning, no log entry. Callers see a
single page that contains far more rows than they asked for, then conclude the
result is exhausted because the continuation token is null.

This was reproduced against the mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview
image. I have not yet verified against production Cosmos DB.

Versions

Component Version
EF Core 10.0.0 (Microsoft.EntityFrameworkCore.Cosmos)
Microsoft.Azure.Cosmos (transitive) 3.51.0
.NET 10.0
Cosmos Emulator mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview
Host macOS 26.4 / arm64 (Apple Silicon)

Repro

Full self-contained repro: https://gist.github.com/jhulbertpmn/d367ee88ca4967d1802f7cea99e46a83

using Microsoft.EntityFrameworkCore;

#pragma warning disable EF9102

await using var ctx = new ReproContext();
await ctx.Database.EnsureCreatedAsync();

// Seed 500 docs across 10 partition keys (50 per partition).
if (!await ctx.Items.AnyAsync())
{
    var baseTime = DateTime.UtcNow.AddDays(-30);
    for (var i = 0; i < 500; i++)
    {
        ctx.Items.Add(new Item
        {
            Id = Guid.NewGuid(),
            GroupId = Guid.Parse($"00000000-0000-0000-0000-{i % 10:D12}"),
            Name = $"Item-{i:D4}",
            CreationTime = baseTime.AddMinutes(i)
        });
    }
    await ctx.SaveChangesAsync();
}

const int PageSize = 10;
var query = ctx.Items.AsNoTracking().OrderByDescending(x => x.CreationTime);

var take = await query.Take(PageSize).ToListAsync();
var page = await query.ToPageAsync(PageSize, continuationToken: null);

Console.WriteLine($"Take(10).ToListAsync():     {take.Count} items");
Console.WriteLine($"ToPageAsync(10, null):      {page.Values.Count} items, continuationToken={(page.ContinuationToken is null ? "null" : "<set>")}");

internal sealed class Item
{
    public Guid Id { get; set; }
    public Guid GroupId { get; set; }
    public string Name { get; set; } = string.Empty;
    public DateTime CreationTime { get; set; }
}

internal sealed class ReproContext : DbContext
{
    public DbSet<Item> Items => Set<Item>();

    protected override void OnConfiguring(DbContextOptionsBuilder o)
    {
        o.UseCosmos(
            "http://localhost:18081",
            "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==",
            "topage-repro",
            cosmos =>
            {
                cosmos.HttpClientFactory(() => new HttpClient(new HttpClientHandler
                {
                    ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
                }));
                cosmos.ConnectionMode(Microsoft.Azure.Cosmos.ConnectionMode.Gateway);
                cosmos.LimitToEndpoint(); // emulator advertises an unreachable internal URL
            });
    }

    protected override void OnModelCreating(ModelBuilder b) =>
        b.Entity<Item>(e =>
        {
            e.ToContainer("Items");
            e.HasNoDiscriminator();
            e.HasKey(x => x.Id);
            e.Property(x => x.Id).ToJsonProperty("id");
            e.HasPartitionKey(x => x.GroupId);
        });
}

Run the emulator first:

docker run -d --name cosmos-repro -p 18081:8081 \
  mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview

Expected behavior

ToPageAsync(10, null) returns at most 10 items and a non-null continuation
token (since 500 > 10).

Actual behavior

ToPageAsync(10, null) returns 500 items (the entire container) and
continuationToken == null.

Trigger isolation

Five permutations of the same data set, all with pageSize = 10:

# Query Result Continuation token
A cross-partition OrderBy(CreationTime).Take(10).ToListAsync() 10 ✅ n/a
B cross-partition OrderBy(CreationTime).ToPageAsync(10, null) 500 ❌ null
C Where(GroupId == X).OrderBy(CreationTime).ToPageAsync(10, null) 50 ❌ null
D cross-partition (no ORDER BY) ToPageAsync(10, null) 10 ✅ set
E Where(GroupId == X) (no ORDER BY) ToPageAsync(10, null) 10 ✅ set

The presence of OrderBy / OrderByDescending is the precise trigger. The
partition-key shape is incidental: the bug fires equally on single- and
cross-partition variants of the ordered query.

Generated SQL (case B)

SELECT VALUE c
FROM root c
ORDER BY c["CreationTime"] DESC

The query is plainly translatable, and query.Take(10).ToListAsync() on the
identical IQueryable returns 10 rows, so the issue is specific to
ToPageAsync's page-sizing behavior — not to query translation.

Real-world impact

We hit this in a Blazor Server app using EF Core's ToPageAsync to drive a
Telerik grid backed by a Cosmos container of ~500 documents. Symptom in
production-shaped UI: grid pager shows page size 10, but the OnRead callback
receives 500 items, the grid renders an empty page (the slice for "page 1 of 10
items" lands outside the actual returned data), and total-count display says
"1–501 of about 501 items". 5-second hang per refresh as the full container is
materialized and shipped over SignalR.

We worked around it by detecting the local/emulator environment and falling
back to Skip().Take() instead of ToPageAsync.

Notes

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions