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

TopicWhat it covers
Query syntax and method syntaxLINQ is a set of extension methods on IEnumerable<T> and IQueryable<T>. Query syntax is the SQL-like formlesson
Common LINQ operatorsEvery operator above has a Func-based overload plus an overload taking an IEqualityComparer<T> orlesson
Deferred execution and querying a databaseLINQ operators that return a sequence build a description of work, not a result. Execution happens when somethinglesson
Lambda expressions and extension methods behind LINQEvery query operator is an extension method taking a delegate, and knowing the difference between a delegate and anlesson
Projection and shaping resultsSelect and SelectMany decide the shape of the data, and projecting in the query rather than after it is the cheapestlesson
Debugging and testing LINQ queriesBreak the pipeline, read the SQL, and write assertions that pin your behaviour without re-testing the frameworklesson
Async LINQ and streaming with IAsyncEnumerableAwait foreach, the operators EF Core provides, and why System.Linq.Async is a tool with a real cost that is not alwayslesson
Query performance and avoiding N+1Multiple enumeration, N+1 queries, missing indexes and the measurement discipline that stops you from optimising thelesson
Expression trees and IQueryable internalsAn IQueryable is a description of a query, and the difference between a delegate and an expression tree is the wholelesson
PLINQ and parallel queriesAsParallel splits work across cores, which only pays off for CPU-bound work over enough items with no shared statelesson

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?
Yes. No sign-up and no tracking: the page is static, every example is on the page itself, and you can print it or save it as a one-page reference.
Where do the examples come from?
Every snippet is taken from the 10 lessons of the LINQ course on this site, and each section links back to the lesson it was pulled from.
How do I go deeper than a cheat sheet?
Open the full LINQ course — it carries the worked explanations, the edge cases and the exercises behind every line here.

.NET ASP.NET Core WinForms WPF

Last refreshed 2026-09-27.