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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.)
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.
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.
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.
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.
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.
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.
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.
SkipWhile and TakeWhile
methods stop at the first non-matching element.
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.
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.
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.
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.
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.
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
List all C# tutorials.