Filtering and Sorting
Clients rarely want everything. Filtering (“only Books under ₹500”) and sorting (“cheapest first”) let one endpoint serve many screens — if you build the query safely.
On this page · 6 min · Intermediate
On this page
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 6 min
- Difficulty
- Intermediate
- Updated
- Oct 28, 2026
Related tutorials
Learning objectives#
- Bind optional filters with a [FromUri] class
- Add Where clauses only for filters that were sent
- Whitelist sort fields and directions
- Keep all filtering in SQL
Explanation#
Put all optional filters in one class (category, minPrice, maxPrice, inStock, sortBy, desc) and bind it with [FromUri].
Start from IQueryable<Product> and add a Where only when a filter has a value. Nothing runs until ToListAsync, so EF6 sends one SQL query with just the conditions you added.
Copy values into local variables before using them in lambdas — the SQL stays clean and the intent is obvious.
Never pass the client’s sortBy into dynamic SQL or reflection unchecked. Use a switch over known names and return 400 for anything else.
Combine with pagination: filter → sort → count → page.
Syntax#
IQueryable<Product> query = _db.Products.AsNoTracking();
if (!string.IsNullOrEmpty(category)) query = query.Where(p => p.Category == category);
if (maxPrice.HasValue) query = query.Where(p => p.Price <= maxPrice.Value);
Code example: One endpoint, many filters#
using System.Data.Entity;
using System.Linq;
using System.Threading.Tasks;
using System.Web.Http;
using ShopApi.Data;
using ShopApi.Models;
public class ProductFilter
{
public string Category { get; set; }
public decimal? MinPrice { get; set; }
public decimal? MaxPrice { get; set; }
public bool? InStock { get; set; }
public string SortBy { get; set; } = "name"; // name | price | newest
public bool Desc { get; set; }
}
public class ProductsController : ApiController
{
private readonly ShopDbContext _db;
public ProductsController(ShopDbContext db) { _db = db; }
// GET api/products?category=Books&maxPrice=500&inStock=true&sortBy=price&desc=true
[HttpGet, Route("api/products")]
public async Task<IHttpActionResult> Get([FromUri] ProductFilter filter)
{
filter = filter ?? new ProductFilter();
IQueryable<Product> query = _db.Products.AsNoTracking();
if (!string.IsNullOrWhiteSpace(filter.Category))
{
var category = filter.Category.Trim();
query = query.Where(p => p.Category == category);
}
if (filter.MinPrice.HasValue)
{
var min = filter.MinPrice.Value;
query = query.Where(p => p.Price >= min);
}
if (filter.MaxPrice.HasValue)
{
var max = filter.MaxPrice.Value;
query = query.Where(p => p.Price <= max);
}
if (filter.InStock == true)
{
query = query.Where(p => p.Stock > 0);
}
switch ((filter.SortBy ?? "name").ToLowerInvariant())
{
case "name":
query = filter.Desc ? query.OrderByDescending(p => p.Name) : query.OrderBy(p => p.Name);
break;
case "price":
query = filter.Desc ? query.OrderByDescending(p => p.Price) : query.OrderBy(p => p.Price);
break;
case "newest":
query = query.OrderByDescending(p => p.CreatedAt);
break;
default:
return BadRequest("sortBy must be name, price or newest.");
}
var items = await query
.Take(100)
.Select(p => new ProductDto { Id = p.Id, Name = p.Name, Price = p.Price, Category = p.Category })
.ToListAsync();
return Ok(items);
}
}
GET /api/products?category=Books&maxPrice=500&sortBy=price&desc=true
SQL (simplified):
SELECT TOP (100) Id, Name, Price, Category FROM dbo.Products
WHERE Category = @p0 AND Price <= @p1
ORDER BY Price DESC
→ 200 [{"Id":9,"Name":"Clean Code","Price":499.00,"Category":"Books"}, ...]
GET /api/products?sortBy=password → 400 "sortBy must be name, price or newest."This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- ProductFilter holds every optional filter; nullable types mean “not sent”.
- Each if adds a Where only when needed, building one IQueryable.
- The switch maps allowed sort names to strongly typed OrderBy calls; anything else is a 400.
- Take(100) caps the result size (combine with real pagination in production).
- EF6 turns the whole chain into a single parameterised SQL statement.
Real-world use case#
A shop’s product listing page has filter chips for category, price range and availability plus a sort dropdown. All of them map to query-string parameters on this one endpoint, which the browser can bookmark and share.
Common mistakes#
_db.Products.ToList().Where(...) loads every row first. Keep the query as IQueryable until the end.
Building "ORDER BY " + sortBy in SQL invites injection, and reflection-based sorting can expose hidden columns. Whitelist.
bool InStock cannot tell “false” from “missing”. Use bool? for optional flags.
Best practices#
- One filter class per list endpoint
- Compose IQueryable conditionally
- Whitelist sort fields
- Index the columns you filter and sort on
Remember this#
Optional filter → nullable property → if (HasValue) Where(...).
IQueryable until the very end.
Sort fields: whitelist, never pass-through.
Practice exercise#
Your task: Add a createdAfter (DateTime?) filter and a "stock" sort option (lowest stock first unless desc=true) to the filter above.
Need a hint?
Show solution
using System;
using System.Linq;
using ShopApi.Models;
public static class ProductQueryExtensions
{
public static IQueryable<Product> CreatedAfter(this IQueryable<Product> query, DateTime? createdAfter)
{
if (!createdAfter.HasValue) return query;
var after = createdAfter.Value;
return query.Where(p => p.CreatedAt > after);
}
public static IQueryable<Product> SortByStock(this IQueryable<Product> query, bool desc)
{
return desc ? query.OrderByDescending(p => p.Stock) : query.OrderBy(p => p.Stock);
}
}
// In the action: query = query.CreatedAfter(filter.CreatedAfter);
// case "stock": query = query.SortByStock(filter.Desc); break;
Mini quiz#
When does an EF6 IQueryable built with several Where calls hit the database?
Want more? Practise all ASP.NET Web API MCQs · ASP.NET Web API interview questions
My notes#
Saved in this browser.