ASP.NET Web API (.NET Framework) · Data

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.

Intermediate 6 min Lesson 18 of 36Covers ASP.NET Web API 2.2 on .NET Framework 4.8.
Before you start:Pagination
0/36 in ASP.NET Web API
On this page · 6 min · Intermediate

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#

C# Illustrative
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#

C#ProductsController.cs Illustrative
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);
    }
}
Expected output
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#

  1. ProductFilter holds every optional filter; nullable types mean “not sent”.
  2. Each if adds a Where only when needed, building one IQueryable.
  3. The switch maps allowed sort names to strongly typed OrderBy calls; anything else is a 400.
  4. Take(100) caps the result size (combine with real pagination in production).
  5. 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#

Filtering in memory

_db.Products.ToList().Where(...) loads every row first. Keep the query as IQueryable until the end.

Dynamic ORDER BY from user input

Building "ORDER BY " + sortBy in SQL invites injection, and reflection-based sorting can expose hidden columns. Whitelist.

Treating false as “not sent”

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?
One more if with a Where on CreatedAt, and one more case in the switch.
Show solution
C# Illustrative
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#

Filtering and Sorting quizMultiple choice Question 1 of 3

When does an EF6 IQueryable built with several Where calls hit the database?

Score so far: 0/0

Want more? Practise all ASP.NET Web API MCQs · ASP.NET Web API interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.