C# LINQ Basics, Re-run on .NET 10

from, where, orderby, select, group and join with examples, all re-run on .NET 10. Deferred execution, the new LeftJoin method, and measured speed against a plain loop.

Revised November 2026. This tutorial first appeared in Korean in 2021. We re-ran every example on .NET 10.0.14 (C# 14). The outputs matched the original, but one example did not compile because of a type-name typo, the code contained invisible BOM characters, and the printed output had lost its line breaks. Below are the fixed examples, plus deferred execution, .NET 10's LeftJoin and measured speed.

LINQ (Language-Integrated Query) lets you query anything that looks like a collection directly in C#. Filtering, sorting and grouping that would take several loops and conditions become one expression.

Loop vs. LINQ

Picking the even numbers from an array and sorting them with a loop:

List<int> intList = new List<int>();
int[] numbers = { 1, 3, 4, 6, 5, 9, 8, 12, 15, 18, 17, 11, 22 };

foreach (int num in numbers)
{
    if (num % 2 == 0)
        intList.Add(num);
}
intList.Sort();

foreach (int num in intList)
    Console.Write("{0} ", num);
4 6 8 12 18 22

The same thing as a LINQ query expression:

int[] numbers = { 1, 3, 4, 6, 5, 9, 8, 12, 15, 18, 17, 11, 22 };
var data = from num in numbers
           where num % 2 == 0
           orderby num
           select num;

foreach (var i in data)
    Console.Write("{0} ", i);
4 6 8 12 18 22

Or in method syntax, which is what the compiler turns query expressions into (Order() exists since .NET 7):

var data = numbers.Where(n => n % 2 == 0).Order();

from

Every query expression starts with from:

from rangeVariable in dataSource

The range variable works like a foreach loop variable but does not hold data itself. The source can be anything implementing IEnumerable<T> (or IQueryable<T>), such as arrays and List<T>.

where

where is a filter: only elements for which the condition is true pass through. You can use more than one.

var data = from num in numbers
           where num < 10
           orderby num
           select num;
// 1 3 4 5 6 8 9

orderby

orderby sorts, ascending by default; add descending to reverse. Several keys are separated by commas. It compiles to OrderBy, OrderByDescending and ThenBy.

var data = from num in numbers
           orderby num descending
           select num;
// 22 18 17 15 12 11 9 8 6 5 4 3 1

select

select shapes the result, and its type sets the type of the whole query: selecting char gives IEnumerable<char>. A query expression must end with select or group.

char[] chars = "str12i3!@$1ng".ToCharArray();
var data = from vchar in chars
           where vchar >= 97 && 122 >= vchar      // 'a'..'z'
           select vchar;

foreach (char c in data)
    Console.Write("{0}", c);
// string

In real code, vchar >= 'a' && vchar <= 'z' or char.IsAsciiLetterLower(vchar) (.NET 7+) reads better than 97 and 122.

group

group A by B into C splits elements A by key B into groups C. into is needed only if you keep working with the groups.

var listStudent = new List<Student>      // the 2021 version had List<student> and did not compile
{
    new Student() { Name = "Kim Cheolsu", Average = 78.5 },
    new Student() { Name = "Kim Younghee", Average = 91.2 },
    new Student() { Name = "Hong Gildong", Average = 77.3 },
    new Student() { Name = "Kim Gilsu", Average = 80.8 }
};

var queryStudent = from student in listStudent
                   orderby student.Average
                   group student by student.Average < 80.0;

foreach (var studentGroup in queryStudent)
{
    Console.WriteLine(studentGroup.Key ? "Average below 80:" : "Average 80 or above:");
    foreach (var student in studentGroup)
        Console.WriteLine("\t{0}: {1}", student.Name, student.Average);
}
Average below 80:
    Hong Gildong: 77.3
    Kim Cheolsu: 78.5
Average 80 or above:
    Kim Gilsu: 80.8
    Kim Younghee: 91.2

The original's List<student> fails because C# is case-sensitive:

error CS0246: The type or namespace name 'student' could not be found

To group by score band, name the group with into and keep going:

var queryStudent = from student in listStudent
                   group student by (int)student.Average / 10 into g
                   orderby g.Key
                   select g;

foreach (var g in queryStudent)
{
    Console.WriteLine("{0} to {1}:", g.Key * 10, g.Key * 10 + 10);
    foreach (var student in g)
        Console.WriteLine("\t{0}: {1}", student.Name, student.Average);
}

With seven students (adding 54.2, 61.4 and 90.8), the groups come out as 50–60, 60–70, 70–80 (two students), 80–90 and 90–100 (two students).

join

join connects two unrelated sources by a key. The comparison uses the equals keyword, not ==, and only equality is allowed.

Inner join

Only items present on both sides come out:

var query = from s in listAverage
            join h in listHobby on s.Name equals h.Name
            select new { s.Name, s.Average, h.Hobby };

Of seven students, the four with a hobby record appear.

Left join: the 2021 way and the .NET 10 way

To keep students without a hobby, you need a left (outer) join. In 2021 that looked like this:

var query = from s in listAverage
            join h in listHobby on s.Name equals h.Name into sg
            from h in sg.DefaultIfEmpty(new MyHobby() { Hobby = "none" })
            select new { s.Name, h.Hobby };

.NET 10 adds a LeftJoin method, which is much simpler:

var query = listAverage.LeftJoin(listHobby,
    s => s.Name, h => h.Name,
    (s, h) => new { s.Name, Hobby = h?.Hobby ?? "none" });

On .NET 10 both versions return exactly the same seven rows, with "none" for the three students without a hobby.

The trap you must know: deferred execution

A LINQ query runs when you read it, not when you write it. This was the biggest gap in the 2021 article.

var src = new List<int> { 1, 2, 3 };
var evens = from n in src where n % 2 == 0 select n;   // nothing has run yet
src.Add(4);
Console.WriteLine(string.Join(" ", evens));             // 2 4  <- the 4 added later appears

int calls = 0;
var q = src.Where(n => { calls++; return n > 0; });
_ = q.Count(); _ = q.Count();
Console.WriteLine(calls);                               // 10  <- 5 items x 2 runs, from scratch each time

If you need a fixed result or will use it more than once, materialize it once with ToList() or ToArray(). This matters most for database queries (IQueryable), which send a new query every time they are enumerated.

Speed against a loop (measured)

Sum of the even numbers among 10 million ints, Release build, .NET 10, Apple M1 Pro, three runs:

Method Run 1 Run 2 Run 3
foreach + if 15.4 ms 7.8 ms 8.1 ms
Where(...).Sum(...) 24.2 ms 21.7 ms 21.8 ms

For simple loops LINQ was two to three times slower, because of delegate calls and enumerator overhead. At about 20 ms for 10 million items, readability still wins in most code. Consider a hand-written loop only on paths that run thousands of times per second.

Summary

Clause Purpose Method syntax
from … in source and range variable —
where filter Where
orderby sort OrderBy, OrderByDescending, ThenBy
select shape the result Select
group … by group GroupBy
join … on … equals connect Join, GroupJoin, LeftJoin (.NET 10)

See Microsoft's >LINQ documentation for more.

FAQ

When does a LINQ query actually run?

When you enumerate it (foreach, ToList, Count…), not when you define it. Items added to the source list after the query was defined still show up. Calling Count() twice on the same query runs the predicate twice.

How do I do a left outer join in LINQ?

Since .NET 10 there is a LeftJoin method. Before that, the pattern was join … into group, then from … in group.DefaultIfEmpty(). We verified on .NET 10 that both return identical results.

Is LINQ slower than a for/foreach loop?

For simple work, somewhat. Summing even numbers over 10 million ints took about 8 ms with foreach and about 22 ms with Where+Sum (Release, Apple M1 Pro). Readability usually matters more; optimize only genuinely hot loops.

  • #C#
  • #LINQ
  • #.NET
  • #dotnet10

t.laboratory

A technical blog that records real problems from the apps, servers and services we build and run — and the code that fixed them.

About · Apps

↑