LINQ cheat sheet
A scannable LINQ reference: 17 short snippets across 10 topics, each linking back to the lesson it came from.
At a glance
| Topic | What it covers | |
|---|---|---|
| Query syntax and method syntax | LINQ is a set of extension methods on IEnumerable<T> and IQueryable<T>. Query syntax is the SQL-like form | lesson |
| Common LINQ operators | Every operator above has a Func-based overload plus an overload taking an IEqualityComparer<T> or | lesson |
| Deferred execution and querying a database | LINQ operators that return a sequence build a description of work, not a result. Execution happens when something | lesson |
| Lambda expressions and extension methods behind LINQ | Every query operator is an extension method taking a delegate, and knowing the difference between a delegate and an | lesson |
| Projection and shaping results | Select and SelectMany decide the shape of the data, and projecting in the query rather than after it is the cheapest | lesson |
| Debugging and testing LINQ queries | Break the pipeline, read the SQL, and write assertions that pin your behaviour without re-testing the framework | lesson |
| Async LINQ and streaming with IAsyncEnumerable | Await foreach, the operators EF Core provides, and why System.Linq.Async is a tool with a real cost that is not always | lesson |
| Query performance and avoiding N+1 | Multiple enumeration, N+1 queries, missing indexes and the measurement discipline that stops you from optimising the | lesson |
| Expression trees and IQueryable internals | An IQueryable is a description of a query, and the difference between a delegate and an expression tree is the whole | lesson |
| PLINQ and parallel queries | AsParallel splits work across cores, which only pays off for CPU-bound work over enough items with no shared state | lesson |
Quick snippets
Query syntax and method syntax
One query, two spellings
// query syntax
var query =
from o in orders
where o.Total > 100
orderby o.Total descending
select new { o.Id, o.Customer, o.Total };
// method syntax - exactly what the compiler emits for the above
var method = orders
.Where(o => o.Total > 100)
.OrderByDescending(o => o.Total)
.Select(o => new { o.Id, o.Customer, o.Total });
Joins, let and grouping
// inner join: customers with at least one order
var report =
from c in customers
join o in orders on c.Id equals o.CustomerId
let isLarge = o.Total > 500
orderby c.Name, o.Total descending
select new { c.Name, o.Id, o.Total, isLarge };
// group join: every customer, with their orders (possibly none)
var withOrders =
from c in customers
join o in orders on c.Id equals o.CustomerId into ordersForCustomer… 6 more lines in the full lesson.
Joins, let and grouping
// group by one key
var byCountry = orders
.GroupBy(o => o.Country)
.Select(g => new
{
Country = g.Key,
Orders = g.Count(),
Revenue = g.Sum(o => o.Total)
})
.OrderByDescending(x => x.Revenue)
.ToList();
… 9 more lines in the full lesson.
Full lesson: Query syntax and method syntax →
Common LINQ operators
Element and aggregation traps
// These throw on an empty or over-long sequence:
var a = nums.First(); // InvalidOperationException if empty
var b = nums.Single(); // throws unless exactly one element
var c = nums.Last(); // throws if empty
var d = nums.Max(); // throws on an empty sequence of values
var e = nums.ElementAt(10); // ArgumentOutOfRangeException
// Safe forms with an explicit fallback:
var a2 = nums.FirstOrDefault(-1);
var b2 = nums.SingleOrDefault();
var e2 = nums.ElementAtOrDefault(10);
… 10 more lines in the full lesson.
Ordering and comparers
// multi-key ordering - ThenBy extends the previous OrderBy
var sorted = people
.OrderBy(p => p.LastName)
.ThenBy(p => p.FirstName)
.ThenByDescending(p => p.Hired)
.ToList();
// case-insensitive de-duplication with an explicit comparer
var uniqueEmails = users
.Select(u => u.Email)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();… 12 more lines in the full lesson.
Full lesson: Common LINQ operators →
Deferred execution and querying a database
EF Core in practice
// N+1: one query for orders, then one per order
foreach (var order in await db.Orders.ToListAsync(ct))
{
var count = await db.Lines.CountAsync(l => l.OrderId == order.Id, ct);
}
// fixed: aggregate and group in a single query
var counts = await db.Orders
.Select(o => new { o.Id, Lines = o.Lines.Count })
.ToDictionaryAsync(x => x.Id, x => x.Lines, ct);
Nothing happens until you ask
var query = orders.Where(o => o.Total > 100); // no work done yet
orders.Add(new Order { Id = 99, Total = 500 });
var list = query.ToList(); // NOW it runs - and includes the order added above
// classic bug: the query closes over a loop variable
var results = new List<IEnumerable<Order>>();
foreach (var country in new[] { "FR", "DE" })
{
results.Add(orders.Where(o => o.Country == country));
}… 8 more lines in the full lesson.
Full lesson: Deferred execution and querying a database →
Lambda expressions and extension methods behind LINQ
Delegates and lambdas
// A lambda with an expression body compiles to a method plus a delegate
Func<int, bool> isEven = n => n % 2 == 0;
// A statement body is the same delegate with more lines
Func<int, bool> isBig = n =>
{
var doubled = n * 2;
return doubled > 100;
};
// Func<T, TResult> returns a value; Action<T> returns nothing
Action<string> log = message => Console.WriteLine(message);… 6 more lines in the full lesson.
Delegates and lambdas
// A closure captures the variable, not its value
var counter = 0;
Func<int> next = () => ++counter; // counter lives on a compiler-generated class
Console.WriteLine(next()); // 1
Console.WriteLine(next()); // 2
// The classic loop-capture bug
var actions = new List<Action>();
for (var i = 0; i < 3; i++)
{
actions.Add(() => Console.WriteLine(i)); // captures the variable i… 10 more lines in the full lesson.
Full lesson: Lambda expressions and extension methods behind LINQ →
Projection and shaping results
Select and SelectMany
// Select: one output element per input element
var names = people.Select(p => p.Name);
// Select with an index
var numbered = people.Select((p, i) => (Index: i + 1, p.Name));
// SelectMany: flatten one level, producing zero or more outputs per input
var allOrders = customers.SelectMany(c => c.Orders);
var pairs = new[] { "ab", "cd" }.SelectMany(s => s.ToCharArray());
// The query syntax form of SelectMany is a second from clause
var allOrders2 = from c in customers… 12 more lines in the full lesson.
Choosing the output shape
// Anonymous type: convenient inside one method, cannot cross a boundary
var anon = orders.Select(o => new { o.Id, o.Total });
// A record or a named DTO is what you want at a boundary
public sealed record OrderRow(int Id, decimal Total, string Customer);
var rows = orders
.Where(o => o.PlacedAt >= since)
.Select(o => new OrderRow(o.Id, o.Total, o.Customer.Name))
.ToList();
// Projection pushes the column list into the SQL: only these columns are read… 15 more lines in the full lesson.
Full lesson: Projection and shaping results →
Debugging and testing LINQ queries
Making a pipeline observable
// A tap you can leave in a pipeline: logs and passes the item through
public static IEnumerable<T> Tap<T>(this IEnumerable<T> source,
Action<T> action,
[CallerArgumentExpression("source")] string? label = null)
{
foreach (var item in source)
{
action(item);
yield return item;
}
}
… 14 more lines in the full lesson.
Making a pipeline observable
// Log the SQL for every query in development, including parameters
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString)
.LogTo(Console.WriteLine, LogLevel.Information)
.EnableSensitiveDataLogging() // development only
.EnableDetailedErrors());
// Or capture the SQL without running the query
var query = db.Orders.Where(o => o.Status == OrderStatus.Open).Select(o => o.Id);
var sql = query.ToQueryString();
Console.WriteLine(sql);
… 6 more lines in the full lesson.
Full lesson: Debugging and testing LINQ queries →
Async LINQ and streaming with IAsyncEnumerable
System.Linq.Async and its limits
// The System.Linq.Async package adds Where, Select and friends over IAsyncEnumerable
using System.Linq;
IAsyncEnumerable<int> doubled = source.SelectAwait(async x => x * 2);
// Inside a database query this is a mistake: the operator cannot be translated,
// so every row is fetched first and the work happens in memory.
var wrong = db.Orders
.ToAsyncEnumerable()
.Where(o => o.Total > 100) // client side!
.ToListAsync();
… 8 more lines in the full lesson.
Full lesson: Async LINQ and streaming with IAsyncEnumerable →
Query performance and avoiding N+1
Measure, then change one thing
// A deliberately blunt stopwatch harness. Good enough to spot an order of
// magnitude, which is what LINQ problems usually are.
public static async Task TimeAsync(string label, Func<Task> work, int runs = 5)
{
await work(); // warm up
var sw = System.Diagnostics.Stopwatch.StartNew();
for (var i = 0; i < runs; i++) await work();
sw.Stop();
Console.WriteLine(label + ": " + (sw.ElapsedMilliseconds / runs) + " ms per run");
}
await TimeAsync("projection", async () =>… 11 more lines in the full lesson.
Full lesson: Query performance and avoiding N+1 →
Expression trees and IQueryable internals
Delegate versus expression tree
using System.Linq.Expressions;
// A delegate is compiled code. A provider cannot inspect it.
Func<Order, bool> asDelegate = o => o.Total > 100;
// An expression tree is data describing the code. A provider can walk it.
Expression<Func<Order, bool>> asExpression = o => o.Total > 100;
Console.WriteLine(asExpression.Body.NodeType); // GreaterThan
Console.WriteLine(asExpression.Parameters[0].Name); // o
// IEnumerable uses Func; IQueryable uses Expression<Func>… 12 more lines in the full lesson.
Full lesson: Expression trees and IQueryable internals →
PLINQ and parallel queries
When parallelism helps
// PLINQ: the same operators, executed on the thread pool
var primes = Enumerable.Range(2, 5_000_000)
.AsParallel()
.WithDegreeOfParallelism(Environment.ProcessorCount)
.Where(IsPrime)
.ToArray();
// Preserving order costs synchronisation; drop it when order does not matter
var ordered = source.AsParallel().AsOrdered().Select(SlowPure).ToArray();
// Aggregation across partitions: pass a seed and a combine function
var total = big… 16 more lines in the full lesson.
Full lesson: PLINQ and parallel queries →
FAQ
Is this LINQ cheat sheet free to use?
Where do the examples come from?
How do I go deeper than a cheat sheet?
Related cheat sheets
.NET ASP.NET Core WinForms WPF
Last refreshed 2026-09-27.