Interview question
Compose optional product filters without early materialization
Builds optional EF Core predicates while preserving server translation, stable ordering, and a bounded result.
TL;DR
Builds optional EF Core predicates while preserving server translation, stable ordering, and a bounded result.
IQueryable composition, optional predicates, server translation, validation, projection, and pagination.
Practice the problem like a real interview: restate, reason, implement, and test.
Start from Products and return ProductSearchItem(Id, Name, Price, AvailableQuantity). Filter values are nullable, InStockOnly is boolean, and the request has a validated maximum of 100 results. Do not call AsEnumerable, ToList, or a custom CLR method before all database work is expressed.
With category 4, minimum price 20, no maximum, and InStockOnly = true, only category-4 products priced at least 20 with positive available quantity are returned.
I keep an IQueryable<Product> and add one predicate for each supplied filter. This reads like normal branching in C#, but EF sees one final expression tree and translates it as one SQL query. Validation stays outside the query, while all data filtering remains server-side.
public static async Task<List<ProductSearchItem>> SearchProductsAsync(
AppDbContext db,
ProductSearch request,
CancellationToken cancellationToken)
{
if (request.MinPrice is not null &&
request.MaxPrice is not null &&
request.MinPrice > request.MaxPrice)
{
throw new ArgumentException("Minimum price cannot exceed maximum price.");
}
IQueryable<Product> query = db.Products.AsNoTracking();
if (request.CategoryId is not null)
query = query.Where(product => product.CategoryId == request.CategoryId);
if (request.MinPrice is not null)
query = query.Where(product => product.Price >= request.MinPrice);
if (request.MaxPrice is not null)
query = query.Where(product => product.Price <= request.MaxPrice);
if (request.InStockOnly)
query = query.Where(product => product.AvailableQuantity > 0);
return await query
.OrderBy(product => product.Name)
.ThenBy(product => product.Id)
.Select(product => new ProductSearchItem(
product.Id,
product.Name,
product.Price,
product.AvailableQuantity))
.Take(Math.Clamp(request.Limit, 1, 100))
.ToListAsync(cancellationToken);
}
The database receives one parameterized query containing only active predicates. Different filter combinations may produce different plans, so high-traffic search endpoints should be checked with representative combinations and data distribution rather than one convenient local case.
Take.ToListAsync and filtering the list in memory.Move to the linked follow-up, next path step, prerequisite, or deeper variant.
Practice the next layer of the same subject.