ZetCode

C# LINQ

last modified November 6, 2026

In this article we show how to use Language Integrated Query (LINQ) in C#.

Language-Integrated Query (LINQ) is the name for a set of technologies based on the integration of query capabilities directly into the C# language. LINQ provides a consistent query experience for in-memory objects (LINQ to Objects), relational databases (through providers such as Entity Framework Core), and XML (LINQ to XML).

LINQ extends the language by the addition of query expressions, which are similar to SQL statements. LINQ query expressions can be used to conveniently extract and process data from arrays, enumerable classes, XML documents, relational databases, and third-party data sources.

Most LINQ queries have deferred execution. They are not executed until we iterate over the query variable, for example, in a foreach statement. Methods that return a single value, such as Count or First, and conversion methods, such as ToList, execute the query immediately.

LINQ queries can be written in the query syntax or the method syntax. The LINQ extension methods are available in the System.Linq namespace, which is imported implicitly in modern projects.

C# LINQ query and method syntax

In LINQ, we can use either the query or the method syntax. The compiler translates query expressions into method calls. A few methods, such as Append or Concat, do not have equivalents in the query syntax.

Program.cs
string[] words = ["falcon", "eagle", "sky", "tree", "water"];

var res = from word in words
          where word.Contains('a')
          select word;

Console.WriteLine(string.Join(", ", res));

var res2 = words.Where(word => word.Contains('a'));

Console.WriteLine(string.Join(", ", res2));

The example uses the query and the method syntax to find all words that contain the 'a' character.

var res = from word in words
          where word.Contains('a')
          select word;

This is the query syntax; it is similar to SQL code.

var res2 = words.Where(word => word.Contains('a'));

This is the method syntax; the methods can be chained.

$ dotnet run
falcon, eagle, water
falcon, eagle, water

C# LINQ element access

There are a couple of helper methods for accessing elements.

Program.cs
string[] words = ["falcon", "oak", "sky", "cloud", "tree", "tea", "water"];

Console.WriteLine(words.ElementAt(2));
Console.WriteLine(words.ElementAt(^2));
Console.WriteLine(words.First());
Console.WriteLine(words.Last());

Console.WriteLine(words.First(word => word.Length == 3));
Console.WriteLine(words.Last(word => word.Length == 3));

Console.WriteLine(words.FirstOrDefault(word => word.Length == 10) ?? "not found");
Console.WriteLine(words.FirstOrDefault(word => word.Length == 10, "n/a"));
Console.WriteLine(words.Single(word => word.Length == 6));

We access elements of an array.

Console.WriteLine(words.ElementAt(2));
Console.WriteLine(words.ElementAt(^2));

We get the third element and the second element from the end with ElementAt. The ^2 index counts from the end.

Console.WriteLine(words.First(word => word.Length == 3));
Console.WriteLine(words.Last(word => word.Length == 3));

The First and Last methods can also take a predicate. We get the first and last elements that have three characters. These methods throw an exception if no element matches.

Console.WriteLine(words.FirstOrDefault(word => word.Length == 10) ?? "not found");
Console.WriteLine(words.FirstOrDefault(word => word.Length == 10, "n/a"));

The FirstOrDefault method returns the default value of the type (here null) if there is no match. Since .NET 6, we can pass our own default value.

Console.WriteLine(words.Single(word => word.Length == 6));

The Single method returns the only matching element; it throws if there are zero or more than one matches.

$ dotnet run
sky
tea
falcon
water
oak
tea
not found
n/a
falcon

C# LINQ Prepend and Append

The Prepend method adds a value to the beginning of the sequence and the Append method adds a value to the end of the sequence. These methods do not modify the source collection; they return a new sequence with the extra element.

Program.cs
int[] vals = [1, 2, 3];

vals.Prepend(0);
vals.Append(4);

Console.WriteLine(string.Join(", ", vals));

var vals2 = vals.Prepend(0);
var vals3 = vals2.Append(4);

Console.WriteLine(string.Join(", ", vals3));

We prepend and append values to the array of integers.

vals.Prepend(0);
vals.Append(4);

These calls have no effect, because their results are thrown away.

var vals2 = vals.Prepend(0);
var vals3 = vals2.Append(4);

We store the returned sequences in variables.

$ dotnet run
1, 2, 3
0, 1, 2, 3, 4

C# LINQ select

The select clause or the Select method projects each element of a sequence into a new form. It selects, projects and transforms elements in a collection. The Select method is called map in other languages.

Program.cs
int[] vals = [2, 4, 6, 8];

var squared = vals.Select(e => e * e);
Console.WriteLine(string.Join(", ", squared));

string[] words = ["sky", "earth", "oak", "falcon"];

var wordLens = words.Select(e => e.Length);
Console.WriteLine(string.Join(", ", wordLens));

var numbered = words.Select((word, idx) => $"{idx + 1}. {word}");
Console.WriteLine(string.Join(", ", numbered));

We transform an array of integers into a sequence of their squares and an array of words into a sequence of word lengths.

var numbered = words.Select((word, idx) => $"{idx + 1}. {word}");

An overload of Select also passes the index of the element.

$ dotnet run
4, 16, 36, 64
3, 5, 3, 6
1. sky, 2. earth, 3. oak, 4. falcon

C# LINQ select into anonymous type

Projections are selection of specific fields from the returned objects. Projections are performed with the select clause. We can project fields into anonymous types or tuples.

Program.cs
User[] users =
[
    new(1, "John", "London", new DateOnly(2001, 4, 1)),
    new(2, "Lenny", "New York", new DateOnly(1997, 12, 11)),
    new(3, "Andrew", "Boston", new DateOnly(1987, 2, 22)),
    new(4, "Peter", "Prague", new DateOnly(1936, 3, 24)),
    new(5, "Anna", "Bratislava", new DateOnly(1973, 11, 18)),
    new(6, "Albert", "Bratislava", new DateOnly(1940, 12, 11)),
    new(7, "Adam", "Trnava", new DateOnly(1983, 12, 1)),
    new(8, "Robert", "Bratislava", new DateOnly(1935, 5, 15)),
    new(9, "Robert", "Prague", new DateOnly(1998, 3, 14)),
];

var res = from user in users
          where user.City == "Bratislava"
          select new { user.Name, user.City };

Console.WriteLine(string.Join(Environment.NewLine, res));

var res2 = users.Where(u => u.Born.Year < 1950)
                .Select(u => (u.Name, u.Born.Year));

Console.WriteLine(string.Join(Environment.NewLine, res2));

record User(int Id, string Name, string City, DateOnly Born);

We select users who live in Bratislava.

var res = from user in users
          where user.City == "Bratislava"
          select new { user.Name, user.City };

With the select new clause, we create an anonymous type with two properties: Name and City.

var res2 = users.Where(u => u.Born.Year < 1950)
                .Select(u => (u.Name, u.Born.Year));

Here we project the users born before 1950 into tuples. Unlike anonymous types, tuples can be returned from methods.

$ dotnet run
{ Name = Anna, City = Bratislava }
{ Name = Albert, City = Bratislava }
{ Name = Robert, City = Bratislava }
(Peter, 1936)
(Albert, 1940)
(Robert, 1935)

C# LINQ SelectMany

The SelectMany method flattens sequences into a single sequence.

Program.cs
int[][] vals =
[
    [1, 2, 3],
    [4],
    [5, 6, 6, 2, 7, 8],
];

var res = vals.SelectMany(nested => nested).Order();
Console.WriteLine(string.Join(", ", res));

List<List<int>> vals2 =
[
    [1, 2, 3, 3],
    [4],
    [5, 6, 6, 7, 7]
];

var res2 = vals2.SelectMany(list => list)
                .Distinct()
                .OrderDescending();

Console.WriteLine(string.Join(", ", res2));

string[] sentences = ["an old falcon", "the blue sky"];

var words = sentences.SelectMany(s => s.Split(' '));
Console.WriteLine(string.Join(", ", words));

We flatten nested collections.

var res = vals.SelectMany(nested => nested).Order();

We have an array of arrays. With the SelectMany method, we flatten it into a single sequence of values. The values are also ordered.

var res2 = vals2.SelectMany(list => list)
                .Distinct()
                .OrderDescending();

We flatten the nested lists into a sequence of unique values. The Distinct method removes duplicates.

var words = sentences.SelectMany(s => s.Split(' '));

The selector may return any sequence; here we split sentences into words.

$ dotnet run
1, 2, 2, 3, 4, 5, 6, 6, 7, 8
7, 6, 5, 4, 3, 2, 1
an, old, falcon, the, blue, sky

C# LINQ Concat

The Concat method concatenates two sequences.

Program.cs
User[] users1 =
[
    new("John", "Doe", "gardener"),
    new("Jane", "Doe", "teacher"),
    new("Roger", "Roe", "driver")
];

User[] users2 =
[
    new("Peter", "Smith", "teacher"),
    new("Lucia", "Black", "accountant"),
    new("Michael", "Novak", "programmer")
];

var allUsers = users1.Concat(users2);

foreach (var user in allUsers)
{
    Console.WriteLine(user);
}

record User(string FirstName, string LastName, string Occupation);

We have two arrays of users. We merge them with Concat.

$ dotnet run
User { FirstName = John, LastName = Doe, Occupation = gardener }
User { FirstName = Jane, LastName = Doe, Occupation = teacher }
User { FirstName = Roger, LastName = Roe, Occupation = driver }
User { FirstName = Peter, LastName = Smith, Occupation = teacher }
User { FirstName = Lucia, LastName = Black, Occupation = accountant }
User { FirstName = Michael, LastName = Novak, Occupation = programmer }

C# LINQ filter

We can filter data with the where clause. The conditions can be combined with the && or || operators. The LINQ Where article covers filtering in more detail.

Program.cs
List<string> words = [ "sky", "rock", "forest", "new",
    "falcon", "jewelry", "small", "eagle", "blue", "gray" ];

var res = from word in words
          where word.Length == 4
          select word;

Console.WriteLine(string.Join(", ", res));

var res2 = from word in words
           where word.StartsWith('f') || word.StartsWith('s')
           select word;

Console.WriteLine(string.Join(", ", res2));

List<Car> cars =
[
    new("Audi", 52642),
    new("Mercedes", 57127),
    new("Skoda", 9000),
    new("Volvo", 29000),
    new("Bentley", 350000),
    new("Citroen", 21000),
    new("Hummer", 41400),
    new("Volkswagen", 21600),
];

var res3 = from car in cars
           where car.Price > 30000 && car.Price < 100000
           select car;

foreach (var car in res3)
{
    Console.WriteLine($"{car.Name} {car.Price}");
}

record Car(string Name, int Price);

We filter words and cars.

var res = from word in words
          where word.Length == 4
          select word;

We pick all words that have four letters.

where word.StartsWith('f') || word.StartsWith('s')

We pick all words that start with either the 'f' or the 's' character.

where car.Price > 30000 && car.Price < 100000

We include all cars whose price is between 30000 and 100000.

$ dotnet run
rock, blue, gray
sky, forest, falcon, small
Audi 52642
Mercedes 57127
Hummer 41400

C# LINQ Cartesian product

A Cartesian product is the multiplication of two sets to form the set of all ordered pairs.

Program.cs
var letters = "abcdefgh";
var digits = "12345678";

var coords = from l in letters
             from d in digits
             select $"{l}{d}";

foreach (var row in coords.Chunk(8))
{
    Console.WriteLine(string.Join(" ", row));
}

We create a Cartesian product of letters and digits; the results are the coordinates of a chessboard.

var coords = from l in letters
             from d in digits
             select $"{l}{d}";

To accomplish the task, we use two from clauses. The compiler translates them into a SelectMany call.

foreach (var row in coords.Chunk(8))

We print the coordinates in rows of eight.

$ dotnet run
a1 a2 a3 a4 a5 a6 a7 a8
b1 b2 b3 b4 b5 b6 b7 b8
c1 c2 c3 c4 c5 c6 c7 c8
d1 d2 d3 d4 d5 d6 d7 d8
e1 e2 e3 e4 e5 e6 e7 e8
f1 f2 f3 f4 f5 f6 f7 f8
g1 g2 g3 g4 g5 g6 g7 g8
h1 h2 h3 h4 h5 h6 h7 h8

C# LINQ Zip

The Zip method takes elements from two or three sequences and combines them. The groups are created from elements at the same position.

Program.cs
string[] students = ["Adam", "John", "Lucia", "Tom"];
int[] scores = [68, 56, 90, 86];

var result = students.Zip(scores, (e1, e2) => $"{e1}'s score: {e2}");

foreach (var user in result)
{
    Console.WriteLine(user);
}

int[] left = [1, 2, 3];
int[] right = [10, 20, 30];

var products = left.Zip(right, (m, n) => m * n);
Console.WriteLine(string.Join(", ", products));

int[] codes = [.. Enumerable.Range(1, 5)];
string[] states = ["Alabama", "Alaska", "Arizona", "Arkansas", "California"];
string[] capitals = ["Montgomery", "Juneau", "Phoenix", "Little Rock", "Sacramento"];

foreach (var (code, state, capital) in codes.Zip(states, capitals))
{
    Console.WriteLine($"{code}: {state}, {capital}");
}

We use the Zip method in three cases.

var result = students.Zip(scores, (e1, e2) => $"{e1}'s score: {e2}");

We have an array of students and a corresponding array of scores. The e1 comes from the students array and the e2 from the scores array, from the same position.

var products = left.Zip(right, (m, n) => m * n);

Here we multiply the values of two arrays.

foreach (var (code, state, capital) in codes.Zip(states, capitals))

Without a result selector, Zip returns tuples. Since .NET 6, it can combine three sequences.

$ dotnet run
Adam's score: 68
John's score: 56
Lucia's score: 90
Tom's score: 86
10, 40, 90
1: Alabama, Montgomery
2: Alaska, Juneau
3: Arizona, Phoenix
4: Arkansas, Little Rock
5: California, Sacramento

C# LINQ built-in aggregate calculations

LINQ allows us to calculate aggregate values, such as min, max, or sum.

Program.cs
List<int> vals = [6, 2, -3, 4, -5, 9, 7, 8];

var n1 = vals.Count();
Console.WriteLine($"There are {n1} elements");

var n2 = vals.Count(e => e % 2 == 0);
Console.WriteLine($"There are {n2} even elements");

var sum = vals.Sum();
Console.WriteLine($"The sum of all values is: {sum}");

var s2 = vals.Sum(e => e > 0 ? e : 0);
Console.WriteLine($"The sum of all positive values is: {s2}");

var avg = vals.Average();
Console.WriteLine($"The average of values is: {avg}");

var max = vals.Max();
Console.WriteLine($"The maximum value is: {max}");

var min = vals.Min();
Console.WriteLine($"The minimum value is: {min}");

List<string> words = ["sky", "forest", "cloud", "rain", "owl"];

var letters = (from word in words
               where word.Length > 3
               select word.Length).Sum();

Console.WriteLine($"Words longer than three have {letters} letters");

We use the Count, Sum, Average, Max, and Min methods.

var s2 = vals.Sum(e => e > 0 ? e : 0);

The aggregate methods can take a selector; we sum only the positive values.

var letters = (from word in words
               where word.Length > 3
               select word.Length).Sum();

The query syntax has no aggregate keywords; we call the method on the query wrapped in parentheses.

$ dotnet run
There are 8 elements
There are 4 even elements
The sum of all values is: 28
The sum of all positive values is: 36
The average of values is: 3.5
The maximum value is: 9
The minimum value is: -5
Words longer than three have 15 letters

C# LINQ custom aggregate calculations

Custom aggregate calculations can be computed with Aggregate. It applies an accumulator function over a sequence.

Program.cs
List<int> vals = [6, 2, -3, 4, -5, 9, 7, 8];

var sum = vals.Aggregate((total, next) => total + next);
Console.WriteLine($"The sum is {sum}");

var product = vals.Aggregate((total, next) => total * next);
Console.WriteLine($"The product is {product}");

List<string> words = ["an", "old", "falcon"];

var sentence = words.Aggregate("Words:", (acc, word) => $"{acc} [{word}]");
Console.WriteLine(sentence);

var longest = words.Aggregate("", (acc, w) => w.Length > acc.Length ? w : acc,
    w => w.ToUpper());
Console.WriteLine(longest);

We calculate the sum and the product of values in the list with Aggregate.

var sentence = words.Aggregate("Words:", (acc, word) => $"{acc} [{word}]");

This overload takes a seed, the initial value of the accumulator. The result type can differ from the element type.

var longest = words.Aggregate("", (acc, w) => w.Length > acc.Length ? w : acc,
    w => w.ToUpper());

The third overload adds a function that transforms the final accumulator value. We find the longest word and turn it to uppercase.

$ dotnet run
The sum is 28
The product is 362880
Words: [an] [old] [falcon]
FALCON

C# LINQ orderby

With the OrderBy method or the orderby clause we can sort the elements of a sequence.

Program.cs
int[] vals = [4, 5, 3, 2, 7, 0, 1, 6];

var result = from e in vals
             orderby e ascending
             select e;

Console.WriteLine(string.Join(", ", result));

var result2 = from e in vals
              orderby e descending
              select e;

Console.WriteLine(string.Join(", ", result2));

We sort the integers in ascending and descending order. The ascending keyword is optional.

$ dotnet run
0, 1, 2, 3, 4, 5, 6, 7
7, 6, 5, 4, 3, 2, 1, 0

C# LINQ sort by multiple fields

In the next example, we sort objects by multiple fields.

Program.cs
List<User> users =
[
    new("John", "Doe", 1230),
    new("Lucy", "Novak", 670),
    new("Ben", "Walter", 2050),
    new("Robin", "Brown", 2300),
    new("Amy", "Doe", 1250),
    new("Joe", "Draker", 1190),
    new("Janet", "Doe", 980),
    new("Albert", "Novak", 1930),
];

Console.WriteLine("sort ascending by last name and salary");

var sortedUsers = users.OrderBy(u => u.LastName).ThenBy(u => u.Salary);

foreach (var user in sortedUsers)
{
    Console.WriteLine(user);
}

Console.WriteLine("sort descending by last name and salary");

var sortedUsers2 = from u in users
                   orderby u.LastName descending, u.Salary descending
                   select u;

foreach (var user in sortedUsers2)
{
    Console.WriteLine(user);
}

record User(string FirstName, string LastName, int Salary);

We sort the users first by their last names, then by their salaries.

var sortedUsers = users.OrderBy(u => u.LastName).ThenBy(u => u.Salary);

We sort the users by their last names and then by their salaries in ascending order.

var sortedUsers2 = from u in users
                   orderby u.LastName descending, u.Salary descending
                   select u;

In the query syntax, the sort keys are separated with commas. Here, we sort the users by their last names and then by their salaries in descending order.

$ dotnet run
sort ascending by last name and salary
User { FirstName = Robin, LastName = Brown, Salary = 2300 }
User { FirstName = Janet, LastName = Doe, Salary = 980 }
User { FirstName = John, LastName = Doe, Salary = 1230 }
User { FirstName = Amy, LastName = Doe, Salary = 1250 }
User { FirstName = Joe, LastName = Draker, Salary = 1190 }
User { FirstName = Lucy, LastName = Novak, Salary = 670 }
User { FirstName = Albert, LastName = Novak, Salary = 1930 }
User { FirstName = Ben, LastName = Walter, Salary = 2050 }
sort descending by last name and salary
User { FirstName = Ben, LastName = Walter, Salary = 2050 }
User { FirstName = Albert, LastName = Novak, Salary = 1930 }
User { FirstName = Lucy, LastName = Novak, Salary = 670 }
User { FirstName = Joe, LastName = Draker, Salary = 1190 }
User { FirstName = Amy, LastName = Doe, Salary = 1250 }
User { FirstName = John, LastName = Doe, Salary = 1230 }
User { FirstName = Janet, LastName = Doe, Salary = 980 }
User { FirstName = Robin, LastName = Brown, Salary = 2300 }

C# LINQ MaxBy & MinBy

With the MaxBy and MinBy methods we can find the elements with the maximum and minimum values of the given property.

Program.cs
List<User> users =
[
    new("John", "Doe", 1230),
    new("Lucy", "Novak", 670),
    new("Ben", "Walter", 2050),
    new("Robin", "Brown", 2300),
    new("Amy", "Doe", 1250),
    new("Joe", "Draker", 1190),
    new("Janet", "Doe", 980),
    new("Albert", "Novak", 1930),
];

Console.WriteLine("User with max salary:");
var u1 = users.MaxBy(u => u.Salary);
Console.WriteLine(u1);

Console.WriteLine("User with min salary:");
var u2 = users.MinBy(u => u.Salary);
Console.WriteLine(u2);

Console.WriteLine($"Max salary: {users.Max(u => u.Salary)}");

record User(string FirstName, string LastName, int Salary);

We find the user with the maximum and minimum salary.

Console.WriteLine($"Max salary: {users.Max(u => u.Salary)}");

Note the difference: Max with a selector returns the maximum value, while MaxBy returns the element having that value.

$ dotnet run
User with max salary:
User { FirstName = Robin, LastName = Brown, Salary = 2300 }
User with min salary:
User { FirstName = Lucy, LastName = Novak, Salary = 670 }
Max salary: 2300

C# LINQ Reverse

The Reverse method inverts the order of the elements in a sequence. (Note that this is not the same as sorting in descending order.)

Program.cs
int[] vals = [1, 3, 6, 0, -1, 2, 9, 9, 8];

var reversed = vals.Reverse();
Console.WriteLine(string.Join(", ", reversed));

var reversed2 = (from val in vals select val).Reverse();
Console.WriteLine(string.Join(", ", reversed2));

Console.WriteLine(string.Join(", ", vals));

Array.Reverse(vals);
Console.WriteLine(string.Join(", ", vals));

We reverse the elements of an array using both the method and the query syntax.

Console.WriteLine(string.Join(", ", vals));

Array.Reverse(vals);

The LINQ Reverse returns a new sequence; the original array is unchanged. To reverse an array in place, we use Array.Reverse.

$ dotnet run
8, 9, 9, 2, -1, 0, 6, 3, 1
8, 9, 9, 2, -1, 0, 6, 3, 1
1, 3, 6, 0, -1, 2, 9, 9, 8
8, 9, 9, 2, -1, 0, 6, 3, 1

C# LINQ group by

We can group data into categories based on a certain key.

Program.cs
List<Car> cars =
[
    new("Audi", "red", 52642),
    new("Mercedes", "blue", 57127),
    new("Skoda", "black", 9000),
    new("Volvo", "red", 29000),
    new("Bentley", "yellow", 350000),
    new("Citroen", "white", 21000),
    new("Hummer", "black", 41400),
    new("Volkswagen", "white", 21600),
];

var groups = from car in cars
             group car by car.Colour;

foreach (var group in groups)
{
    Console.WriteLine(group.Key);

    foreach (var car in group)
    {
        Console.WriteLine($"  {car.Name} {car.Price}");
    }
}

record Car(string Name, string Colour, int Price);

We separate the available cars into groups by their colour.

var groups = from car in cars
             group car by car.Colour;

Each group implements IGrouping<TKey, TElement>. It has a Key property and it is a sequence of the group's elements.

$ dotnet run
red
  Audi 52642
  Volvo 29000
blue
  Mercedes 57127
black
  Skoda 9000
  Hummer 41400
yellow
  Bentley 350000
white
  Citroen 21000
  Volkswagen 21600

C# LINQ group by and aggregate

In the following example, we perform grouping and aggregation operations.

Program.cs
Revenue[] revenues =
[
    new(1, "Q1", 2340),
    new(2, "Q1", 1200),
    new(3, "Q1", 980),
    new(4, "Q2", 340),
    new(5, "Q2", 780),
    new(6, "Q3", 2010),
    new(7, "Q3", 3370),
    new(8, "Q4", 540),
];

var res = from revenue in revenues
          group revenue by revenue.Quarter
          into g
          select new { Quarter = g.Key, Total = g.Sum(e => e.Amount) };

foreach (var line in res)
{
    Console.WriteLine(line);
}

Console.WriteLine("quarters with two revenues:");

var res2 = from revenue in revenues
           group revenue by revenue.Quarter
           into g
           where g.Count() == 2
           select new { Quarter = g.Key, Total = g.Sum(c => c.Amount) };

foreach (var line in res2)
{
    Console.WriteLine(line);
}

record Revenue(int Id, string Quarter, int Amount);

We have revenues for four quarters. We group the revenues by the quarters and sum the amounts.

group revenue by revenue.Quarter
into g
select new { Quarter = g.Key, Total = g.Sum(e => e.Amount) };

The into keyword stores the groups in the g variable, so that the query can continue with them.

where g.Count() == 2

We can filter the groups with the where clause. In SQL, this corresponds to the HAVING clause. We pick only those quarters which have exactly two revenues.

$ dotnet run
{ Quarter = Q1, Total = 4520 }
{ Quarter = Q2, Total = 1120 }
{ Quarter = Q3, Total = 5380 }
{ Quarter = Q4, Total = 540 }
quarters with two revenues:
{ Quarter = Q2, Total = 1120 }
{ Quarter = Q3, Total = 5380 }

C# LINQ CountBy, AggregateBy and Index

.NET 9 added the CountBy, AggregateBy, and Index methods. The first two compute per-key results without building the intermediate groups.

Program.cs
Revenue[] revenues =
[
    new(1, "Q1", 2340),
    new(2, "Q1", 1200),
    new(3, "Q1", 980),
    new(4, "Q2", 340),
    new(5, "Q2", 780),
    new(6, "Q3", 2010),
    new(7, "Q3", 3370),
    new(8, "Q4", 540),
];

foreach (var (quarter, count) in revenues.CountBy(r => r.Quarter))
{
    Console.WriteLine($"{quarter}: {count} revenues");
}

var totals = revenues.AggregateBy(r => r.Quarter, 0, (sum, r) => sum + r.Amount);

foreach (var (quarter, total) in totals)
{
    Console.WriteLine($"{quarter}: {total}");
}

foreach (var (idx, r) in revenues.Take(3).Index())
{
    Console.WriteLine($"{idx}: {r}");
}

record Revenue(int Id, string Quarter, int Amount);

We count and sum the revenues of each quarter.

foreach (var (quarter, count) in revenues.CountBy(r => r.Quarter))

The CountBy method returns key-value pairs with the key and the number of its elements.

var totals = revenues.AggregateBy(r => r.Quarter, 0, (sum, r) => sum + r.Amount);

The AggregateBy method takes a key selector, a seed, and an accumulator function. It applies the accumulator separately to each key.

foreach (var (idx, r) in revenues.Take(3).Index())

The Index method pairs each element with its position.

$ dotnet run
Q1: 3 revenues
Q2: 2 revenues
Q3: 2 revenues
Q4: 1 revenues
Q1: 4520
Q2: 1120
Q3: 5380
Q4: 540
0: Revenue { Id = 1, Quarter = Q1, Amount = 2340 }
1: Revenue { Id = 2, Quarter = Q1, Amount = 1200 }
2: Revenue { Id = 3, Quarter = Q1, Amount = 980 }

C# LINQ Chunk

The Chunk method splits the elements of a sequence into chunks of the given size.

Program.cs
List<int> vals = [1, 2, 3, 4, 5, 6, 7, 8];
IEnumerable<int[]> chunks = vals.Chunk(2);

int res = 0;

foreach (var chunk in chunks)
{
    res += chunk[0] * chunk[1];
}

Console.WriteLine(res);

foreach (var chunk in Enumerable.Range(1, 10).Chunk(4))
{
    Console.WriteLine($"[{string.Join(", ", chunk)}]");
}

In the program, we compute the following expression: x1 * x2 + x3 * x4 ...

foreach (var chunk in Enumerable.Range(1, 10).Chunk(4))

The last chunk may be smaller than the requested size.

$ dotnet run
100
[1, 2, 3, 4]
[5, 6, 7, 8]
[9, 10]

C# LINQ word frequency

In the next example, we count the frequency of words in a file.

$ wget https://raw.githubusercontent.com/janbodnar/data/main/the-king-james-bible.txt

We use the King James Bible.

Program.cs
using System.Text.RegularExpressions;

var fileName = "the-king-james-bible.txt";
var text = File.ReadAllText(fileName);

var words = Regex.Matches(text, "[A-Za-z']+").Select(m => m.Value);

var res = words
    .CountBy(w => w, StringComparer.OrdinalIgnoreCase)
    .OrderByDescending(e => e.Value)
    .Take(10);

foreach (var (word, count) in res)
{
    Console.WriteLine($"{word}: {count}");
}

var res2 = words
    .GroupBy(w => w)
    .Select(g => new { Word = g.Key, Count = g.Count() })
    .OrderByDescending(e => e.Count)
    .Take(3);

Console.WriteLine(string.Join(", ", res2));

We count the frequency of the words from the King James Bible.

var words = Regex.Matches(text, "[A-Za-z']+").Select(m => m.Value);

We find all words with the Regex.Matches method and get their values.

var res = words
    .CountBy(w => w, StringComparer.OrdinalIgnoreCase)
    .OrderByDescending(e => e.Value)
    .Take(10);

We count the words case-insensitively and take the ten most frequent ones. The comparer makes The and the the same key; the key is reported in the form in which it was first found.

var res2 = words
    .GroupBy(w => w)
    .Select(g => new { Word = g.Key, Count = g.Count() })
    .OrderByDescending(e => e.Count)
    .Take(3);

Before .NET 9, the same task was done with GroupBy. This version is case-sensitive, so the counts are lower.

$ dotnet run
The: 64020
and: 51696
of: 34668
to: 13580
that: 12912
In: 12667
he: 10419
shall: 9838
unto: 8997
for: 8971
{ Word = the, Count = 62103 }, { Word = and, Count = 38848 }, { Word = of, Count = 34478 }

C# LINQ join

The join clause joins sequences. The LINQ Join article covers joins in more detail.

Program.cs
string[] basketA = ["coin", "book", "fork", "cord", "needle"];
string[] basketB = ["watches", "coin", "pen", "book", "pencil"];

var res = from item1 in basketA
          join item2 in basketB
          on item1 equals item2
          select item1;

Console.WriteLine(string.Join(", ", res));

Department[] departments = [new(1, "Sales"), new(2, "IT"), new(3, "Legal")];

Employee[] employees =
[
    new("John", 1), new("Lucy", 2), new("Peter", 2), new("Anna", 4)
];

var res2 = employees.LeftJoin(departments, e => e.DeptId, d => d.Id,
    (e, d) => $"{e.Name} - {d?.Name ?? "no department"}");

Console.WriteLine(string.Join(Environment.NewLine, res2));

record Department(int Id, string Name);
record Employee(string Name, int DeptId);

With the join clause, we find all items that are present in both arrays.

var res2 = employees.LeftJoin(departments, e => e.DeptId, d => d.Id,
    (e, d) => $"{e.Name} - {d?.Name ?? "no department"}");

The LeftJoin method was added in .NET 10. It keeps all elements of the first sequence; when there is no matching element in the second sequence, the result selector receives null.

$ dotnet run
coin, book
John - Sales
Lucy - IT
Peter - IT
Anna - no department

C# LINQ partitioning

The Skip method skips the specified number of elements from the start of the sequence and returns the remaining elements. The SkipLast method returns the sequence without the specified number of last elements. The SkipWhile method skips elements in a sequence as long as the specified condition is true and then returns the remaining elements.

The Take method returns the specified number of contiguous elements from the start of a sequence. The TakeLast method returns the specified number of last elements. The TakeWhile method returns elements from a sequence as long as a specified condition is true, and then skips the remaining elements.

Note: The SkipWhile and TakeWhile methods stop at the first non-matching element.
Program.cs
int[] vals = [1, 2, 7, 8, 5, 6, 3, 4, 9, 10];

var res1 = vals.Skip(3);
Console.WriteLine(string.Join(", ", res1));

var res2 = vals.SkipLast(3);
Console.WriteLine(string.Join(", ", res2));

var res3 = vals.SkipWhile(e => e < 5);
Console.WriteLine(string.Join(", ", res3));

Console.WriteLine("----------");

var res4 = vals.Take(3);
Console.WriteLine(string.Join(", ", res4));

var res5 = vals.TakeLast(3);
Console.WriteLine(string.Join(", ", res5));

var res6 = vals.TakeWhile(e => e < 5);
Console.WriteLine(string.Join(", ", res6));

var res7 = vals.Take(2..5);
Console.WriteLine(string.Join(", ", res7));

The example uses the partitioning methods.

var res7 = vals.Take(2..5);

Since .NET 6, Take also accepts a range. We get the elements at indexes 2, 3, and 4.

$ dotnet run
8, 5, 6, 3, 4, 9, 10
1, 2, 7, 8, 5, 6, 3
7, 8, 5, 6, 3, 4, 9, 10
----------
1, 2, 7
4, 9, 10
1, 2
7, 8, 5

C# LINQ conversions

We can transform the returned sequence into an array, list, dictionary, set, or lookup.

Program.cs
User[] users =
[
    new(1, "John", "London"),
    new(2, "Lenny", "New York"),
    new(3, "Andrew", "Boston"),
    new(4, "Peter", "Prague"),
    new(5, "Anna", "Bratislava"),
    new(6, "Albert", "Bratislava"),
    new(7, "Adam", "Trnava"),
    new(8, "Robert", "Bratislava"),
    new(9, "Robert", "Prague"),
];

string[] cities = users.Select(u => u.City).Distinct().ToArray();
Console.WriteLine(string.Join(", ", cities));

List<User> inBratislava = (from user in users
                           where user.City == "Bratislava"
                           select user).ToList();

inBratislava.ForEach(Console.WriteLine);

Dictionary<int, string> userIds = users.ToDictionary(u => u.Id, u => u.Name);
Console.WriteLine(userIds[7]);

HashSet<string> names = users.Select(u => u.Name).ToHashSet();
Console.WriteLine(names.Count);

ILookup<string, User> byCity = users.ToLookup(u => u.City);
Console.WriteLine(string.Join(", ", byCity["Prague"].Select(u => u.Name)));
Console.WriteLine(byCity["Vienna"].Count());

record User(int Id, string Name, string City);

We execute several queries on our data source; the results are converted to various collection types.

string[] cities = users.Select(u => u.City).Distinct().ToArray();

We select all cities, apply the Distinct method and finally call the ToArray method.

Dictionary<int, string> userIds = users.ToDictionary(u => u.Id, u => u.Name);

We turn the user ids and names into a dictionary. The keys must be unique; otherwise ToDictionary throws an exception.

HashSet<string> names = users.Select(u => u.Name).ToHashSet();

The set contains eight names, since Robert appears twice.

ILookup<string, User> byCity = users.ToLookup(u => u.City);

A lookup is an immutable dictionary that maps keys to sequences of values. For a missing key, it returns an empty sequence instead of throwing.

$ dotnet run
London, New York, Boston, Prague, Bratislava, Trnava
User { Id = 5, Name = Anna, City = Bratislava }
User { Id = 6, Name = Albert, City = Bratislava }
User { Id = 8, Name = Robert, City = Bratislava }
Adam
8
Peter, Robert
0

C# LINQ generating sequences

The Range, Repeat, Empty, Sequence, and InfiniteSequence methods can be used to generate sequences.

Program.cs
var res = Enumerable.Range(1, 10).Select(e => e * e * e);
Console.WriteLine(string.Join(", ", res));

int[] vals = [8, 4, 3, 2, 5, 11, 15, 10, 3, 5, 6];

foreach (var e in vals)
{
    Console.WriteLine(string.Concat(Enumerable.Repeat("*", e)));
}

var none = Enumerable.Empty<int>();
Console.WriteLine($"empty: {none.Count()}, sum: {none.Sum()}");

var evens = Enumerable.Sequence(0, 20, 4);
Console.WriteLine(string.Join(", ", evens));

var powers = Enumerable.InfiniteSequence(1, 1).Select(e => 1 << e).Take(8);
Console.WriteLine(string.Join(", ", powers));

We generate several sequences.

var res = Enumerable.Range(1, 10).Select(e => e * e * e);

With Range, we generate integers 1 through 10 and then cube them.

Console.WriteLine(string.Concat(Enumerable.Repeat("*", e)));

With the help of the Repeat method, we generate a horizontal bar for each of the values from the vals array.

var none = Enumerable.Empty<int>();

The Empty method returns an empty sequence.

var evens = Enumerable.Sequence(0, 20, 4);

The Sequence method, added in .NET 10, generates values from a start to an inclusive end with a step.

var powers = Enumerable.InfiniteSequence(1, 1).Select(e => 1 << e).Take(8);

The InfiniteSequence method, also new in .NET 10, never ends; we limit it with Take.

$ dotnet run
1, 8, 27, 64, 125, 216, 343, 512, 729, 1000
********
****
***
**
*****
***********
***************
**********
***
*****
******
empty: 0, sum: 0
0, 4, 8, 12, 16, 20
2, 4, 8, 16, 32, 64, 128, 256

C# LINQ quantifiers

With quantifiers, we check for certain conditions.

Program.cs
List<int> vals = [-1, -3, 0, 1, -3, 2, 9, -4];

bool positive = vals.Any(x => x > 0);
Console.WriteLine($"There is a positive value: {positive}");

bool allPositive = vals.All(x => x > 0);
Console.WriteLine($"All values are positive: {allPositive}");

bool hasSix = vals.Contains(6);
Console.WriteLine($"The list contains 6: {hasSix}");

List<int> empty = [];
Console.WriteLine($"Any in empty list: {empty.Any()}");
Console.WriteLine($"All in empty list: {empty.All(x => x > 0)}");

With the Any method, we check whether any element in the list is a positive value. With the All method, we check whether all elements in the list are positive. Finally, with the Contains method we determine whether the list contains the value six.

Console.WriteLine($"All in empty list: {empty.All(x => x > 0)}");

For an empty sequence, All returns true, since there is no element that violates the condition.

$ dotnet run
There is a positive value: True
All values are positive: False
The list contains 6: False
Any in empty list: False
All in empty list: True

C# LINQ set operations

LINQ has methods performing set operations, including Union, Intersect, Except, and Distinct. Since .NET 6, they have variants that compare elements by a key.

Program.cs
var vals1 = "abcde";
var vals2 = "defgh";

var data = vals1.Union(vals2);
Console.WriteLine("{" + string.Join(" ", data) + "}");

var data2 = vals1.Intersect(vals2);
Console.WriteLine("{" + string.Join(" ", data2) + "}");

var data3 = vals1.Except(vals2);
Console.WriteLine("{" + string.Join(" ", data3) + "}");

int[] nums = [1, 1, 2, 3, 4, 4, 4, 5, 6, 7, 7, 8];

var data4 = nums.Distinct();
Console.WriteLine("{" + string.Join(" ", data4) + "}");

User[] users = [new("John", "Prague"), new("Lucy", "Vienna"), new("Peter", "Prague")];

var onePerCity = users.DistinctBy(u => u.City);
Console.WriteLine(string.Join(", ", onePerCity));

string[] banned = ["Vienna"];
var allowed = users.ExceptBy(banned, u => u.City);
Console.WriteLine(string.Join(", ", allowed));

record User(string Name, string City);

We do set operations on characters of two strings and on an array.

var data = vals1.Union(vals2);

The Union method produces the set union of the two sequences.

var data2 = vals1.Intersect(vals2);

The Intersect method produces the set intersection.

var data3 = vals1.Except(vals2);

The Except method produces the set difference.

var data4 = nums.Distinct();

The Distinct method returns distinct elements from the array.

var onePerCity = users.DistinctBy(u => u.City);

The DistinctBy method keeps the first user from each city.

var allowed = users.ExceptBy(banned, u => u.City);

The ExceptBy method removes users whose city is in the banned list. Note that it is a set operation: its result also contains each key only once, so Peter, the second user from Prague, is dropped too. To merely filter elements, use Where.

$ dotnet run
{a b c d e f g h}
{d e}
{a b c}
{1 2 3 4 5 6 7 8}
User { Name = John, City = Prague }, User { Name = Lucy, City = Vienna }
User { Name = John, City = Prague }

C# LINQ XML

LINQ can be used to process XML.

Program.cs
using System.Xml.Linq;

string xml = """
    <Users>
        <User>
            <Name>Jack</Name>
            <Sex>male</Sex>
        </User>
        <User>
            <Name>Paul</Name>
            <Sex>male</Sex>
        </User>
        <User>
            <Name>Frank</Name>
            <Sex>male</Sex>
        </User>
        <User>
            <Name>Martina</Name>
            <Sex>female</Sex>
        </User>
        <User>
            <Name>Lucia</Name>
            <Sex>female</Sex>
        </User>
    </Users>
    """;

var xdoc = XDocument.Parse(xml);

var females = from u in xdoc.Root!.Elements("User")
              where (string?)u.Element("Sex") == "female"
              select (string?)u.Element("Name");

foreach (var e in females)
{
    Console.WriteLine(e);
}

We parse the XML data and choose all female names.

var females = from u in xdoc.Root!.Elements("User")
              where (string?)u.Element("Sex") == "female"
              select (string?)u.Element("Name");

The Elements method returns the child elements with the given name. Casting an XElement to string returns its text value; the cast to a nullable string returns null for a missing element.

$ dotnet run
Martina
Lucia

Source

Language Integrated Query (LINQ)

In this article we have worked with LINQ in C#.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all C# tutorials.