C# LINQ Query Syntax: A Practical Guide
c# linq query syntax: Understand C# LINQ query syntax with practical examples, translation to method calls, and guidance on choosing between query and method syntax.
C# LINQ query syntax provides a declarative way to express data queries using clauses like from, where, select, and orderby. It is compiled by the C# compiler into the same underlying method calls as the fluent syntax, but it can be more readable for complex queries. This article explains how query syntax works, how it translates to method calls, and when to choose it over the fluent form.
Query Syntax Basics: from, where, select
The core structure of a query expression starts with a from clause that introduces a range variable, followed by optional where, orderby, group, join, and a final select or group clause. The simplest form filters and projects:
int[] numbers = { 1, 2, 3, 4, 5, 6 }; var evenNumbers = from n in numbers where n % 2 == 0 select n;
The from clause declares n as the range variable, which is strongly typed based on the source element type. The where clause filters the sequence, and select projects each surviving element. The result is an IEnumerable<int> that is lazily evaluated.
You can also introduce additional range variables with let to store intermediate values:
var squared = from n in numbers let square = n * n where square > 10 select square;
Here let creates a new variable square that can be used in subsequent clauses.
Ordering, Grouping, and Joins in Query Syntax
Query syntax supports orderby for sorting, group for grouping, and join for combining two sequences. These clauses mirror SQL constructs closely.
var products = new[] { new { Id = 1, Name = "Laptop", CategoryId = 1, Price = 1200 }, new { Id = 2, Name = "Mouse", CategoryId = 2, Price = 25 }, new { Id = 3, Name = "Keyboard", CategoryId = 2, Price = 80 } }; var categories = new[] { new { Id = 1, Name = "Electronics" }, new { Id = 2, Name = "Accessories" } }; var query = from p in products join c in categories on p.CategoryId equals c.Id orderby c.Name, p.Price descending select new { p.Name, Category = c.Name, p.Price };
The join clause uses equals (not ==) to match keys. The orderby clause can sort by multiple keys, with ascending (default) or descending.
Grouping uses the group ... by pattern and can introduce a new range variable with into:
var grouped = from p in products group p by p.CategoryId into g select new { CategoryId = g.Key, Count = g.Count() };
The into keyword creates a new range variable g that represents the grouped result, with a Key property and the ability to aggregate.
How Query Syntax Translates to Method Calls
Query syntax is syntactic sugar. The compiler translates each clause into calls to standard LINQ extension methods. For example, the earlier evenNumbers query becomes:
var evenNumbers = numbers.Where(n => n % 2 == 0).Select(n => n);
A query with multiple from clauses becomes a SelectMany call. The translation rules are defined by the C# specification, and the compiler uses the extension methods available in the System.Linq namespace. This means query syntax works with any type that implements IEnumerable<T> and has the appropriate extension methods, including custom LINQ providers.
Because the translation is mechanical, you can mix query syntax with method calls. For instance:
var q = (from n in numbers where n > 2 select n).Distinct().ToList();
Here the query expression is evaluated to an IEnumerable<int>, then Distinct and ToList are applied.
Query Syntax vs. Method Syntax: Choosing the Right Form
Both syntaxes produce the same result, but they differ in readability and flexibility. The choice often comes down to the complexity of the query and personal or team preference.
| Aspect | Query Syntax | Method Syntax |
|---|---|---|
| Readability | SQL-like, good for joins and grouping | Compact for simple filters and projections |
| Type inference | Range variables are strongly typed | Lambda parameters are inferred |
| Custom operators | Limited to standard query keywords | Can use any extension method |
| Mixing | Can call methods within clauses | Full control over the pipeline |
Query syntax is limited to the operators that have corresponding keywords: from, where, select, group, join, orderby, let. If you need operators like Distinct, Take, or Aggregate, you must call them as methods, either on the query result or by mixing syntax.
Use query syntax when the query involves multiple joins or grouping and the SQL-like structure improves clarity. Use method syntax for simple filters or when you need to chain custom extension methods. There is no performance difference; the compiler generates the same delegate calls.
Deferred Execution and Streaming Behavior
Like all LINQ queries, query syntax is deferred. The query is not executed until you iterate over it. This has important implications when the source collection changes after the query is defined.
var numbers = new List<int> { 1, 2, 3, 4 }; var query = from n in numbers where n > 2 select n; numbers.Add(5); // Modify source after query creation foreach (var n in query) { Console.WriteLine(n); // Prints 3, 4, 5 }
The query sees the updated source because it is evaluated at iteration time. This is true for both syntaxes.
Some operators are streaming, meaning they yield elements as they are produced (e.g., Where, Select). Others are buffering, meaning they must consume the entire source before producing results (e.g., OrderBy, GroupBy). This affects memory usage and latency. For large data sets, prefer streaming operators when possible.
Common Mistakes and Pitfalls with Query Syntax
Several mistakes are common when writing query syntax.
Forgetting the select clause is a frequent error. Query syntax requires a final select or group clause unless you are using a join ... into or a group ... into that already produces a result. The compiler will report a syntax error.
Misusing let can lead to variables that are never used, which is harmless but confusing. More importantly, let can cause an extra projection in the translation, which may affect performance if the computed value is expensive.
Incorrect join syntax is another issue. The on clause must use equals, not ==. Also, the order of the key selectors matters: the left side refers to the first sequence, the right side to the second.
Mixing query and method syntax incorrectly can cause subtle bugs. For example, calling a method on a range variable inside a query clause may work, but if that method cannot be translated by a LINQ provider (like Entity Framework), it will throw at runtime or cause client-side evaluation.
Using Query Syntax with IQueryable and Database Providers
When working with IQueryable<T>, such as a DbSet in Entity Framework Core, query syntax is translated into a provider-specific query (e.g., SQL). The same query expression is converted to an expression tree and then to SQL.
using var db = new AppDbContext(); var londonCustomers = from c in db.Customers where c.City == "London" select c;
The query is not executed until you iterate or call a terminal method like ToList(). The provider translates the where clause into a SQL WHERE clause.
However, not all query syntax constructs are translatable. For example, calling a custom method inside a where clause will prevent translation and cause the query to be evaluated on the client, which can be inefficient. Always check the generated SQL or use tools like ToQueryString() in EF Core to verify that the query is fully server-side.
Also, some operators like Take and Skip are supported, but the translation depends on the provider. When using query syntax with IQueryable, keep the query simple and avoid constructs that force client evaluation, such as let with a method call that cannot be converted to SQL.