SDK Query Pagination and LINQ
Use LINQ queries, FeedIterator pagination, MaxItemCount, continuation tokens, and session tokens for efficient data retrieval with the Cosmos DB .NET SDK.
Beyond raw SQL
Imagine reading a very long book. You donβt read all 500 pages at once β you use a bookmark. In Cosmos DB, a continuation token is your bookmark. You read a page of results, get a bookmark, and next time you pick up right where you left off.
LINQ is like having a translator β you write C# code, and it gets converted to Cosmos DB SQL automatically. Not everything translates, but the common stuff works great.
LINQ queries
Basic LINQ
using Microsoft.Azure.Cosmos.Linq;
IOrderedQueryable<TaskItem> queryable = container
.GetItemLinqQueryable<TaskItem>();
// Build the LINQ query
var linqQuery = queryable
.Where(t => t.TenantId == "tenant-abc" && t.Type == "task")
.Where(t => t.Status == "active")
.OrderByDescending(t => t.CreatedAt)
.Take(20);
// Convert to FeedIterator for execution
using FeedIterator<TaskItem> feed = linqQuery.ToFeedIterator();
List<TaskItem> results = new();
while (feed.HasMoreResults)
{
FeedResponse<TaskItem> response = await feed.ReadNextAsync();
results.AddRange(response);
Console.WriteLine($"Page RU: {response.RequestCharge}");
}
LINQ translation table
| C# LINQ | Cosmos DB SQL |
|---|---|
.Where(t => t.Status == "active") | WHERE c.status = 'active' |
.OrderBy(t => t.CreatedAt) | ORDER BY c.createdAt ASC |
.OrderByDescending(t => t.CreatedAt) | ORDER BY c.createdAt DESC |
.Take(10) | TOP 10 |
.Select(t => new { t.Id, t.Title }) | SELECT c.id, c.title |
.Count() | SELECT VALUE COUNT(1) |
.Where(t => t.Tags.Contains("urgent")) | WHERE ARRAY_CONTAINS(c.tags, 'urgent') |
.Where(t => t.Name.StartsWith("Web")) | WHERE STARTSWITH(c.name, 'Web') |
Not all LINQ translates. Complex expressions, custom methods, or unsupported functions throw a runtime exception during translation. Test your LINQ queries during development.
FeedIterator pagination
Cosmos DB returns results in pages. Use FeedIterator to process them:
var query = new QueryDefinition(
"SELECT * FROM c WHERE c.tenantId = @tenant AND c.type = 'task'")
.WithParameter("@tenant", "tenant-abc");
var options = new QueryRequestOptions
{
MaxItemCount = 25, // max items per page
PartitionKey = new PartitionKey("tenant-abc")
};
using FeedIterator<TaskItem> feed = container.GetItemQueryIterator<TaskItem>(
query, requestOptions: options);
while (feed.HasMoreResults)
{
FeedResponse<TaskItem> page = await feed.ReadNextAsync();
foreach (TaskItem task in page)
{
Console.WriteLine($" {task.Title}");
}
Console.WriteLine($"--- Page: {page.Count} items, {page.RequestCharge} RU ---");
}
MaxItemCount is a maximum, not exact. A page may return fewer items if:
- The 4 MB response size limit is reached
- The query runs out of RU budget for that page
- There are simply fewer items remaining
Continuation tokens
For stateless pagination (e.g., API endpoints with βnext pageβ links), use continuation tokens:
// First page β no continuation token
string? continuationToken = null;
var options = new QueryRequestOptions { MaxItemCount = 10 };
using FeedIterator<TaskItem> feed = container.GetItemQueryIterator<TaskItem>(
query,
continuationToken: continuationToken,
requestOptions: options);
if (feed.HasMoreResults)
{
FeedResponse<TaskItem> page = await feed.ReadNextAsync();
// Save the continuation token for the next request
continuationToken = page.ContinuationToken;
// Return to client: { items: [...], nextToken: "eyJ..." }
}
// Subsequent pages β pass the token back
using FeedIterator<TaskItem> nextFeed = container.GetItemQueryIterator<TaskItem>(
query,
continuationToken: continuationToken, // resume from where we left off
requestOptions: options);
Key properties of continuation tokens:
- Opaque: Donβt parse or modify them β theyβre an internal format that can change between SDK versions
- Stateless: The server doesnβt store session state; the token contains all resume information
- Expiration: They donβt expire, but they become invalid if the containerβs partition layout changes significantly
Exam tip: continuation tokens are opaque
Never parse, modify, or depend on the internal structure of continuation tokens. Theyβre opaque strings that the SDK uses internally. The format can change between SDK versions without notice. Just pass them back to GetItemQueryIterator to resume. The exam may present options that suggest parsing the token β always reject those.
Session tokens
Session tokens enable read-your-writes consistency in session consistency level (the default):
// Write an item β capture the session token
ItemResponse<TaskItem> writeResponse = await container.CreateItemAsync(task, pk);
string sessionToken = writeResponse.Headers.Session;
// Read with the session token β guaranteed to see the write
var readOptions = new ItemRequestOptions
{
SessionToken = sessionToken
};
ItemResponse<TaskItem> readResponse = await container.ReadItemAsync<TaskItem>(
task.Id, pk, readOptions);
// This read is guaranteed to reflect the write above
How session tokens work:
- Every write returns a session token in the response headers
- The SDK automatically tracks session tokens internally
- If youβre using a single CosmosClient instance (singleton), session consistency works automatically
- If you have multiple clients or services, you need to pass session tokens explicitly
Exam tip: session tokens across services
Session consistency guarantees read-your-writes within the same session. A single CosmosClient automatically maintains the session. But if Service A writes and Service B reads (different CosmosClient instances), Service B may not see the write immediately. To fix this, pass the session token from Service Aβs write response to Service Bβs read request. The exam tests this multi-service scenario.
Putting it all together
π Priyaβs task list API uses pagination:
[HttpGet("tasks")]
public async Task<ActionResult> GetTasks(
[FromQuery] string tenantId,
[FromQuery] string? pageToken = null)
{
var query = new QueryDefinition(
"SELECT * FROM c WHERE c.tenantId = @tenant AND c.type = 'task' ORDER BY c.createdAt DESC")
.WithParameter("@tenant", tenantId);
var options = new QueryRequestOptions
{
MaxItemCount = 20,
PartitionKey = new PartitionKey(tenantId)
};
using FeedIterator<TaskItem> feed = _container.GetItemQueryIterator<TaskItem>(
query, continuationToken: pageToken, requestOptions: options);
FeedResponse<TaskItem> page = await feed.ReadNextAsync();
return Ok(new
{
items = page.ToList(),
nextPageToken = page.ContinuationToken,
ruCharge = page.RequestCharge
});
}
π¬ Video walkthrough
π¬ Video coming soon
SDK Query Pagination β DP-420 Module 9
SDK Query Pagination β DP-420 Module 9
~14 minFlashcards
Knowledge check
Priya's API returns 20 tasks per page using MaxItemCount = 20. A request returns only 12 items with a non-null continuation token. Why?
Service A writes a new task and returns the session token. Service B (separate CosmosClient) reads the same task but gets a 404. What's the fix?
Ravi writes a LINQ query using a custom extension method: .Where(t => MyCustomHelper(t.Name)). What happens?
Next up: Server-Side Programming β stored procedures, UDFs, and triggers that run JavaScript directly inside the Cosmos DB engine.