LINQ Query Syntax and Advanced Patterns

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"
Note: The 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.
Tip: In a team, pick one style and use it consistently across the codebase. Most modern .NET codebases use method syntax because it is more concise, works with all LINQ operators (including Skip/Take/Distinct which have no query syntax equivalent), and integrates naturally with lambda-based callbacks. Use query syntax when it is genuinely clearer โ€” particularly for complex multi-join queries that read more naturally as SQL-like clauses.
Warning: Query syntax requires 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).

🧠 Test Yourself

You want page 3 of 10 items per page from a list of 100 posts sorted by date. What does Skip((3-1)*10).Take(10) return?