Skip to content

Querying

Mirko Da Corte edited this page Aug 21, 2026 · 6 revisions

How to read many documents from a repository — LINQ queries, pagination, projections, streaming cursors, and how to keep large scans cheap. For single-document reads see CRUD operations.

LINQ queries

QueryElementsAsync hands you an IQueryable<TModel> translated to a MongoDB aggregation, which you terminate with a driver async operator (from Etherna.MongoDB.Driver.Linq):

using Etherna.MongoDB.Driver.Linq;

var cutoff = DateTime.UtcNow.AddYears(-2);
var adults = await db.Cats.QueryElementsAsync(cats => cats
    .Where(c => c.Birthday <= cutoff)
    .OrderBy(c => c.Name)
    .ToListAsync());

var count  = await db.Cats.QueryElementsAsync(cats => cats.LongCountAsync());
var oldest = await db.Cats.QueryElementsAsync(cats =>
    cats.OrderBy(c => c.Birthday).FirstOrDefaultAsync());

The query runs server-side; only the result is materialized. Pass AggregateOptions as the second argument when you need them (e.g. AllowDiskUse). The provider is the driver's LINQ3 provider.

Projections

Shape the result inside the query to fetch only what you need:

var cutoff = DateTime.UtcNow.AddYears(-2);
var names = await db.Cats.QueryElementsAsync(cats => cats
    .Where(c => c.Birthday <= cutoff)
    .Select(c => new { c.Id, c.Name })
    .ToListAsync());

Pagination

QueryPaginatedElementsAsync filters, orders, and pages in one call, returning a PaginatedEnumerable<TResult>:

var cutoff = DateTime.UtcNow.AddYears(-2);
PaginatedEnumerable<Cat> page = await db.Cats.QueryPaginatedElementsAsync(
    filter: cats => cats.Where(c => c.Birthday <= cutoff),
    orderKeySelector: c => c.Name,
    page: 0,
    take: 20,
    useDescendingOrder: false);

foreach (var cat in page.Elements) { /* ... */ }
PaginatedEnumerable<T> Meaning
Elements The page's items.
CurrentPage Zero-based current page.
MaxPage Index of the last page.
PageSize Items per page (take).
TotalElements Total matching documents.

A negative page, a page size below 1, and a page so far in the sequence that the elements to skip exceed int.MaxValue are refused with an ArgumentOutOfRangeException, before any query runs.

Deep pages cost. Paging by number skips the elements of every page before the requested one, and the server walks them to discard them — with the right index too. It is the right shape for a UI showing page numbers, on the first pages; for scanning a whole collection use a streaming read, and for an endless list prefer filtering from the last key you read (Where(c => c.Name.CompareTo(lastName) > 0)) over a growing page number.

Streaming large result sets

To stream rather than buffer, FindAsync returns a driver IAsyncCursor<TProjection> — wrapped so the db execution context stays alive until you dispose it:

using var cursor = await db.Cats.FindAsync<Cat>(Builders<Cat>.Filter.Empty);
while (await cursor.MoveNextAsync())
    foreach (var cat in cursor.Current) { /* ... */ }

Large scans: turn off the cache

Every entity a query materializes normally enters the scope's identity map (and is change-tracked). Over a big scan that's wasted memory that grows with the result set. Wrap read-only scans in the no-cache serializer modifier so materialized models skip the identity map and change tracking:

using (db.Engine.SerializerModifierAccessor.EnableCacheSerializerModifier(noCache: true))
{
    await db.Cats.QueryElementsAsync(async cats =>
    {
        await foreach (var cat in cats.ToAsyncEnumerable())
            Process(cat);
        return 0;   // QueryElementsAsync requires a result
    });
}

You can also inject ISerializerModifierAccessor directly. The modifier applies to everything read inside the using scope, on the current execution flow — so on a background thread with no ambient context, open one first (see Execution contexts).

Best practice. Any job that cursors over a whole collection should read under no-cache — see Best practices and pitfalls.

The companion EnableReferenceSerializerModifier(readOnlyId: true) materializes references with only their id loaded — the denormalized summary members are discarded on read. Reading any other member still triggers a lazy full load, so use it when the ids alone are what you need.


Next: Change tracking and saving for write internals, or Transactions to group writes.

Clone this wiki locally