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.
Move to the linked follow-up, next path step, prerequisite, or deeper variant.
Practice the next layer of the same subject.
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.
Share the question with a concise Core-answer excerpt and invite other developers to add their perspective.
Help improve the interview library.
Say thanks with a standalone, one-time $5 contribution. No account required.
Aporeon is shaped by Aleksandar Tomovski, a software developer with experience on both sides of technical interviews. Content is reviewed for accuracy, natural spoken delivery, useful depth, and honest trade-offs.