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

  1. The compiler parses the query expression and identifies range variables, clause order, and result types.
  2. The compiler translates the query into method calls. For example, where becomes Where, select becomes Select, orderby becomes OrderBy or ThenBy, join becomes Join, and group becomes GroupBy.
  3. Lambda expressions are generated from the expressions inside the clauses. In where student.Score >= 85, the compiler creates logic equivalent to student => student.Score >= 85.
  4. For IEnumerable<T>, the translated methods usually return iterator objects. These objects store the source and delegates until enumeration begins.
  5. Streaming clauses such as where and select can process one item at a time. Sorting, grouping, and joining usually need buffering because they must inspect more than one item before returning results.
  6. 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 from clauses, grouping, and long projections.
  • Use method syntax when the operation has no query clause, such as Any, Count, FirstOrDefault, Take, or Distinct.
  • Keep range variable names meaningful. Prefer student or order over vague names such as x in larger queries.
  • Remember deferred execution. Materialize with ToList() or ToArray() 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 let for 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

  1. Create an array of product names. Use query syntax to print names with at least five characters, sorted alphabetically, in uppercase.
  2. Create a list of employees with department and salary. Use group ... by and into to print each department’s average salary.
  3. Create two collections: authors and books. Use a query syntax join to 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 from and ends with select or group, unless into continues the query.
  • where filters, orderby sorts, select projects, join combines related sequences, and group builds 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.