On this page
LINQ: Language Integrated Query
What is LINQ?
LINQ (Language Integrated Query) is a C# feature that lets you query and transform collections directly from the language, with the same syntax as calling object methods.
LINQ works on any type that implements IEnumerable<T>: lists, arrays, sets, and also databases (through EF Core) and XML.
// Without LINQ — imperative
var result = new List<string>();
foreach (var name in names)
{
if (name.StartsWith("A"))
result.Add(name.ToUpper());
}
result.Sort();
// With LINQ — declarative
var result = names
.Where(n => n.StartsWith("A"))
.Select(n => n.ToUpper())
.OrderBy(n => n)
.ToList();Method syntax
The most common form. Chains extension methods:
Where — filter
var products = GetProducts();
// Simple filter
var expensive = products.Where(p => p.Price > 500);
// Multiple conditions
var available = products.Where(p => p.Stock > 0 && p.Price < 1000);
// With index
var oddIndexed = products.Where((p, i) => i % 2 != 0);Select — project
// Select a single property
IEnumerable<string> names = products.Select(p => p.Name);
// Anonymous object projection
var summary = products.Select(p => new
{
p.Id,
p.Name,
FormattedPrice = $"${p.Price:F2}"
});
// SelectMany — flatten nested collections
var tags = courses.SelectMany(c => c.Tags);
// With index
var numbered = products.Select((p, i) => $"{i + 1}. {p.Name}");Ordering
// OrderBy — ascending
var byPrice = products.OrderBy(p => p.Price);
// OrderByDescending — descending
var mostExpensive = products.OrderByDescending(p => p.Price);
// ThenBy — secondary order
var compound = products
.OrderBy(p => p.Category)
.ThenByDescending(p => p.Price)
.ThenBy(p => p.Name);GroupBy — grouping
var groups = products
.GroupBy(p => p.Category)
.Select(g => new
{
Category = g.Key,
Count = g.Count(),
AvgPrice = g.Average(p => p.Price),
Cheapest = g.Min(p => p.Price),
MostExp = g.Max(p => p.Price),
});
// Group by multiple keys
var multiGroups = products
.GroupBy(p => new { p.Category, IsExpensive = p.Price > 100 })
.Select(g => new
{
g.Key.Category,
g.Key.IsExpensive,
Items = g.ToList()
});Join — combining collections
record Order(int Id, int ProductId, int Quantity);
var orders = new List<Order>
{
new(1, 1, 2),
new(2, 3, 1),
new(3, 2, 5),
};
// Inner join
var details = orders.Join(
products,
order => order.ProductId,
product => product.Id,
(order, product) => new
{
OrderId = order.Id,
Product = product.Name,
Subtotal = product.Price * order.Quantity
}
);
// Left join with GroupJoin + SelectMany
var allWithOrders = products.GroupJoin(
orders,
p => p.Id,
ord => ord.ProductId,
(p, ords) => new
{
p.Name,
TotalOrdered = ords.Sum(o => o.Quantity)
}
);Aggregate
// Generic accumulator — like reduce in JavaScript
var words = new List<string> { "Hello", "world", "with", "C#" };
string sentence = words.Aggregate((acc, s) => acc + " " + s);
// "Hello world with C#"
// With seed value
decimal discount = 0.1m;
decimal totalWithDiscount = products.Aggregate(
0m,
(acc, p) => acc + p.Price * (1 - discount)
);Quantifier and element methods
// Verification
bool hasProducts = products.Any();
bool hasLowPrice = products.Any(p => p.Price < 50);
bool allInStock = products.All(p => p.Stock > 0);
// Counting
int total = products.Count();
int expensiveCount = products.Count(p => p.Price > 500);
long totalLong = products.LongCount(); // for huge collections
// Element access
var first = products.First();
var firstExpensive = products.First(p => p.Price > 500);
var firstOrNull = products.FirstOrDefault(p => p.Price > 9999);
var last = products.Last();
// Single — throws if 0 or more than 1
var unique = products.Single(p => p.Id == 3);
var uniqueOrNull = products.SingleOrDefault(p => p.Id == 999);
// By index
var third = products.ElementAt(2);
// Skip and take
var page2 = products.Skip(10).Take(10); // pagination
var top3 = products.Take(3);
var skipTwo = products.Skip(2);
// TakeWhile / SkipWhile — conditional
var untilExpensive = products.TakeWhile(p => p.Price < 500);Query syntax
An alternative declarative syntax, similar to SQL:
var result =
from p in products
where p.Price > 100 && p.Stock > 0
orderby p.Category, p.Price descending
select new
{
p.Name,
p.Category,
PriceStr = $"${p.Price:F2}"
};
// GroupBy in query syntax
var groups =
from p in products
group p by p.Category into g
select new
{
Category = g.Key,
Items = g.ToList(),
Average = g.Average(p => p.Price)
};Deferred execution
LINQ uses lazy evaluation. The query does not execute until you iterate the result:
// This line only DEFINES the query, does not execute it
IEnumerable<Product> expensive = products.Where(p => p.Price > 500);
// The query executes HERE (when iterating)
foreach (var p in expensive)
Console.WriteLine(p.Name);
// Materialize to execute immediately and store the result
List<Product> expensiveList = expensive.ToList(); // executes and stores
Product[] expensiveArray = expensive.ToArray(); // executes and stores
// Count() also executes the query
int count = expensive.Count(); // executesPractice
- Top 5: Given a
List<Product>, use LINQ to get the 5 most expensive products with stock > 0, showing only name and formatted price. - Statistics by category: Group products by category and show the total items, average price, and total inventory value (price × stock).
- Join data: Create lists of
Order(Id, CustomerId, Total)andCustomer(Id, Name). Use LINQ Join to display orders with the customer name.
In the next lesson we will explore async/await: how to write asynchronous code in C# to handle I/O operations without blocking the main thread.
Deferred execution with ToList() and ToArray()
LINQ uses lazy evaluation: the query does not execute until you iterate the result. Call .ToList() or .ToArray() to materialize and immediately execute the query. This matters when the data source can change, or when you want to measure execution time accurately.
First vs FirstOrDefault vs Single
First() throws if the collection is empty. FirstOrDefault() returns null/default if empty. Single() throws if there are 0 or more than 1 element. SingleOrDefault() returns null if empty, throws if there is more than 1. Choose based on your expectations about the data.
LINQ and performance
LINQ is very readable but not always the fastest option. For very large collections (millions of elements), consider using for loops with arrays directly. AsParallel() can parallelize LINQ queries across multiple cores to improve performance.
// Sample data
record Product(int Id, string Name, decimal Price, string Category, int Stock);
var products = new List<Product>
{
new(1, "Laptop Pro", 1299.99m, "Electronics", 10),
new(2, "Optical Mouse", 29.99m, "Peripherals", 50),
new(3, "4K Monitor", 449.99m, "Electronics", 8),
new(4, "Mech Keyboard", 89.99m, "Peripherals", 30),
new(5, "Laptop Air", 999.99m, "Electronics", 15),
new(6, "BT Headphones", 79.99m, "Audio", 20),
};
// Where — filter
var electronics = products.Where(p => p.Category == "Electronics");
// Select — project/transform
var names = products.Select(p => p.Name).ToList();
var summary = products.Select(p => new { p.Name, p.Price });
// OrderBy / OrderByDescending / ThenBy
var ordered = products
.OrderBy(p => p.Category)
.ThenByDescending(p => p.Price);
// GroupBy — group
var byCategory = products
.GroupBy(p => p.Category)
.Select(g => new
{
Category = g.Key,
Count = g.Count(),
Average = g.Average(p => p.Price),
});
foreach (var group in byCategory)
Console.WriteLine($"{group.Category}: {group.Count} items, avg ${group.Average:F2}");
Sign in to track your progress