Language Integrated Query, or LINQ, gives C# a consistent way to filter, transform, sort, group, join, and summarize data. The same query concepts work with in-memory collections and with providers that translate expression trees for databases or other sources.
Import System.Linq when needed. A LINQ query describes the result you want; the source and chosen provider determine how it executes.
Create a LINQ Query
Most sequence operators accept lambda expressions. Chain operators in the order that data should flow through them.
Example:
using System.Linq;
int[] scores = [72, 91, 84, 67, 95];
var highScores = scores
.Where(score => score >= 80) // Keep matching values.
.OrderByDescending(score => score);
foreach (int score in highScores)
{
Console.WriteLine(score);
}
Method Syntax and Query Syntax
C# supports method syntax and SQL-like query syntax. The compiler translates query expressions into method calls. Some operations, including Count() and Max(), have no query-clause form.
string[] names = ["Aarav", "Olivia", "Harry", "Mia"];
// Query syntax.
var queryForm =
from name in names
where name.Length >= 5
orderby name
select name.ToUpperInvariant();
// Equivalent method syntax.
var methodForm = names
.Where(name => name.Length >= 5)
.OrderBy(name => name)
.Select(name => name.ToUpperInvariant());
| Operation | Common operators |
|---|---|
| Filtering | Where, OfType |
| Projection | Select, SelectMany |
| Ordering | OrderBy, ThenBy, Reverse |
| Grouping | GroupBy, ToLookup |
| Joining | Join, GroupJoin |
| Aggregation | Count, Sum, Average, Min, Max, Aggregate |
| Element selection | First, Single, ElementAt and OrDefault variants |
Project Results with Select
Projection changes the shape of each result. Create an anonymous object when downstream code needs only selected values.
record Product(int Id, string Name, decimal Price);
Product[] products =
[
new(1, "Keyboard", 3200m),
new(2, "Mouse", 1400m)
];
var cards = products.Select(product => new
{
product.Name,
// Calculate a display value without changing the source.
PriceWithTax = product.Price * 1.18m
});
Flatten Data with SelectMany
SelectMany() maps each source item to a sequence and flattens those sequences into one result.
string[][] teams =
[
["Aarav", "Mia"],
["Olivia", "Harry"]
];
// Produce one sequence containing every team member.
IEnumerable<string> members = teams.SelectMany(team => team);
Sort by Multiple Keys
Start with OrderBy() or OrderByDescending(). Add secondary keys with ThenBy() or ThenByDescending().
var ordered = products
.OrderBy(product => product.Price)
.ThenBy(product => product.Name); // Break equal-price ties.
Group Values
GroupBy() creates groups identified by a key. Each group is enumerable and exposes its key.
record Employee(string Name, string Department, decimal Salary);
Employee[] employees =
[
new("Aarav", "Engineering", 92000m),
new("Olivia", "Design", 81000m),
new("Harry", "Engineering", 88000m)
];
var totals = employees
.GroupBy(employee => employee.Department)
.Select(group => new
{
Department = group.Key,
Count = group.Count(),
TotalSalary = group.Sum(employee => employee.Salary)
});
Join Two Sequences
Join() performs an inner equijoin. It matches outer and inner keys, then uses a result selector to create each output value.
record Department(int Id, string Name);
record Staff(string Name, int DepartmentId);
Department[] departments = [new(1, "Sales"), new(2, "Support")];
Staff[] staff = [new("Mia", 2), new("Noah", 1)];
var directory = staff.Join(
departments,
person => person.DepartmentId, // Outer key.
department => department.Id, // Inner key.
(person, department) => new { person.Name, Department = department.Name }
);
Understand Deferred Execution
Many operators that return IEnumerable<T> do not read the source immediately. They run when you enumerate the query. If the source changes before enumeration, the results may change too.
List<int> numbers = [1, 2, 3, 4];
var evenNumbers = numbers.Where(number => number % 2 == 0);
numbers.Add(6);
// Enumeration happens here, so the result includes 6.
Console.WriteLine(string.Join(", ", evenNumbers));
Call ToList() or ToArray() when you need a snapshot or want to avoid executing an expensive query more than once.
Choose Safe Element Operators
First() requires at least one element. Single() requires exactly one. Their OrDefault variants return a default value when no element exists, but SingleOrDefault() still throws when more than one item matches.
Do not hide an unexpected duplicate by replacing Single() with First(). Choose the operator that expresses the data rule, then handle the possible empty result clearly.
LINQ Providers and Translation
IEnumerable<T> queries run as .NET code over objects. IQueryable<T> providers inspect expression trees and may translate supported operations to another language such as SQL. A method that works in LINQ to Objects may not be translatable by every provider.
Common LINQ Mistakes
- Enumerating the same expensive deferred query repeatedly.
- Calling ToList() too early and moving filtering from a provider to memory.
- Using Count() only to test for data instead of Any().
- Forgetting that OrderBy starts a new ordering and ThenBy extends it.
- Adding side effects inside predicates or projections.
- Assuming every .NET method can be translated by an IQueryable provider.
Conclusion
C# LINQ composes clear operations for filtering, projection, sorting, grouping, joining, and aggregation. Understand deferred execution, materialize results intentionally, select element operators that match your rules, and consider provider translation when the query runs outside an in-memory collection.