C# LINQ Query Syntax
LINQ query syntax is a SQL-like way to write queries directly in C#. It lets you filter, sort, transform, join, and group data with keywords such as from, where, orderby, and select. It matters because some queries, especially joins and groups, are easier to read when written as a clear sequence of clauses instead of a long chain of method calls.
Overview: How Query Syntax Works
LINQ has two main styles: method syntax and query syntax. Method syntax calls extension methods such as Where, Select, OrderBy, Join, and GroupBy. Query syntax uses C# language keywords that the compiler translates into those same method calls. The runtime does not have a separate query engine for query syntax. After compilation, an in-memory query over a List<T> is still ordinary calls into System.Linq.Enumerable.
A query expression begins with from. The name introduced there is called a range variable. In from student in students, student represents one item at a time from the students sequence. Later clauses use that range variable to filter, sort, project, group, or join. Query syntax always ends with either select or group, unless it uses into to continue with another query stage.
For objects that implement IEnumerable<T>, most LINQ query syntax uses deferred execution. Creating the query usually stores the source sequence and the operations to apply later. The query runs when it is enumerated, such as in a foreach loop, ToList(), Count(), First(), or string.Join. This means a query can see later changes to the source collection unless you materialize the result with ToList() or ToArray().
Query syntax is not identical to SQL. The from clause comes first because C# needs to know the type of the range variable before it can compile the later clauses. Also, LINQ query syntax is strongly typed. If student.Score is an int, comparisons and projections are checked by the compiler before the program runs. That gives LINQ the readability of a query language while keeping the safety of C# expressions.
Syntax
string[] source = { "ant", "bee", "cat", "ox" };
var query =
from item in source
where item.Length == 3
orderby item
select item.ToUpper();
| Part | Meaning |
|---|---|
from item in source |
Starts the query and introduces item as the range variable for each element. |
where item.Length == 3 |
Filters the sequence. The condition must produce a bool. |
orderby item |
Sorts matching items. Use descending for reverse order. |
select item.ToUpper() |
Projects each remaining item into the result sequence. |
var query |
Commonly used because query operators create generic iterator types that are verbose and not useful to name directly. |
Common query clauses include from, where, orderby, select, let, join, group, and into. A complete program should include the System.Linq namespace so the translated method calls are available.
Examples
Example 1: Filter, Sort, And Project Numbers
using System;
using System.Linq;
class Program
{
static void Main()
{
int[] numbers = { 7, 2, 9, 4, 6, 1 };
var query =
from number in numbers
where number % 2 == 0
orderby number descending
select number * 10;
foreach (int value in query)
{
Console.WriteLine(value);
}
}
}
Output:
60
40
20
The from clause reads each integer from the array. The where clause keeps even numbers, orderby number descending sorts them from largest to smallest, and select number * 10 changes each remaining number into its final result. The query does not run until the foreach loop asks for values.
Example 2: Use let And Anonymous Objects
using System;
using System.Collections.Generic;
using System.Linq;
record Student(string Name, string Track, int Score);
class Program
{
static void Main()
{
var students = new List<Student>
{
new Student("Mina", "Web", 92),
new Student("Omar", "Data", 81),
new Student("Ava", "Web", 87),
new Student("Noah", "Data", 95),
new Student("Ivy", "Games", 78)
};
var honorRoll =
from student in students
let passedWithHonor = student.Score >= 85
where passedWithHonor
orderby student.Track, student.Name
select new
{
student.Name,
student.Track,
Grade = student.Score
};
foreach (var student in honorRoll)
{
Console.WriteLine($"{student.Track}: {student.Name} ({student.Grade})");
}
}
}
Output:
Data: Noah (95)
Web: Ava (87)
Web: Mina (92)
The let clause creates another range variable inside the query. Here it names the condition student.Score >= 85, which can improve readability when the expression is reused or meaningful. The select new { ... } expression creates anonymous objects containing only the values needed for the report.
Example 3: Join Related Collections
using System;
using System.Collections.Generic;
using System.Linq;
record Customer(int Id, string Name);
record Order(int Id, int CustomerId, decimal Total);
class Program
{
static void Main()
{
var customers = new List<Customer>
{
new Customer(1, "Mina"),
new Customer(2, "Omar"),
new Customer(3, "Ava")
};
var orders = new List<Order>
{
new Order(101, 1, 25.50m),
new Order(102, 3, 40.00m),
new Order(103, 1, 12.75m)
};
var report =
from order in orders
join customer in customers
on order.CustomerId equals customer.Id
orderby customer.Name, order.Id
select new { order.Id, customer.Name, order.Total };
foreach (var row in report)
{
Console.WriteLine($"Order {row.Id}: {row.Name} paid {row.Total:F2}");
}
}
}
Output:
Order 102: Ava paid 40.00
Order 101: Mina paid 25.50
Order 103: Mina paid 12.75
Query syntax is often clearer for joins because the relationship is visible in the on ... equals ... clause. This is an inner join: only orders with a matching customer are included. The word equals is required in query syntax; writing == there is a compile-time error.
Example 4: Group And Continue With into
using System;
using System.Collections.Generic;
using System.Linq;
record Sale(string Region, string Product, decimal Amount);
class Program
{
static void Main()
{
var sales = new List<Sale>
{
new Sale("East", "Keyboard", 120.00m),
new Sale("West", "Mouse", 45.50m),
new Sale("East", "Mouse", 75.00m),
new Sale("North", "Cable", 30.00m),
new Sale("West", "Keyboard", 140.00m)
};
var totals =
from sale in sales
group sale by sale.Region into regionGroup
orderby regionGroup.Key
select new
{
Region = regionGroup.Key,
Count = regionGroup.Count(),
Total = regionGroup.Sum(sale => sale.Amount)
};
foreach (var region in totals)
{
Console.WriteLine($"{region.Region}: {region.Count} sales, {region.Total:F2}");
}
}
}
Output:
East: 2 sales, 195.00
North: 1 sales, 30.00
West: 2 sales, 185.50
The group sale by sale.Region clause creates groups keyed by region. The into regionGroup clause continues the query using each group as the new range variable. Each group has a Key and can be enumerated, counted, or summed.
How It Works Step By Step
- The compiler parses the query expression and identifies range variables, clause order, and result types.
- The compiler translates the query into method calls. For example,
wherebecomesWhere,selectbecomesSelect,orderbybecomesOrderByorThenBy,joinbecomesJoin, andgroupbecomesGroupBy. - Lambda expressions are generated from the expressions inside the clauses. In
where student.Score >= 85, the compiler creates logic equivalent tostudent => student.Score >= 85. - For
IEnumerable<T>, the translated methods usually return iterator objects. These objects store the source and delegates until enumeration begins. - Streaming clauses such as
whereandselectcan process one item at a time. Sorting, grouping, and joining usually need buffering because they must inspect more than one item before returning results. - When the query is enumerated, the CLR executes normal generic methods, delegates, and enumerators. Query syntax has already disappeared from the compiled program.
Because query syntax is translated by the compiler, it works with more than in-memory collections. Providers such as Entity Framework can receive expression trees from IQueryable<T> queries and translate them to SQL. This lesson focuses on IEnumerable<T>, but the same syntax can have different execution rules when a database provider is involved.
Common Mistakes
Putting Clauses In SQL Order
var query =
where score >= 80
from score in scores
select score;
This does not compile because C# query syntax must begin with from. The compiler needs the range variable before it can understand later expressions.
using System;
using System.Linq;
class Program
{
static void Main()
{
int[] scores = { 72, 91, 84 };
var query =
from score in scores
where score >= 80
select score;
Console.WriteLine(string.Join(", ", query));
}
}
Output:
91, 84
Using == In A Query Join
var query =
from order in orders
join customer in customers
on order.CustomerId == customer.Id
select customer.Name;
In a query expression join, C# requires equals, not ==. The left side is evaluated using the outer range variable and the right side is evaluated using the joined sequence’s range variable.
using System;
using System.Linq;
record Customer(int Id, string Name);
record Order(int CustomerId);
class Program
{
static void Main()
{
Customer[] customers = { new Customer(1, "Mina") };
Order[] orders = { new Order(1) };
var query =
from order in orders
join customer in customers
on order.CustomerId equals customer.Id
select customer.Name;
Console.WriteLine(string.Join(", ", query));
}
}
Output:
Mina
Expecting The Query To Run Immediately
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
List<string> names = new List<string> { "Mina", "Omar" };
var query =
from name in names
where name.Length == 4
select name;
names.Add("Noah");
Console.WriteLine(string.Join(", ", query));
}
}
Output:
Mina, Omar, Noah
The query was created before Noah was added, but it ran afterward. Use ToList() when you want a snapshot at a specific moment.
Best Practices
- Use query syntax when it improves readability, especially for joins, multiple
fromclauses, grouping, and long projections. - Use method syntax when the operation has no query clause, such as
Any,Count,FirstOrDefault,Take, orDistinct. - Keep range variable names meaningful. Prefer
studentororderover vague names such asxin larger queries. - Remember deferred execution. Materialize with
ToList()orToArray()before reusing results or before changing the source collection. - Avoid side effects inside query clauses. Queries should describe data transformation, not secretly change state.
- Use
letfor expensive or repeated expressions so they are named once and can be reused in later clauses. - Do not assume query syntax is faster than method syntax. For the same query, the compiler usually translates it to the same underlying operations.
- When querying a database through
IQueryable<T>, avoid calling custom C# methods inside the query unless the provider can translate them.
Practice Exercises
- Create an array of product names. Use query syntax to print names with at least five characters, sorted alphabetically, in uppercase.
- Create a list of employees with department and salary. Use
group ... byandintoto print each department’s average salary. - Create two collections: authors and books. Use a query syntax
jointo print each book title with its author’s name.
Summary
- LINQ query syntax is C# syntax that the compiler translates into LINQ method calls.
- Every query expression starts with
fromand ends withselectorgroup, unlessintocontinues the query. wherefilters,orderbysorts,selectprojects,joincombines related sequences, andgroupbuilds grouped results.- For
IEnumerable<T>, most query expressions use deferred execution and run only when enumerated. - Query syntax and method syntax are interoperable, so choose the style that makes the query easiest to read.
