GraphQL Sorting¶
Verified by tests
SortConventionTests, OrderByStrippingMiddlewareTests, OrderByStrippingExpressionVisitorTests, QueryExecutionTests — library CI run #37346231411 (2026-10-05)
Whizbang's HotChocolate integration provides flexible sorting capabilities for PerspectiveRow<T> data.
Basic Sorting¶
Sort results using the order argument:
Sort Directions¶
ASC- Ascending (A-Z, 0-9, oldest first)DESC- Descending (Z-A, 9-0, newest first)
# Ascending
Sorting on Data Properties¶
Sort by any property in your read model:
# By string
# By string
{
products(order: { data: { name: ASC } }) {
nodes { ... }
}
}
# By number
{
products(order: { data: { price: DESC } }) {
nodes { ... }
}
}
# By date
{
orders(order: { data: { orderDate: DESC } }) {
nodes { ... }
}
}
Multi-Column Sorting¶
Sort by multiple columns using an array:
{
orders(order: [
{ data: { status: ASC } }
{ data: { totalAmount: DESC } }
]) {
nodes {
data {
status
totalAmount
customerName
}
}
}
}
This sorts by status ascending first, then by total amount descending within each status.
Sorting on System Fields¶
Sort by PerspectiveRow system fields:
# By ID
# By ID
{
orders(order: { id: ASC }) {
nodes { ... }
}
}
# By version (for optimistic concurrency)
{
orders(order: { version: DESC }) {
nodes { ... }
}
}
# By creation date
{
orders(order: { createdAt: DESC }) {
nodes { ... }
}
}
# By last update
{
orders(order: { updatedAt: DESC }) {
nodes { ... }
}
}
Sorting on Metadata¶
When metadata is exposed:
Combining Sort and Filter¶
Sort and filter work together:
{
orders(
where: { data: { status: { eq: "Completed" } } }
order: [
{ data: { totalAmount: DESC } }
{ createdAt: DESC }
]
) {
nodes {
data {
customerName
totalAmount
status
}
}
}
}
Sorting with Paging¶
Always combine sorting with paging for consistent results:
{
orders(
order: { createdAt: DESC }
first: 10
after: "cursor..."
) {
nodes {
data {
customerName
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
Sort with Variables¶
Use GraphQL variables for dynamic sorting:
query GetProducts($sortField: ProductSortInput!)
query GetProducts($sortField: ProductSortInput!) {
products(order: [$sortField]) {
nodes {
data {
name
price
}
}
}
}
Default Sort Order¶
If no order is specified, Whizbang does not apply any implicit ordering — rows come back in whatever order the database (or the lens's own pre-existing OrderBy, if it has one) produces. Cursor paging over an unordered set is not stable.
Best Practice: Always specify an explicit sort order for predictable results, especially when paging.
Sort Precedence with Pre-Existing OrderBy¶
When application code applies a default OrderBy before HotChocolate's sorting middleware runs, use [UseOrderByStripping] to ensure GraphQL sorting takes precedence:
Sort Precedence with Pre-Existing OrderBy
[UsePaging]
[UseFiltering]
[UseSorting]
[UseOrderByStripping] // Must be after UseSorting
public IQueryable<PerspectiveRow<Order>> GetOrders([Service] IOrderLens lens) {
return lens.Query; // May have pre-existing OrderBy from application code
}
How It Works¶
The [UseOrderByStripping] attribute:
- Strips pre-existing ordering when a GraphQL
orderargument is provided - Preserves original ordering when no GraphQL sorting is requested
- Ensures GraphQL sort is primary - prevents HotChocolate from using
ThenByinstead ofOrderBy
When to Use¶
Use [UseOrderByStripping] when:
- Your lens query applies a default
OrderBy(e.g.,query.OrderBy(x => x.CreatedAt)) - You want GraphQL sorting to completely replace application-level ordering
- You're seeing
ThenByDescendingerrors when sorting
Example Scenario¶
Without [UseOrderByStripping]:
Example Scenario
// Application code
return _orders.Query.OrderBy(x => x.Id); // Default order by ID
// GraphQL: order: { data: { name: DESC } }
// Result: OrderBy(Id).ThenByDescending(Name) ❌ Name is secondary
With [UseOrderByStripping]:
Example Scenario (2)
// Application code with middleware
[UseOrderByStripping]
return _orders.Query.OrderBy(x => x.Id);
// GraphQL: order: { data: { name: DESC } }
// Result: OrderByDescending(Name) ✅ Name is primary
Middleware Order¶
The [UseOrderByStripping] attribute must be placed AFTER [UseSorting] in the attribute stack:
Middleware Order
[UsePaging] // Outermost
[UseProjection]
[UseFiltering]
[UseSorting]
[UseOrderByStripping] // Innermost (closest to resolver) ✅
public IQueryable<T> GetData()
Performance Considerations¶
- Index sort columns - Ensure indexes exist for frequently sorted fields
- Limit multi-column sorts - Each additional sort column may reduce index efficiency
- Sort + Filter alignment - Best performance when sort and filter use the same indexed columns
- Consider composite indexes - For common sort+filter combinations
Example: Dashboard Query¶
A typical dashboard query combining filter, sort, and paging:
query RecentOrders($tenantId: String!, $status: String)
query RecentOrders($tenantId: String!, $status: String) {
orders(
where: {
and: [
{ scope: { tenantId: { eq: $tenantId } } }
{ data: { status: { eq: $status } } }
]
}
order: [
{ data: { priority: DESC } }
{ createdAt: DESC }
]
first: 20
) {
nodes {
id
data {
customerName
status
priority
totalAmount
}
createdAt
}
pageInfo {
hasNextPage
endCursor
}
}
}
Next Steps¶
- Scoping - Multi-tenancy and security filtering