Mert Özen Further With Every Line
Backend LINQ — #01

What Is LINQ? Query Syntax and Method Syntax in C#

Mert Özen Nov 7, 2026 11 min 2 views
What Is LINQ? Query Syntax and Method Syntax in C#

What LINQ changes about the queries we used to write with loops, the C# 3.0 features behind it, how the compiler translates query syntax into method calls, when to pick which notation, and the first pitfalls.

Version note: The code in this post uses C# 10 and the .NET 6 APIs; it was built and run with the .NET 10 SDK.

Guess First

We have a small catalog of five books. The query below looks like SQL, but it is plain C#. Which books does it print, and in what order? And think about this too: what exactly is this expression that starts with from, and what does the compiler turn it into?

var books = new List<Book>
{
    new("978-0132350884", "Clean Code", "Robert C. Martin", 37.90m, 12),
    new("978-0135957059", "The Pragmatic Programmer", "David Thomas", 42.50m, 0),
    new("978-0321125217", "Domain-Driven Design", "Eric Evans", 54.99m, 3),
    new("978-0201633610", "Design Patterns", "Erich Gamma", 49.95m, 7),
    new("978-1617294532", "C# in Depth", "Jon Skeet", 44.99m, 5),
};

IEnumerable<string> titles =
    from book in books
    where book.Stock > 0 && book.Price < 50m
    orderby book.Price descending
    select book.Title;

foreach (string title in titles)
    Console.WriteLine(title);

record Book(string Isbn, string Title, string Author, decimal Price, int Stock);

The Same Job with a Loop

Here is the output:

Design Patterns
C# in Depth
Clean Code

The books that are in stock and cheaper than 50 survive, ordered from most to least expensive. The Pragmatic Programmer is out because it has no stock, Domain-Driven Design because it costs 54.99. The query says what we want: "take the books that are in stock and cheap, sort them by price descending, give me their titles". It says nothing about how.

Before LINQ we would write the same thing like this:

var selected = new List<Book>();
foreach (Book book in books)
{
    if (book.Stock > 0 && book.Price < 50m)
        selected.Add(book);
}

selected.Sort((x, y) => y.Price.CompareTo(x.Price));

var titles = new List<string>();
foreach (Book book in selected)
    titles.Add(book.Title);

Console.WriteLine(string.Join(" | ", titles));
Design Patterns | C# in Depth | Clean Code

Same result, but most of the code is busy with the how: create a temporary list, loop, check the condition, add, write a comparison function, loop again, collect the titles. The reader has to reverse-engineer the intent from these steps. You write this pattern again for every new report, and every time it is a fresh chance for a small bug: swapping x and y in the comparison, forgetting to clear the temporary list.

LINQ (Language Integrated Query) makes querying part of the language. Think of telling the waiter your order instead of walking into the kitchen and cooking yourself: you say what you want and the library takes care of how. Throughout this series we will open up that vocabulary of "what", the LINQ operators, one by one.

One Query, Two Notations

You can write LINQ in two ways. The from ... where ... select form from the opening example is called query syntax. The method syntax version of the same query chains extension methods:

IEnumerable<string> querySyntax =
    from book in books
    where book.Stock > 0 && book.Price < 50m
    orderby book.Price descending
    select book.Title;

IEnumerable<string> methodSyntax = books
    .Where(book => book.Stock > 0 && book.Price < 50m)
    .OrderByDescending(book => book.Price)
    .Select(book => book.Title);

Console.WriteLine(string.Join(" | ", methodSyntax));
Console.WriteLine(querySyntax.SequenceEqual(methodSyntax));
Design Patterns | C# in Depth | Clean Code
True

SequenceEqual compares two sequences element by element and returns True: both notations produce the same result. That is no coincidence. In one of its first steps the compiler translates query syntax into method calls. where becomes a call to Where, orderby ... descending a call to OrderByDescending, and select a call to Select. The resulting IL calls the same methods in both cases; there is no performance difference between them.

Where, OrderByDescending and Select are extension methods defined in the Enumerable static class in the System.Linq namespace, and they work on IEnumerable<T>. Arrays, List<T>, Dictionary<TKey, TValue>, HashSet<T> or an iterator you wrote yourself: anything that implements IEnumerable<T> can be queried with LINQ. When you query a dictionary, the elements you walk over are KeyValuePair<TKey, TValue> values. The ImplicitUsings setting in the .NET 6 console template adds System.Linq as a global using, so you don't need a using at the top of the file.

The C# 3.0 Pieces That Make LINQ Possible

LINQ arrived in November 2007 with C# 3.0 and .NET Framework 3.5. It is not a single feature but the sum of several language features that shipped together in that release: extension methods, lambda expressions, type inference with var, anonymous types, object and collection initializers, expression trees and query expressions. Each of them is useful on its own, but they find their real purpose in LINQ.

The best way to see that LINQ is not magic is to write a small piece of it yourself:

// Our own extension method; the same idea as LINQ's Where
foreach (Book book in books.Filter(b => b.Stock == 0))
    Console.WriteLine(book.Title);

// Anonymous type: the compiler names the type
var summary = books
    .Select(b => new { b.Title, InStock = b.Stock > 0 })
    .Take(2);

foreach (var item in summary)
    Console.WriteLine(item);

static class SequenceExtensions
{
    public static IEnumerable<T> Filter<T>(this IEnumerable<T> source, Func<T, bool> predicate)
    {
        foreach (T item in source)
        {
            if (predicate(item))
                yield return item;
        }
    }
}
The Pragmatic Programmer
{ Title = Clean Code, InStock = True }
{ Title = The Pragmatic Programmer, InStock = False }

Filter is an extension method thanks to its first parameter marked with the this keyword. When you write books.Filter(...), the compiler turns it into a call to SequenceExtensions.Filter(books, ...). b => b.Stock == 0 is a lambda: the compiler converts it to a delegate of type Func<Book, bool>. The yield return in the body produces the elements one by one. If you read the iterator post in the collections series, this shape will look familiar. Apart from argument validation and a few optimizations, .NET's own Where works on the same idea.

new { b.Title, InStock = b.Stock > 0 } in the second part is an anonymous type. The compiler names it, its properties are read-only, and its ToString() output is the { Title = ..., InStock = ... } format the compiler generates. Because the type has no name, you have to declare the variable with var; that is exactly why var was added to C# 3.0 along with LINQ. We will come back to anonymous types in the projection post.

What Query Syntax Turns Into

We said query syntax is translated into method calls. That translation is purely name-based: the compiler looks for methods called Where and Select with suitable signatures. They don't even have to be on IEnumerable<T>:

var shelf = new Shelf(books);

// Shelf is not an IEnumerable; query syntax still compiles
var expensive =
    from b in shelf
    where b.Price > 45m
    select b.Title;

foreach (string title in expensive)
    Console.WriteLine(title);

class Shelf
{
    private readonly List<Book> _books;

    public Shelf(IEnumerable<Book> books) => _books = new List<Book>(books);

    public Shelf Where(Func<Book, bool> predicate)
    {
        Console.WriteLine("Shelf.Where called");
        var kept = new List<Book>();
        foreach (Book book in _books)
        {
            if (predicate(book))
                kept.Add(book);
        }
        return new Shelf(kept);
    }

    public List<TResult> Select<TResult>(Func<Book, TResult> selector)
    {
        Console.WriteLine("Shelf.Select called");
        var results = new List<TResult>();
        foreach (Book book in _books)
            results.Add(selector(book));
        return results;
    }
}
Shelf.Where called
Shelf.Select called
Domain-Driven Design
Design Patterns

Shelf neither implements IEnumerable<T> nor uses anything from LINQ. Query syntax still compiled, because the compiler rewrote from b in shelf where ... select ... word for word into shelf.Where(b => b.Price > 45m).Select(b => b.Title), and those calls bound to Shelf's own methods. Query syntax is a notational shortcut; its meaning is decided by the methods it calls.

Notice one detail in the output: the "Shelf.Where called" and "Shelf.Select called" lines were printed as soon as the query was defined, before the foreach started. Our methods do the work right away and return a list. LINQ's own operators don't behave like this; they return an object that describes the query and leave the work until the moment you iterate. That difference is exactly the topic of the next post.

Which One to Write When

Most teams use method syntax in everyday code. Not every operator has a query equivalent; query syntax only knows a handful of keywords such as from, where, select, orderby, group, join, let and into. Operators like Count, Take, Distinct, First and Sum have no keyword, so you call them with method syntax. Query syntax shines in queries that need intermediate variables or combine several sources:

using System.Globalization;

// Keep the decimal separator the same on every machine
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

// let computes an intermediate value once and names it
List<string> discounted =
    (from b in books
     let salePrice = b.Price * 0.9m
     where salePrice < 45m
     orderby salePrice
     select $"{b.Title}: {salePrice:0.00}")
    .Take(3)
    .ToList();

discounted.ForEach(Console.WriteLine);

// Count and Distinct have no query keyword, so we continue with method syntax
int inStock = (from b in books where b.Stock > 0 select b).Count();
int authors = books.Select(b => b.Author).Distinct().Count();
Console.WriteLine($"In stock: {inStock}, authors: {authors}");
Clean Code: 34.11
The Pragmatic Programmer: 38.25
C# in Depth: 40.49
In stock: 4, authors: 5

let computes the discounted price once and gives it a name; where, orderby and select use that name without recomputing it. To write the same query in method syntax you would first have to Select into an anonymous type to carry the intermediate value; that is how the compiler translates let anyway. At the end we closed the query with parentheses and continued with .Take(3).ToList(): the two notations mix happily in a single expression.

A practical rule: for single-source queries with a few steps, method syntax is shorter and the tooling support (IntelliSense, refactoring) is better. For SQL-like queries that need let, join or nested from clauses, query syntax can read better. The only real rule is consistency: in one codebase, having one person write a kind of query in query syntax and another in method syntax tires the reader.

One Syntax, Different Data Sources

The "Language Integrated" part of LINQ's name promises this: whether the data is in memory, in an XML document or in a database, you write the query in the same language. The flavor we have used in this post, for in-memory collections, is called LINQ to Objects. The same syntax queries XML as well:

using System.Xml.Linq;

XElement catalog = XElement.Parse(@"
<catalog>
  <book isbn='978-0132350884' price='37.90'>Clean Code</book>
  <book isbn='978-0321125217' price='54.99'>Domain-Driven Design</book>
  <book isbn='978-1617294532' price='44.99'>C# in Depth</book>
</catalog>");

IEnumerable<string> overForty =
    from book in catalog.Elements("book")
    where (decimal)book.Attribute("price")! > 40m
    select $"{book.Value} ({book.Attribute("isbn")!.Value})";

foreach (string line in overForty)
    Console.WriteLine(line);
Domain-Driven Design (978-0321125217)
C# in Depth (978-1617294532)

XElement.Elements("book") from System.Xml.Linq returns an IEnumerable<XElement>, and the rest is the LINQ we know. To convert the attribute to decimal we used XAttribute's explicit conversion operator.

On the database side things are a bit different. When you write dbContext.Books.Where(b => b.Price > 40m) in EF Core, the Where call is made on IQueryable<T>, not on IEnumerable<T>. In that case the lambda travels not as code to run but as an expression tree that can be inspected, and the provider translates it into SQL. The syntax is the same, but who runs the query and where changes completely. We will unpack this distinction in detail in the next post; in the EF Core series we already saw what that translation looks like in practice.

Common Pitfalls

Forgetting using System.Linq. With ImplicitUsings on in the new templates you will rarely hit this, but in an older project or a library with the setting turned off, if you write this:

using System;
using System.Collections.Generic;

var books = new List<Book>
{
    new("978-0132350884", "Clean Code", 37.90m),
    new("978-0321125217", "Domain-Driven Design", 54.99m),
};

var cheap = books.Where(b => b.Price < 40m);
Console.WriteLine(cheap.Count());

record Book(string Isbn, string Title, decimal Price);

the compiler cannot find anything called Where on List<Book>:

Program.cs(10,19): error CS1061: 'List<Book>' does not contain a definition for 'Where' and no accessible extension method 'Where' accepting a first argument of type 'List<Book>' could be found (are you missing a using directive or an assembly reference?)

The message also tells you why: Where is not a method of List<T> itself but an extension method, and extension methods are found only when their namespace is brought into view with using. Adding using System.Linq; at the top of the file is enough.

Leaving query syntax half-finished. A query expression must end with select or group. If SQL habits make you stop at where:

var books = new List<Book>
{
    new("978-0132350884", "Clean Code", 37.90m),
    new("978-0321125217", "Domain-Driven Design", 54.99m),
};

var cheap = from b in books
            where b.Price < 40m;

record Book(string Isbn, string Title, decimal Price);
Program.cs(8,32): error CS0742: A query body must end with a select clause or a group clause

Thinking a LINQ operator changes the source. A LINQ query always produces a new sequence; it never touches the source:

var books = new List<Book>
{
    new("978-0321125217", "Domain-Driven Design", 54.99m),
    new("978-0132350884", "Clean Code", 37.90m),
};

books.OrderBy(b => b.Price);
Console.WriteLine(books[0].Title);

List<Book> sorted = books.OrderBy(b => b.Price).ToList();
Console.WriteLine(sorted[0].Title);
Console.WriteLine(books[0].Title);

record Book(string Isbn, string Title, decimal Price);
Domain-Driven Design
Clean Code
Domain-Driven Design

The result of the first OrderBy call was not assigned anywhere, so it was wasted and books kept its order. The compiler gives no warning for that line. If you want to use the sorted result, assign it to a variable; if you want to sort the list in place, use List<T>.Sort. The same confusion applies to List<T>.ForEach: it is a List<T> method, not a LINQ operator, and it doesn't exist on IEnumerable<T>.

Cramming everything into one giant query. If a ten-line chain doesn't read well, name the intermediate steps: var inStock = books.Where(...), var cheapest = inStock.OrderBy(...). Since LINQ operators don't compute intermediate results, splitting costs nothing; the next post shows why.

A Small Experiment

★☆☆ Add a new book to the catalog from the opening example and change the query to "titles of books whose author starts with E, alphabetically". Write it first in query syntax, then in method syntax, and confirm with SequenceEqual that both results are the same.

★★☆ Rewrite the query that uses let in method syntax. You will need to Select into an anonymous type to carry the discounted price. Which version did you find more readable?

★★★ Add an OrderBy<TKey>(Func<Book, TKey> keySelector) method to the Shelf class and make the query from b in shelf orderby b.Price select b.Title compile. Hint: the orderby clause turns into the OrderBy call the compiler looks for, and if you return a Shelf the chain can continue with Select.

In this post we said that Shelf's methods ran the moment the query was defined while LINQ's wait. In the next post we look at that waiting, deferred execution: what it means, exactly when a query runs, and the difference between IEnumerable<T> and IQueryable<T>.