Searching
A search box sends free text — “mouse”, “usb c”, even “50%”. The ShopSmart API must find matching products across names and descriptions, rank the best matches first, treat special characters literally and reject searches that are too short.
On this page · 7 min · Intermediate
On this page
- In simple words
- 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
- 7 min
- Difficulty
- Intermediate
- Updated
- Oct 29, 2026
Related tutorials
In simple words#
Search is the shop assistant who finds “red shoes” even if you typed “Red Shoe”. The API matches text safely and quickly.
- Contains / LIKE
- Simple text matching.
- Full-text search
- Faster, smarter search in SQL Server.
- Debounce
- Clients wait before sending each keystroke.
Learning objectives#
- Search several columns with EF.Functions.Like
- Escape % and _ in user input
- Order results by simple relevance
- Validate search terms and know when to move to full-text search
Explanation#
EF.Functions.Like(p.Name, "%mouse%") translates to SQL LIKE. Contains() does too, but Like gives you control over wildcards and the escape character.
In LIKE, % and _ are wildcards. Escape them in the user’s text (e.g. with \) and pass the escape character to EF.Functions.Like, so a search for “50%” finds “50% off” and not every product containing “50”.
Simple relevance: order name matches before description matches, then names that start with the term, then alphabetically.
Validate input: a minimum length (2–3 characters) and a maximum, plus paging. Very short terms match everything and are expensive.
LIKE ‘%term%’ cannot use a normal index, so it slows down on big tables. For large catalogues use SQL Server full-text search (CONTAINS/FREETEXT) or a search engine such as Azure AI Search or Elasticsearch.
Syntax#
var pattern = quot;%{Escape(term)}%";
var results = db.Products.Where(p => EF.Functions.Like(p.Name, pattern, "\\")
|| EF.Functions.Like(p.Description!, pattern, "\\"));
Code example: A product search endpoint#
// Program.cs
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders(); // keep the demo output clean
builder.Services.AddControllers();
builder.Services.AddDbContext<AppDbContext>(o => o.UseSqlServer(
@"Server=(localdb)\mssqllocaldb;Database=ShopSmartApi_Search;Trusted_Connection=True;TrustServerCertificate=True"));
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await db.Database.EnsureDeletedAsync();
await db.Database.EnsureCreatedAsync();
db.Products.AddRange(
new Product { Name = "Wireless Mouse", Description = "Silent clicks, USB receiver", Price = 799m },
new Product { Name = "Mouse Pad XL", Description = "Fits keyboard and mouse", Price = 399m },
new Product { Name = "USB-C Hub", Description = "7 ports, 100W charging", Price = 1499m },
new Product { Name = "Gift Card", Description = "Save 50% on accessories", Price = 500m },
new Product { Name = "Webcam 1080p", Description = "Works with every 500-series hub", Price = 2199m });
await db.SaveChangesAsync();
}
app.MapControllers();
app.Urls.Add("http://127.0.0.1:5219");
await app.StartAsync();
using var http = new HttpClient { BaseAddress = new Uri("http://127.0.0.1:5219") };
foreach (var term in new[] { "mouse", "usb", "50%", "m" })
{
var response = await http.GetAsync(quot;/api/search?q={Uri.EscapeDataString(term)}");
var json = JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement;
var text = response.IsSuccessStatusCode
? string.Join(" | ", json.EnumerateArray().Select(p => p.GetString()))
: json.GetProperty("errors").EnumerateObject().First().Value[0].GetString();
Console.WriteLine(quot;q={term,-6} {(int)response.StatusCode} {text}");
}
await app.StopAsync();
using (var scope = app.Services.CreateScope())
await scope.ServiceProvider.GetRequiredService<AppDbContext>().Database.EnsureDeletedAsync();
// Controllers/SearchController.cs
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
[ApiController]
[Route("api/search")]
public class SearchController(AppDbContext db) : ControllerBase
{
private static string EscapeLike(string text) =>
text.Replace("\\", "\\\\").Replace("%", "\\%").Replace("_", "\\_");
[HttpGet]
public async Task<IEnumerable<string>> Search([FromQuery, Required, StringLength(50, MinimumLength = 2)] string q,
CancellationToken ct)
{
var term = q.Trim();
var pattern = quot;%{EscapeLike(term)}%";
return await db.Products.AsNoTracking()
.Where(p => EF.Functions.Like(p.Name, pattern, "\\") || EF.Functions.Like(p.Description!, pattern, "\\"))
.OrderBy(p => EF.Functions.Like(p.Name, pattern, "\\") ? 0 : 1) // name matches first
.ThenBy(p => EF.Functions.Like(p.Name, EscapeLike(term) + "%", "\\") ? 0 : 1) // then names starting with it
.ThenBy(p => p.Name)
.Take(20)
.Select(p => p.Name)
.ToListAsync(ct);
}
}
q=mouse 200 Mouse Pad XL | Wireless Mouse q=usb 200 USB-C Hub | Wireless Mouse q=50% 200 Gift Card q=m 400 The field q must be a string with a minimum length of 2 and a maximum length of 50.
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#
- “mouse” matches two product names; “Mouse Pad XL” starts with the term, so it comes before “Wireless Mouse”.
- “usb” finds the hub by name and the mouse by its description (“USB receiver”).
- “50%” is escaped, so it only matches the literal text “50%” in the gift card’s description — not “500-series” or “500”.
- “m” is shorter than the minimum length, so validation returns 400 before any query runs.
- Take(20) keeps responses small; real apps add paging as in the previous lessons. (The Description property is the optional text column on the course’s Product entity.)
Real-world use case#
ShopSmart’s search box calls this endpoint for the first version of search. When the catalogue grew past 100,000 products, the same contract was kept but the implementation moved to Azure AI Search with typo tolerance and synonyms.
Common mistakes#
A search for “_” or “%” matches everything and can be used to make expensive queries. Escape them.
q=a returns most of the catalogue and scans the whole table. Require a minimum length.
Leading-wildcard LIKE cannot use indexes. Use full-text indexes or a search service at scale.
Best practices#
- Escape LIKE wildcards
- Minimum and maximum term length
- Simple relevance ordering plus a limit
- Full-text or a search service for big catalogues
Remember this#
EF.Functions.Like(column, "%term%", escapeChar).
Escape \, % and _ in user input.
Too short → 400. Too big a table → full-text search.
Practice exercise#
Your task: Change the search so every word must match: q="usb hub" returns products whose name or description contains both “usb” and “hub”.
Need a hint?
Show solution
// Controllers/AllWordsSearchController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
[ApiController]
[Route("api/search/all-words")]
public class AllWordsSearchController(AppDbContext db) : ControllerBase
{
[HttpGet]
public async Task<IEnumerable<string>> Search([FromQuery] string q, CancellationToken ct)
{
IQueryable<Product> query = db.Products.AsNoTracking();
foreach (var word in q.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
{
var pattern = quot;%{word.Replace("%", "\\%").Replace("_", "\\_")}%";
query = query.Where(p => EF.Functions.Like(p.Name, pattern, "\\") || EF.Functions.Like(p.Description!, pattern, "\\"));
}
return await query.OrderBy(p => p.Name).Take(20).Select(p => p.Name).ToListAsync(ct);
}
}
Mini quiz#
Why escape % in a search term?
Want more? Practise all ASP.NET Core Web API MCQs · ASP.NET Core Web API interview questions
My notes#
Saved in this browser.