Interview question
Build a safe filtered list endpoint
Implements bounded filtering and deterministic keyset pagination without accepting arbitrary query expressions.
TL;DR
Implements bounded filtering and deterministic keyset pagination without accepting arbitrary query expressions.
Query parsing, allowlists, page-size bounds, deterministic ordering, keyset pagination, and DTO projection.
Practice the problem like a real interview: restate, reason, implement, and test.
Return orders ordered by created time and id. Accept status, take, and an opaque cursor. Reject unsupported statuses or sorts before querying.
Two orders sharing the same timestamp appear once across adjacent pages because the cursor includes both timestamp and id.
I parse the cursor into the exact ordering tuple, apply the same tuple to the WHERE boundary, fetch one extra row, and emit a next cursor only when another page exists.
app.MapGet("/orders", async Task<Results<Ok<OrderPage>, ValidationProblem>>
(string? status, int? take, string? cursor, AppDbContext db, CancellationToken ct) =>
{
OrderStatus? parsedStatus = null;
if (status is not null) {
if (!Enum.TryParse<OrderStatus>(status, true, out var value) ||
!Enum.IsDefined(value))
return TypedResults.ValidationProblem(new() {
["status"] = ["Status must be open, paid, or cancelled."]
});
parsedStatus = value;
}
if (!OrderCursor.TryDecode(cursor, out var after))
return TypedResults.ValidationProblem(new() {
["cursor"] = ["Cursor is invalid."]
});
var size = Math.Clamp(take ?? 25, 1, 100);
var query = db.Orders.AsNoTracking();
if (parsedStatus is not null)
query = query.Where(x => x.Status == parsedStatus.Value);
if (after is not null)
query = query.Where(x => x.CreatedAt < after.CreatedAt ||
(x.CreatedAt == after.CreatedAt && x.Id.CompareTo(after.Id) < 0));
var rows = await query
.OrderByDescending(x => x.CreatedAt).ThenByDescending(x => x.Id)
.Select(x => new OrderListItem(x.Id, x.Number, x.Status, x.CreatedAt))
.Take(size + 1).ToListAsync(ct);
var hasMore = rows.Count > size;
var items = rows.Take(size).ToArray();
var next = hasMore ? OrderCursor.Encode(items[^1].CreatedAt, items[^1].Id) : null;
return TypedResults.Ok(new OrderPage(items, next));
});
The query cost is bounded by page size and supported indexes. The cursor is opaque to clients but must be versioned or rejected cleanly if its format changes.
Move to the linked follow-up, next path step, prerequisite, or deeper variant.
Practice the next layer of the same subject.