← Open in the full interactive course (progress tracking, search & more)

Every LINQ example so far in this module has used one style. C# quietly ships a second one that reads like a sentence.

Every single example across this module — filtering, projecting, sorting, grouping, joining, aggregating — has been written the same way: products.Where(...).OrderBy(...). That's called method syntax, and it's covered fully in the next lesson. But C# has a second, entirely different-looking way to write the exact same queries, one deliberately designed to read like a sentence out of SQL: from p in products where ... select p. This is query syntax, and it exists for a genuinely good reason — once queries get complex, with joins and grouping, it can read far more clearly than a long method chain.

This lesson covers the from/where/select query syntax in full, why some teams reach for it, and complete worked examples — including the cases where it shines and the cases where it visibly strains.

What Is It?

The Simple Explanation

Query syntax is an alternate way to write a LINQ query using dedicated C# keywords — from, where, select, orderby, group, join, let — arranged to visually resemble a SQL SELECT statement, even though it isn't SQL and isn't a database query at all. It's plain C# the whole way through.

The Technical Definition

A query expression is C# syntax built from these contextual keywords, always starting with from and always ending with either select or group ... by:

from range-variable in source where condition orderby key select result

Crucially, this is not a separate feature the runtime executes differently — the compiler translates every query expression into ordinary method-syntax calls before your code is even compiled to IL. This translation is defined precisely in the C# language specification, and it's exactly what the next lesson dissects in full. For now, treat query syntax as a second, purely syntactic way to spell the same queries you've already been writing.

Why Does It Exist?

Compare a query with a join and a group — the kind of query that gets genuinely hard to read as a long method chain — in both styles:

// Method syntax — readable, but the shape of the join gets visually buried var report = customers .Join(orders, c => c.Id, o => o.CustomerId, (c, o) => new { c.Name, o.Total }) .GroupBy(x => x.Name) .Select(g => new { Name = g.Key, Total = g.Sum(x => x.Total) }) .OrderByDescending(x => x.Total);
// Query syntax — the join and grouping read closer to their SQL equivalent var report = from c in customers join o in orders on c.Id equals o.CustomerId group o by c.Name into g orderby g.Sum(o => o.Total) descending select new { Name = g.Key, Total = g.Sum(o => o.Total) };

The second version reads, almost word for word, like the SQL a database developer would already know how to write: "from customers, joined to orders, grouped by name, ordered by total, select the shape I want." Query syntax exists specifically because join and group ... into chains are where method syntax's nested lambdas and repeated selector arguments start to visually crowd out the actual logic — and for teams with a SQL background, this style lowers the learning curve for LINQ considerably.

Big Picture

THE SHAPE OF A QUERY EXPRESSION
from p in products — name a range variable, name the source
where p.Stock > 0 — (optional) narrow it down
orderby p.Price — (optional) put it in order
select p.Name — mandatory: shape the final result
Every clause after from is optional except the final select (or a terminal group ... by) — a query expression is not valid without one of those two.

How It Works

READING A QUERY EXPRESSION, CLAUSE BY CLAUSE
1. from NAMES THE RANGE VARIABLE AND THE SOURCE
2. where FILTERS USING THE RANGE VARIABLE
3. orderby SORTS — COMMA-SEPARATED FOR MULTI-KEY SORTS
4. select SHAPES THE FINAL RESULT

Simple Example

public record Product(int Id, string Name, string Category, decimal Price, int Stock); List<Product> products = [ new(1, "Wireless Mouse", "Electronics", 24.99m, 120), new(2, "Mechanical Keyboard", "Electronics", 89.99m, 0), new(3, "Standing Desk", "Furniture", 349.00m, 15), new(4, "Desk Lamp", "Furniture", 19.50m, 60), new(5, "Noise-Cancelling Headphones", "Electronics", 199.99m, 8), ]; // A basic query: filter, then shape the result var affordable = from p in products where p.Price < 100 && p.Stock > 0 orderby p.Price select new { p.Name, p.Price }; foreach (var item in affordable) Console.WriteLine($"{item.Name} — ${item.Price}"); // Wireless Mouse — $24.99

Code → Meaning → Result: p is introduced once by from and reused in every clause after it — where, orderby, and select all refer back to the same range variable. Just like method syntax, this whole expression is deferred: nothing runs until affordable is enumerated by the foreach loop.

The let Clause — Naming an Intermediate Value

let introduces a new variable mid-query, computed once per item, useful when the same computed value is needed in more than one later clause:

var discounted = from p in products let discountedPrice = p.Price * 0.9m where discountedPrice < 50 select new { p.Name, discountedPrice }; foreach (var item in discounted) Console.WriteLine($"{item.Name}: ${item.discountedPrice:F2}"); // Wireless Mouse: $22.49 // Desk Lamp: $17.55

Without let, you'd have to repeat p.Price * 0.9m in both the where clause and the select clause — let computes it once and gives it a name usable everywhere afterward, which method syntax can only replicate with an extra Select step beforehand.

Real-World Example

An admin report needs each customer's order count and total spend — a join plus a grouping, the exact combination query syntax is built to make readable:

public record Customer(int Id, string Name, string City); public record Order(int Id, int CustomerId, DateOnly OrderDate, decimal Total); List<Customer> customers = [ new(1, "Ana Cole", "Austin"), new(2, "Ben Diaz", "Chicago"), new(3, "Cara Lopez", "Austin"), ]; List<Order> orders = [ new(101, 1, new DateOnly(2026, 1, 5), 120.00m), new(102, 1, new DateOnly(2026, 2, 14), 45.50m), new(103, 2, new DateOnly(2026, 1, 20), 89.99m), new(104, 3, new DateOnly(2026, 3, 1), 310.00m), ]; var customerSummary = from c in customers join o in orders on c.Id equals o.CustomerId into customerOrders select new { c.Name, c.City, OrderCount = customerOrders.Count(), TotalSpent = customerOrders.Sum(o => o.Total) }; foreach (var s in customerSummary.OrderByDescending(s => s.TotalSpent)) Console.WriteLine($"{s.Name} ({s.City}): {s.OrderCount} orders, ${s.TotalSpent}"); // Cara Lopez (Austin): 1 orders, $310.00 // Ana Cole (Austin): 2 orders, $165.50 // Ben Diaz (Chicago): 1 orders, $89.99

Notice the final sort was written in method syntax, tacked onto the end of the query expression — this is completely normal and extremely common. A query expression evaluates to an ordinary IEnumerable<T>, so any method-syntax operator can be chained onto it afterward. The two styles aren't rivals you must pick one of forever; they interoperate freely in the same line of code.

Analogy

Two Languages, One Meaning

Think of query syntax and method syntax as two different phrasings of the exact same request to a waiter: "I'd like the salad, no onions, sorted by size" versus "salad → hold the onions → sort by size." Both convey identical instructions; one just happens to read more like a spoken sentence, and the other like a sequence of steps. Neither phrasing changes what actually arrives at the table — that's the compiler's job, and it treats both exactly the same, as the next lesson shows directly.

Under the Hood

A COMPILE-TIME TRANSLATION, NOT A SEPARATE RUNTIME FEATURE
1. THE COMPILER REWRITES QUERY SYNTAX BEFORE YOUR CODE EVEN COMPILES TO IL
2. THE COMPILED IL IS IDENTICAL EITHER WAY
3. THE NEXT LESSON SHOWS THE EXACT TRANSLATION, SIDE BY SIDE

Common Confusion

1. Query syntax is not SQL, and it doesn't talk to a database

The keywords look deliberately SQL-like, but this lesson has been entirely about IEnumerable<T> — plain, in-memory, LINQ-to-Objects queries over C# collections. No database, no network call, no SQL is involved anywhere in this module. (When query syntax is used against IQueryable<T> — the subject of an earlier lesson in this module — it can eventually be translated into real SQL by a provider like EF Core, but that's a separate, later topic outside this module's scope.)

2. Not every LINQ operator has a query-syntax keyword

Where, Select, OrderBy/ThenBy, GroupBy, and Join all have dedicated keywords. Many others — Count(), Sum(), Take(), Skip(), Distinct(), ToList(), everything from the Immediate Execution lesson — do not. There is no take or distinct keyword. Whenever you need one of these, you drop into method syntax, either wrapping the whole query expression in parentheses and calling the method on it, or simply chaining it on the end exactly as the Real-World Example above did with OrderByDescending.

Common Mistakes

Mistake 1 — Forgetting the mandatory trailing select (or group ... by)

from p in products where p.Stock > 0 with nothing after it — this doesn't compile. A query expression is not complete without a final select or a terminal group ... by clause. Always end with select p (or a projection) if you just want the filtered items back, exactly as-is.

Mistake 2 — Forcing a whole complex query into query syntax when method syntax alone would be clearer

Reaching for from/select out of habit for a simple, single-filter query like "products in stock" — this only adds visual overhead for no benefit. Query syntax earns its keep on queries with joins, grouping, or several chained conditions where the SQL-like phrasing genuinely improves readability; for a single Where, plain method syntax (products.Where(p => p.Stock > 0)) is shorter and just as clear.

When Should I Use It?

Query syntax tends to shine when

Method syntax is usually simpler when

Mental Model

Query syntax = "the same LINQ query, spelled out like a sentence"

Remember:
· Every query expression starts with from and must end with select or group ... by.
· Not every operator has a keyword — method syntax fills the gaps, and the two mix freely in one expression.
· The compiler rewrites query syntax into method calls before compiling — identical IL, identical performance either way.
· Reach for it when a join or grouping makes the SQL-like phrasing genuinely clearer, not as a universal default.

Key Takeaway


Check Your Understanding

You've learned how to read and write LINQ query syntax. Let's check your understanding.

1. Which clause is mandatory in every valid LINQ query expression?

Show answer

Correct: C

Why C is correct: As shown in Big Picture and Common Mistakes, a query expression must end with either select or a terminal group ... by — every other clause covered in this lesson is optional.

Why A, B, and D are incorrect: where, orderby, and let are all genuinely optional — a valid query expression can omit any or all of them, as long as it starts with from and ends with select or group ... by.

Reinforcement: Think of from ... select as the required bookends; everything between them is optional shaping.

2. Why doesn't a query like from p in products select p.Name.Distinct()-style thinking work for something like "distinct results" purely in query syntax?

Show answer

Correct: A

Why A is correct: As covered in Common Confusion, not every LINQ operator has a matching query-syntax keyword. Distinct() is one of the ones without one — to use it, you chain it in method syntax onto the result of the query expression, e.g. (from p in products select p.Category).Distinct().

Why B is incorrect: The two styles mix freely — the Real-World Example in this lesson chained OrderByDescending in method syntax directly onto a query expression.

Why C is incorrect: Distinct() works on any type with well-defined equality, strings included.

Why D is incorrect: Query expressions return an IEnumerable<T>, which can contain any number of results — this isn't a limitation that exists.

Reinforcement: When query syntax hits an operator without a keyword, drop into method syntax — that's the normal, expected pattern, not a workaround.

3. What does the let clause in a query expression actually do?

Show answer

Correct: B

Why B is correct: As shown in Simple Example, let discountedPrice = p.Price * 0.9m computes that value once per item and makes it available by name to every clause that follows, avoiding repeating the same expression in both where and select.

Why A is incorrect: Like every LINQ operator in this module, let never mutates the source — it only introduces a computed value within the query itself.

Why C is incorrect: let has nothing to do with forcing execution — the whole query expression, let included, remains just as deferred as any other LINQ query until it's enumerated.

Why D is incorrect: from always comes first — let can only appear after it, as one of the optional clauses in between from and the final select.

Reinforcement: let exists purely to avoid repeating a computed expression across multiple clauses in the same query.

4. What happens to a query expression at compile time, relative to method syntax?

Show answer

Correct: B

Why B is correct: As explained in Under the Hood, this is a purely compile-time, textual translation defined by the C# language specification — by the time IL is generated, there's no distinction left between code originally written in query syntax versus method syntax.

Why A is incorrect: There is exactly one execution engine — LINQ-to-Objects' method implementations. Query syntax never runs through anything separate.

Why C is incorrect: Since the compiled IL is identical either way, there's no runtime performance difference between the two styles at all.

Why D is incorrect: Query syntax works over any IEnumerable<T> source, exactly like method syntax — List<T> included.

Reinforcement: Query syntax and method syntax are two spellings of the same instructions — the next lesson shows the exact translation in detail.

You can now read and write LINQ query syntax with confidence. Next up: the method syntax it compiles down to — and why most real-world C# code reaches for method syntax by default.


dotnetmadeeasy.com — Learn C# and .NET, the right way.