BrandGhost
MongoDB Reads in C#: Projection, Sorting, and Pagination
All articles

MongoDB Reads in C#: Projection, Sorting, and Pagination

Learn MongoDB reads in C# with typed projections, unique sort tie-breakers, bounded results, and pagination boundaries without repeating basic filter tutorials.

MongoDB reads in C# involve more than selecting matching documents. An application also needs the right result shape, a defined order, and a bounded amount of data. A filter can be correct while the endpoint still returns unnecessary fields or presents inconsistent page boundaries.

This guide explains projection, sorting, and pagination for a C# application developer who already understands a basic filter. It does not reproduce the filter-builder tutorial. Instead, it follows the next questions: what should come back, in which order, and how does a later request continue?

Current-information check: October 10, 2026. The official driver site links to API 3.12.0, the baseline used here. Examples were reviewed against documentation and versioned source, not run. Check the current query documentation before adapting them to a different package or server version.

Separate selection from shaping in MongoDB reads in C#

A filter answers which documents qualify. A projection describes the returned data. A sort defines order, and a positive limit bounds the returned count. These are separate parts of the query contract even though the fluent API lets you write them in one chain.

For the selection part, reuse the existing MongoDB filtering guide. This article assumes a known customer predicate and focuses on the surrounding read behavior.

The find guide distinguishes creating the query from obtaining a cursor or materializing a list. A query object is not evidence that documents were read. ToListAsync and ToCursorAsync are examples of terminal operations that perform the requested retrieval.

That distinction matters in explanations and in eventual verification. Printing a filter or constructing a fluent chain can show the requested shape. It cannot establish the server's returned documents, selected plan, or elapsed time.

Use typed projection for a smaller read shape

A collection's document type need not be the output type of every read. The 3.12.0 fluent-query source defines an expression-based Project overload returning IFindFluent<TDocument, TNewProjection>. It wraps the expression in FindExpressionProjectionDefinition.

For a simple field selection, an anonymous type makes the changed output shape visible. The following complete console example uses an explicitly mapped immutable stored record and projects only the fields needed by a summary.

using System;
using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.Driver;

BsonClassMap.RegisterClassMap<Order>(map =>
{
    map.AutoMap();
    map.MapIdMember(order => order.Id);
    map.MapMember(order => order.CustomerId).SetElementName("customerId");
    map.MapMember(order => order.Sequence).SetElementName("sequence");
    map.MapMember(order => order.TotalCents).SetElementName("totalCents");
    map.MapCreator(order => new Order(
        order.Id, order.CustomerId, order.Sequence, order.TotalCents));
});

var uri = Environment.GetEnvironmentVariable("MONGODB_URI")
    ?? throw new InvalidOperationException("MONGODB_URI is required.");

using var client = new MongoClient(uri);
var orders = client.GetDatabase("application")
    .GetCollection<Order>("orders");
var filter = Builders<Order>.Filter.Eq(order => order.CustomerId, "customer-17");

var summaries = await orders.Find(filter)
    .SortByDescending(order => order.Sequence)
    .ThenByDescending(order => order.Id)
    .Limit(20)
    .Project(order => new { order.Id, order.Sequence, order.TotalCents })
    .ToListAsync();

foreach (var summary in summaries)
{
    Console.WriteLine($"{summary.Id}: {summary.TotalCents}");
}

public sealed record Order(
    ObjectId Id, string CustomerId, long Sequence, long TotalCents);

The program assumes the collection already contains documents with the mapped fields and compatible types. It does not insert sample data, show a fabricated result, or imply that the example has been exercised.

The query retains Id and Sequence because they define the ordering boundary. Removing fields just because they are not displayed can make continuation harder if the next request needs them.

A simple field projection is intentionally different from projecting an arbitrary application method call. The presence of an expression overload does not mean every C# expression can be translated. Review the supported operation and the resulting BSON rather than treating expression syntax as unlimited server-side execution.

Do not turn partial reads into full writes

A projection is useful because the reader needs less than the stored document. That same property makes a projected result a poor accidental replacement model. Fields that were not requested should not be assumed to survive a write built from the partial object.

Keep a summary, a complete stored model, and an update request conceptually separate even when they share members. The existing update tutorial addresses operation-specific changes; projection is a read concern.

Projection also does not automatically prove index coverage. A server may still fetch stored documents to produce the result. Coverage is an execution-plan question requiring evidence, not a guarantee attached to a small anonymous type.

An anonymous typed projection is not required-field validation. In the default 3.12 immutable mapping, absent projected members can use CLR defaults, such as zero for a numeric value. Test missing fields separately from incompatible BSON values; explicit record creator maps do not imply the same default policy.

The official index guide demonstrates covered-query shapes, but those examples do not establish coverage for your collection and query. Inspect the actual plan for the actual command and deployed indexes.

Give repeated sort values a tie-breaker

The server sort documentation says MongoDB does not store documents in a particular collection order and that equal sort keys do not guarantee a consistent relative order. It recommends including a unique field when consistent ordering is desired.

For an unsharded ordinary collection in these examples, _id supplies the unique tie-breaker. Sequence is an application-defined long value representing the desired primary order; it is not assumed to be globally unique or equivalent to insertion time.

Sorting only by Sequence would leave documents with the same sequence unresolved. Sorting by descending Sequence and descending Id defines which of those tied documents comes first. The next-page predicate must use the same direction for both parts.

The result is deterministic ordering for an unchanged dataset. It is not a snapshot guarantee across separate requests. If sort fields change between reads, documents can move relative to the earlier boundary even though every individual query has a fully defined order.

The same distinction appears in filtering, sorting, and pagination design. That article provides related application concerns; MongoDB's exact sort behavior still comes from its server documentation.

Understand offset pagination

Offset pagination describes a result window using skipped rows and a limit. It is easy to explain to a user interface that displays page numbers. Its limitation is that a later offset does not mean the server can jump directly to the corresponding position without processing earlier results.

The server pagination guidance describes range queries as a way to avoid scanning unwanted documents as offsets grow. That is a documented tendency, not a measured latency claim for the example collection.

This separate complete console program illustrates a bounded offset read. It uses BSON documents so the example does not repeat the typed-mapping setup:

using System;
using MongoDB.Bson;
using MongoDB.Driver;

var uri = Environment.GetEnvironmentVariable("MONGODB_URI")
    ?? throw new InvalidOperationException("MONGODB_URI is required.");

using var client = new MongoClient(uri);
var orders = client.GetDatabase("application")
    .GetCollection<BsonDocument>("orders");

const int pageNumber = 3;
const int pageSize = 20;
var offset = checked((pageNumber - 1) * pageSize);
var filter = Builders<BsonDocument>.Filter.Eq("customerId", "customer-17");
var sort = Builders<BsonDocument>.Sort.Descending("sequence")
    .Descending("_id");
var projection = Builders<BsonDocument>.Projection
    .Include("_id")
    .Include("sequence")
    .Include("totalCents");

var page = await orders.Find(filter)
    .Sort(sort)
    .Skip(offset)
    .Limit(pageSize)
    .Project(projection)
    .ToListAsync();

foreach (var document in page)
{
    Console.WriteLine(document.ToJson());
}

The fixed values are illustrative page inputs, not universal endpoint limits. An application accepting external values should validate positive page numbers, strictly positive page sizes within an application-defined maximum, and arithmetic boundaries. A zero limit means no limit, not an empty page. The checked calculation makes overflow explicit instead of silently producing a wrong offset.

Offset pages can also shift when documents are inserted or removed before the requested window. A unique sort resolves ties but does not make those separate reads a single immutable view. Decide whether page-number navigation or continuation behavior matches the reader's task.

Continue from a compound boundary

For descending (Sequence, Id) order, documents after a boundary have a lower sequence, or the same sequence and a lower ID. That is a lexicographic rule. It is not enough to compare only one field when the sort has two components.

The following complete program uses that boundary against BSON documents. It uses the result of an initial page rather than inventing a cursor unrelated to the chosen filter and sort:

using System;
using System.Linq;
using MongoDB.Bson;
using MongoDB.Driver;

var uri = Environment.GetEnvironmentVariable("MONGODB_URI")
    ?? throw new InvalidOperationException("MONGODB_URI is required.");

using var client = new MongoClient(uri);
var orders = client.GetDatabase("application")
    .GetCollection<BsonDocument>("orders");
var filters = Builders<BsonDocument>.Filter;
var customerFilter = filters.Eq("customerId", "customer-17");
var sort = Builders<BsonDocument>.Sort.Descending("sequence")
    .Descending("_id");

var firstPage = await orders.Find(customerFilter)
    .Sort(sort)
    .Limit(20)
    .ToListAsync();

if (firstPage.Count > 0)
{
    var last = firstPage.Last();
    var sequence = last["sequence"].AsInt64;
    var id = last["_id"].AsObjectId;
    var after = filters.Or(
        filters.Lt("sequence", sequence),
        filters.And(
            filters.Eq("sequence", sequence),
            filters.Lt("_id", id)));

    var nextPage = await orders.Find(filters.And(customerFilter, after))
        .Sort(sort)
        .Limit(20)
        .ToListAsync();

    foreach (var document in nextPage)
    {
        Console.WriteLine(document.ToJson());
    }
}

The example deliberately assumes sequence is stored as BSON Int64 and _id as ObjectId. It does not silently coerce incompatible data. A typed production model should establish those representations and reject incompatible contracts explicitly.

The range-pagination documentation supplies the underlying less-than continuation idea. The compound rule here is the application-level extension required by the two-field descending order. It still needs runtime verification with ties, multiple pages, and the application's data.

This design avoids an offset, but it does not guarantee the fastest query for every workload. Appropriate indexes, the equality predicate, data distribution, and sort behavior still matter. Query-plan evidence is the next layer, not an implied property of a continuation token.

Treat a cursor as part of the query contract

A client-facing continuation token should preserve the exact values used for the boundary, including their types. It should also belong to the query shape that created it. A token from one customer filter should not silently continue a different customer's results.

Sort direction matters as well. A boundary from descending order cannot be reused unchanged for ascending order. If the API supports multiple sort modes, the continuation representation needs enough context to identify the intended mode.

The token is not permission to access data. Application authorization and customer restrictions still apply on every request. Do not replace the base customer predicate with the continuation predicate; combine them, as the example does.

This article does not implement token encoding or signing. Those are separate application-boundary concerns. The educational point is the logical contract: a continuation is meaningful only relative to a filter, a sort, and the stored value representations.

Bound MongoDB reads in C# without overstating efficiency

A positive limit bounds how many documents are returned, not necessarily how many keys or documents the server examines. Likewise, a batch size is not the same thing as a total-result limit. The find guide describes cursor batching and list materialization as separate behavior.

For a small API page, materializing a list provides a clear finite response. For a larger processing task, cursor iteration may avoid holding the entire result in memory at once. Neither choice substitutes for an explicit workload boundary.

The published MongoDB Testcontainers article remains the setup companion for eventual database-backed checks. No server, Docker container, build, or test was run while preparing this article.

Describe the page before evaluating it

Before a future verification run, describe the expected page in application language. A customer's initial page should contain only that customer's documents. Its ordering should resolve ties. Its summary should include the boundary fields needed to continue. A later page should use the same base predicate and sort, with the additional boundary condition.

That description is useful because a nonempty list is weak evidence. It does not show that another customer's document was excluded or that equal sequence values were handled correctly. A fixture containing only one matching document cannot expose most pagination mistakes.

Include several tied primary values in an eventual fixture and enough matching documents to cross a page boundary. Keep the dataset unchanged while first checking the ordering rule, then discuss concurrent changes as a separate scenario. That separates the deterministic query contract from questions about changing data.

The same separation applies to projection. A summary should be checked for the fields it promises, not merely for a reduced serialized byte count. No byte count was measured here, and no smaller-response percentage is claimed. The contract is about which data is requested; performance evidence belongs to a defined measurement.

Finally, an empty next page is not automatically an error. It may simply mean the initial page contained the remaining matches. A continuation design should distinguish the absence of further results from malformed input or a failed database operation rather than treating all three as the same empty response.

MongoDB reads in C# should also have a clearly described stopping point. A page request is finite. A background export may continue through many batches. Giving both operations the same unbounded list-shaped implementation hides their different responsibilities.

For MongoDB reads in C#, a finite page can be explained in terms of the predicate, sort, projection, and limit presented here. A larger processing task needs an additional discussion about delivery and cancellation. That discussion should not be silently folded into a claim that asynchronous retrieval solves every resource concern.

Frequently asked questions

These answers summarize result shaping and continuation, not a guarantee about every changing dataset.

What does projection change in MongoDB reads?

Projection changes the returned shape. It can expose selected fields without returning the complete stored document. It does not update storage or prove that the query is covered by an index.

Can a typed collection return a different type?

The pinned fluent API provides an expression-based projection with a distinct output type. Simple field selections are illustrated here. Arbitrary application expressions are not automatically guaranteed to translate.

Why add Id after the main sort field?

The primary sort field may contain duplicates. A unique tie-breaker resolves their relative ordering. The continuation predicate must reflect both fields and their directions.

Is offset pagination always wrong?

No. It is a clear model for page-number navigation. Its documented scanning behavior and shifting windows are limitations to understand, not proof that every offset query is unsuitable.

Does keyset pagination freeze the result set?

No. It continues from a boundary in a defined order. Changes to documents or sort values between separate requests can affect the later page. Snapshot requirements need their own design.

Were the page examples verified against a server?

No. They are source-reviewed illustrations. Actual results, translation, tie handling in a deployed dataset, and execution plans remain unexercised.

Understand the read contract

MongoDB reads in C# become clearer when selection, shape, order, and continuation are explained independently. A projection serves the reader's data needs. A unique sort resolves ties. A page boundary defines how another request proceeds.

None of those choices alone establishes performance or consistency under concurrent changes. Keeping their guarantees precise provides a useful foundation for the next investigation: how the server executes the particular query.

Get the Dev Leader Newsletter

C#, .NET, and software engineering content every Saturday.

Subscribe to the newsletter
An error has occurred. This application may no longer respond until reloaded. Reload