Skip to main content

How to Sort EF Core Results from a Query String Safely

· 5 min read
Jagdish Kumawat
Founder @ Dewiride

Clients want to choose a list's sort order, as in ?sort=-price. It's tempting to pass that text through to the query, but a query string should never decide which columns your database sorts on. An allow-list and a C# switch give clients the choice, and keep the control in your code.

Prerequisites​

Step 1: List the Sort Orders You Offer​

Add Sort to the record that holds the list's options, with [AllowedValues]:

src/Dewiride.Inventory.Api/Features/Products/ProductContracts.cs (excerpt)
[property: AllowedValues("name", "-name", "price", "-price", "newest", null, ErrorMessage = "Sort must be name, -name, price, -price or newest.")]
string? Sort = null,

A leading - for descending is a common convention. null is in the list, so leaving sort out is allowed. property: is needed on a record used with [AsParameters], because of a bug in ASP.NET Core 10.

Anything else gets a 400 before your code runs:

Response (?sort=drop)
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json

{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"Sort": [
"Sort must be name, -name, price, -price or newest."
]
},
"traceId": "00-916d9c346513e8f97c203032b49ce27c-b1283f7f49c2bbdc-00"
}

Step 2: Map Each Value to a Sort​

A switch turns each allowed value into real code:

src/Dewiride.Inventory.Api/Features/Products/ProductEndpoints.cs (excerpt)
// Every sort ends with the Id, so rows with the same name or price keep a fixed order
// and can't move between pages.
var sorted = query.Sort switch
{
"-name" => products.OrderByDescending(p => p.Name).ThenBy(p => p.Id),
"price" => products.OrderBy(p => p.UnitPrice).ThenBy(p => p.Id),
"-price" => products.OrderByDescending(p => p.UnitPrice).ThenBy(p => p.Id),
"newest" => products.OrderByDescending(p => p.CreatedAtUtc).ThenBy(p => p.Id),
_ => products.OrderBy(p => p.Name).ThenBy(p => p.Id),
};

The switch expression in a macOS-style code window, mapping each sort value to OrderBy or OrderByDescending with ThenBy on Id, and using the name as the default.

The _ arm is the default, for no sort at all. The client's text is only ever compared with fixed strings, so it never reaches the SQL. Every sort is a property you chose, which also lets you make sure it has an index if it needs one.

Step 3: Read the SQL​

?sort=-price:

API log (the inner query)
ORDER BY `p`.`UnitPrice` DESC, `p`.`Id`

The ThenBy is there, so two products with the same price always come in the same order. EF Core's pagination page gives the reason: "always make sure that your ordering is fully unique".

Why Not Accept Any Column?​

Building OrderBy from a column name takes extra code or a library, and it opens up things you may not want:

  • Sorting on columns that have no index, which is slow on a big table.
  • Sorting on data the client shouldn't know about, because the order itself reveals it.
  • Column names become part of your API, so renaming one breaks clients.

A short list of named sorts avoids all three, and it's easy to add to.

Common Mistakes​

MistakeWhat happensDo this instead
Using the client's text in raw SQLSQL injectionMap it with a switch
No ThenBy on a unique columnRows that tie can move between pagesEnd with the key
No default sortResults have no orderGive the _ arm a sort
[AllowedValues] without nullLeaving out sort gets a 400Include null
Forgetting property: on the attribute/openapi/v1.json answers 500Add it

FAQ​

Why does the variable need no type?​

The arms of the switch are all IOrderedQueryable<Product>, so var works. You then call Skip and Take on it.

Can clients sort by two columns?​

Not with this design. Add a named sort, such as category-price, if you need one. It's more code per option, and much easier to reason about.

Conclusion​

Allow a short list of sort names with [AllowedValues], map each one to OrderBy in a switch, end each with ThenBy on the key, and give the default arm a sort. Clients choose from what you offer, and their text never reaches the SQL.

Additional Resources​

Stay Updated

Subscribe to our newsletter for the latest tutorials, tech insights, and developer news.

By subscribing, you agree to our privacy policy. Unsubscribe at any time.