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.
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.
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 resultCrucially, 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.
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.
from p in products — name a range variable, name the sourcewhere p.Stock > 0 — (optional) narrow it downorderby p.Price — (optional) put it in orderselect p.Name — mandatory: shape the final result
from is optional except the final select (or a terminal group ... by) — a query expression is not valid without one of those two.from p in products — p is the range variable, standing in for "one item" throughout every clause that follows, exactly like the lambda parameter in method syntax.where p.Stock > 0 — a boolean condition, exactly like a Where predicate. Multiple where clauses can appear, and behave the same as chained Where calls: they combine as AND.orderby p.Category, p.Price descending — a single clause can hold several comma-separated keys, which is query syntax's equivalent of chaining OrderBy(...).ThenByDescending(...).select p.Name or select new { p.Name, p.Price } — this is mandatory, and it's what the whole expression ultimately evaluates to: an IEnumerable<T> of whatever shape select produces.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.99Code → 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.
let Clause — Naming an Intermediate Valuelet 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.55Without 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.
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.99Notice 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.
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.
from/where/select keyword pattern and mechanically rewrites it into the equivalent chain of Where, Select, OrderBy, and Join method calls — a purely textual, well-defined transformation specified in the C# language specification.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.)
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.
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.
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.
join, especially a multi-table style join between two or more sequences.let names it once, cleanly.Where, Select, or short chain with no join or group.Take, Skip, Distinct, Count(), and the rest.from and must end with select or group ... by.from/where/orderby/select/group/join/let to write LINQ queries in a style resembling SQL.from and must end with select or a terminal group ... by.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?
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?
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?
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?
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.