LINQ has two equivalent syntaxes: method syntax (chained method calls: source.Where(x => ...).Select(x => ...)) and query syntax (SQL-like clauses: from x in source where ... select ...). Both compile to identical IL โ the choice is purely stylistic. Query syntax shines for multi-join and let scenarios; method syntax is more concise for simple chains and is needed for operators without a query syntax equivalent (Skip, Take, Distinct, GroupBy with aggregation). Advanced patterns like pagination, chunking, and custom operators complete the LINQ toolkit.
Query Syntax
var posts = GetPosts();
// โโ Method syntax โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
var methodResult = posts
.Where(p => p.IsPublished && p.ViewCount > 500)
.OrderByDescending(p => p.ViewCount)
.Select(p => new { p.Title, p.ViewCount });
// โโ Equivalent query syntax โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
var queryResult =
from p in posts
where p.IsPublished && p.ViewCount > 500
orderby p.ViewCount descending
select new { p.Title, p.ViewCount };
// Both produce identical results โ choose based on readability
// โโ Let clause โ intermediate variable โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
var withSlug =
from p in posts
let slug = p.Title.ToLowerInvariant().Replace(" ", "-")
where p.IsPublished
select new { p.Title, Slug = slug, p.ViewCount };
// Equivalent in method syntax:
var withSlugMethod = posts
.Where(p => p.IsPublished)
.Select(p => new
{
p.Title,
Slug = p.Title.ToLowerInvariant().Replace(" ", "-"),
p.ViewCount,
});
// โโ Multi-from (cross join / flat join) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
var tags = new[] { "dotnet", "csharp" };
var authors = new[] { "Alice", "Bob" };
var pairs =
from tag in tags
from author in authors
select $"{author} writes about {tag}";
// "Alice writes about dotnet", "Alice writes about csharp",
// "Bob writes about dotnet", "Bob writes about csharp"
let clause in query syntax introduces a named variable that is computed once and reused in subsequent clauses. This is useful when a computed value is used in both the where filter and the select projection โ without let, you would compute it twice. In method syntax, the equivalent is a nested Select that adds the computed value to an anonymous type, followed by further operations on that enriched type.select or group ... by as the final clause โ you cannot end a query syntax block with where or orderby. If you need to apply Skip/Take (pagination) after a query syntax expression, either switch to method syntax or wrap the query: (from p in posts where p.IsPublished select p).Skip(skip).Take(take).ToList(). Mixing both syntaxes in one expression is valid.Pagination with Skip and Take
// Pagination โ the most common real-world LINQ pattern
int page = 2;
int pageSize = 10;
var pagedPosts = posts
.Where(p => p.IsPublished)
.OrderByDescending(p => p.CreatedAt)
.Skip((page - 1) * pageSize) // skip the first (page-1) pages
.Take(pageSize) // take exactly pageSize items
.ToList();
// Always order before paginating โ without OrderBy, page results are non-deterministic
// In EF Core, this translates to: SELECT ... ORDER BY ... OFFSET ... ROWS FETCH NEXT ... ROWS ONLY
Advanced .NET 6+ LINQ Operators
// Chunk โ split a sequence into fixed-size batches
var allPosts = GetAllPosts();
foreach (var batch in allPosts.Chunk(50))
{
await ProcessBatchAsync(batch); // batch is Post[] of up to 50 items
}
// DistinctBy, MinBy, MaxBy โ operate on a key selector
var uniqueAuthors = posts.DistinctBy(p => p.AuthorId);
Post latestPost = posts.MaxBy(p => p.CreatedAt)!;
Post shortestTitle = posts.MinBy(p => p.Title.Length)!;
// ExceptBy, IntersectBy, UnionBy โ set operations on a key
var newPosts = allPosts.ExceptBy(cachedIds, p => p.Id);
// Index โ enumerate with index (like Python's enumerate)
foreach ((int i, Post p) in posts.Index())
Console.WriteLine($"[{i}] {p.Title}");
Custom LINQ Extension Methods
// Write your own composable LINQ operators
public static class LinqExtensions
{
// Paginate any sequence
public static IEnumerable<T> Paginate<T>(
this IEnumerable<T> source, int page, int pageSize)
=> source.Skip((page - 1) * pageSize).Take(pageSize);
// Filter out null values and change the type to non-nullable
public static IEnumerable<T> WhereNotNull<T>(
this IEnumerable<T?> source) where T : class
=> source.Where(x => x is not null)!;
// Apply a transformation only if a condition is true
public static IEnumerable<T> If<T>(
this IEnumerable<T> source,
bool condition,
Func<IEnumerable<T>, IEnumerable<T>> transform)
=> condition ? transform(source) : source;
}
// Usage
var results = posts
.Where(p => p.IsPublished)
.If(filterByTag != null, q => q.Where(p => p.Tags.Contains(filterByTag!)))
.OrderByDescending(p => p.CreatedAt)
.Paginate(page, pageSize)
.ToList();
Common Mistakes
Mistake 1 โ Paginating without ordering (non-deterministic results)
โ Wrong โ page 2 may return different items each call:
posts.Skip(10).Take(10).ToList(); // no OrderBy โ order undefined
โ Correct โ always establish a stable order first.
Mistake 2 โ Using query syntax for operators that have no query syntax
โ Wrong โ Skip/Take have no query syntax equivalent, compile error if attempted.
โ
Correct โ wrap the query syntax expression in parentheses and chain method syntax: (from p in posts ... select p).Skip(n).Take(m).