Paging
When a controller action returns IQueryable<T>, the query pipeline automatically applies server-side paging and sorting. This means you write a simple method that returns a queryable, and the framework handles the rest.
Why IQueryable matters
The key to automatic paging is returning IQueryable<T> instead of IEnumerable<T> or List<T>. When the pipeline sees an IQueryable, it can append .Skip() and .Take() before the database executes the query — so only the requested page of data travels over the wire.
If you return a materialized collection like List<T>, the pipeline has no way to apply paging at the database level. All rows are fetched first, defeating the purpose.
[Route("api/accounts")]
public class Accounts : Controller
{
readonly IMongoCollection<DebitAccount> _collection;
public Accounts(IMongoCollection<DebitAccount> collection) => _collection = collection;
// ✅ Returns IQueryable — paging and sorting are applied automatically
[HttpGet]
public IQueryable<DebitAccount> AllAccounts() => _collection.AsQueryable();
}
How it works
When a client sends paging parameters in the query string, the QueryableQueryRenderer intercepts the IQueryable result and:
- Counts the total number of matching items
- Applies sorting based on
sortbyandsortDirection - Applies
.Skip(page * pageSize)and.Take(pageSize) - Returns the page of data wrapped in a
QueryResultwith aPagingInfocontainingpage,size,totalItems, andtotalPages
The client controls paging with these query string parameters:
| Parameter | Type | Description |
|---|---|---|
page |
int |
Zero-based page number |
pageSize |
int |
Number of items per page |
sortby |
string |
Field name to sort by |
sortDirection |
asc or desc |
Sort direction |
Example requests
GET /api/accounts?page=0&pageSize=25
GET /api/accounts?page=2&pageSize=10&sortby=name&sortDirection=asc
When no paging parameters are provided, the full result set is returned without paging.
Complete example with filtering
Paging works alongside query arguments. The pipeline applies paging after your method returns the filtered IQueryable:
[Route("api/accounts")]
public class Accounts : Controller
{
readonly IMongoCollection<DebitAccount> _collection;
public Accounts(IMongoCollection<DebitAccount> collection) => _collection = collection;
[HttpGet]
public IQueryable<DebitAccount> AllAccounts() => _collection.AsQueryable();
[HttpGet("by-owner/{ownerId}")]
public IQueryable<DebitAccount> AccountsByOwner(CustomerId ownerId)
=> _collection.AsQueryable().Where(a => a.Owner == ownerId);
}
Both endpoints support paging automatically because they return IQueryable<T>.
Return type comparison
| Return type | Paging | Sorting | DB-level optimization |
|---|---|---|---|
IQueryable<T> |
✅ Automatic | ✅ Automatic | ✅ Skip/Take pushed to DB |
IEnumerable<T> |
❌ | ❌ | ❌ All rows loaded |
List<T> |
❌ | ❌ | ❌ All rows loaded |
T[] |
❌ | ❌ | ❌ All rows loaded |
Manual paging with IQueryContextManager
If you need full control over how paging is applied — for example, when using the MongoDB driver directly instead of LINQ — inject IQueryContextManager and read the paging context manually:
[Route("api/accounts")]
public class Accounts : Controller
{
readonly IMongoCollection<DebitAccount> _collection;
readonly IQueryContextManager _queryContextManager;
public Accounts(
IMongoCollection<DebitAccount> collection,
IQueryContextManager queryContextManager)
{
_collection = collection;
_queryContextManager = queryContextManager;
}
[HttpGet("manual")]
public QueryResult ManualPaging()
{
var context = _queryContextManager.Current;
var query = _collection.Find(_ => true);
if (context.Sorting != Sorting.None)
{
query = context.Sorting.Direction == SortDirection.Ascending
? query.SortBy(context.Sorting.Field)
: query.SortByDescending(context.Sorting.Field);
}
var totalItems = (int)query.CountDocuments();
if (context.Paging.IsPaged)
{
query = query.Skip(context.Paging.Skip).Limit(context.Paging.Size);
}
var data = query.ToList();
return new QueryResult
{
Data = data,
Paging = new PagingInfo(context.Paging.Page, context.Paging.Size, totalItems)
};
}
}
Manual paging is rarely needed. Prefer returning
IQueryable<T>and letting the pipeline handle it.