DEV SCRIPTS

LINQ FAQs

LINQ FAQ

LINQ FAQ

Practical questions & answers with real C# code examples

100 Questions
LINQ Fundamentals

LINQ queries using IEnumerable<T> are not executed when you define them — they run when you iterate (foreach, ToList, First, Count, etc.). This means the query runs against the current state of the source, and re-iterating re-executes it. To fix the result in time, materialize with ToList() or ToArray().

csharp
var numbers = new List<int> { 1, 2, 3, 4, 5 };

// Query defined — NOT executed yet
var query = numbers.Where(n => n > 2);

numbers.Add(10); // modify source AFTER defining query

// Query executes HERE — includes 10 because it was added
foreach (var n in query)
    Console.WriteLine(n); // 3, 4, 5, 10

// Materialize immediately to freeze the result
var snapshot = numbers.Where(n => n > 2).ToList();
numbers.Add(99); // too late — snapshot won't include 99
Console.WriteLine(snapshot.Count); // 4, not 5

Both are compiled to the same IL. Query syntax reads like SQL and shines for joins and grouping. Method syntax (fluent) is more compact and covers operators unavailable in query syntax (Count, Skip, Take, Distinct). Teams often mix both.

csharp
var products = GetProducts();

// Query syntax — readable for complex projections
var expensiveQuery =
    from p in products
    where p.Price > 100 && p.IsActive
    orderby p.Price descending
    select new { p.Name, p.Price };

// Method syntax — same result, more composable
var expensiveMethod = products
    .Where(p => p.Price > 100 && p.IsActive)
    .OrderByDescending(p => p.Price)
    .Select(p => new { p.Name, p.Price });

// Query syntax for join (SQL-like, readable)
var joined =
    from o in orders
    join c in customers on o.CustomerId equals c.Id
    select new { o.OrderDate, c.Name };

// Method syntax for operations not in query syntax
var result = products
    .Where(p => p.IsActive)
    .Skip(20)
    .Take(10)
    .ToList();

IEnumerable<T> executes in memory (LINQ to Objects). IQueryable<T> builds an expression tree that a provider (Entity Framework, etc.) translates to SQL. Calling IEnumerable methods on a DbSet pulls all rows into memory first — a critical performance trap.

csharp
// ❌ Bad: AsEnumerable pulls ALL rows into memory, then filters in C#
var result = dbContext.Products
    .AsEnumerable()            // fetches entire table
    .Where(p => p.Price > 100) // filters in memory
    .ToList();

// ✅ Good: IQueryable translates Where to SQL WHERE clause
var result = dbContext.Products
    .Where(p => p.Price > 100) // runs as SQL: WHERE Price > 100
    .ToList();

// Check at runtime which you have:
IQueryable<Product> q = dbContext.Products.Where(p => p.IsActive);
Console.WriteLine(q.Expression); // shows expression tree

// AsEnumerable is useful when you need in-memory operations
// that EF can't translate (custom C# methods):
var result = dbContext.Products
    .Where(p => p.IsActive)    // SQL filter
    .AsEnumerable()             // switch to in-memory from here
    .Where(p => MyCustomCheck(p)); // C# method EF can't translate

LINQ operators are static extension methods in System.Linq.Enumerable. They accept and return IEnumerable<T>, enabling chaining. Each operator returns a new lazy iterator — the source is never mutated. You can write your own to extend the pipeline.

csharp
// What the compiler sees under the hood:
// numbers.Where(n => n > 2).Select(n => n * 10)
// becomes:
// Enumerable.Select(Enumerable.Where(numbers, n => n > 2), n => n * 10)

// Writing your own LINQ extension:
public static class LinqExtensions
{
    // Yield-based: lazy, efficient
    public static IEnumerable<T> WhereNot<T>(
        this IEnumerable<T> source,
        Func<T, bool> predicate)
    {
        foreach (var item in source)
            if (!predicate(item))
                yield return item;
    }

    // Batching operator
    public static IEnumerable<IEnumerable<T>> Batch<T>(
        this IEnumerable<T> source, int size)
    {
        var batch = new List<T>(size);
        foreach (var item in source)
        {
            batch.Add(item);
            if (batch.Count == size)
            {
                yield return batch;
                batch = new List<T>(size);
            }
        }
        if (batch.Count > 0) yield return batch;
    }
}

// Usage:
var evens = numbers.WhereNot(n => n % 2 != 0);
var pages = items.Batch(10);

Terminating operators trigger immediate execution: ToList(), ToArray(), ToDictionary(), ToHashSet(), First(), Count(), Sum(), Any(), and all aggregates. Choose based on how you’ll use the result.

csharp
var products = dbContext.Products.Where(p => p.IsActive);

// Materialize to List (mutable, index access)
List<Product> list = products.ToList();

// Materialize to array (fixed size, slightly faster iteration)
Product[] arr = products.ToArray();

// Materialize to Dictionary for O(1) lookup by key
Dictionary<int, Product> byId = products.ToDictionary(p => p.Id);

// Materialize to HashSet for O(1) membership checks
HashSet<int> activeIds = products.Select(p => p.Id).ToHashSet();

// Aggregates execute immediately and return a scalar
int count  = products.Count();
decimal sum = products.Sum(p => p.Price);
bool any   = products.Any(p => p.Price > 1000);
Product? first = products.FirstOrDefault(p => p.Featured);

let introduces a new range variable computed from the current element, eliminating redundant calculations. In method syntax, the equivalent is a Select that projects to an anonymous type carrying both the original item and the computed value.

csharp
// Query syntax with let — fullName computed once, reused twice
var result =
    from u in users
    let fullName = $"{u.FirstName} {u.LastName}"
    where fullName.Length > 10
    orderby fullName
    select new { fullName, u.Email };

// Method syntax equivalent using anonymous type projection
var result = users
    .Select(u => new { u, fullName = $"{u.FirstName} {u.LastName}" })
    .Where(x => x.fullName.Length > 10)
    .OrderBy(x => x.fullName)
    .Select(x => new { x.fullName, x.u.Email });

// Useful for expensive calculations
var bestPriced =
    from p in products
    let discounted = p.Price * (1 - p.DiscountRate)
    where discounted < 50
    orderby discounted
    select new { p.Name, OriginalPrice = p.Price, FinalPrice = discounted };

Each LINQ method returns a new IEnumerable<T> that wraps the previous one, forming a chain of lazy iterators. When the terminal operator iterates, each element passes through the entire chain — no intermediate collections are created (unless you explicitly materialize).

csharp
// This pipeline processes ONE element at a time through all steps
// No intermediate List is allocated
var result = Enumerable.Range(1, 1_000_000)
    .Where(n => n % 2 == 0)          // step 1: filter
    .Select(n => n * n)               // step 2: project
    .Where(n => n > 1000)             // step 3: filter again
    .Take(5)                          // step 4: stop early!
    .ToList();                        // executes the pipeline

// Because of Take(5), the pipeline stops after finding 5 matches
// It does NOT process all 1,000,000 numbers — only as many as needed

// Conditional pipeline building (add operators based on runtime state)
IEnumerable<Product> query = products.AsEnumerable();
if (filter.MinPrice.HasValue)
    query = query.Where(p => p.Price >= filter.MinPrice.Value);
if (filter.CategoryId.HasValue)
    query = query.Where(p => p.CategoryId == filter.CategoryId.Value);
if (!string.IsNullOrEmpty(filter.Search))
    query = query.Where(p => p.Name.Contains(filter.Search));

var paged = query.Skip(filter.Page * 20).Take(20).ToList();

Anonymous types let you project to a lightweight ad-hoc shape without defining a class. The compiler generates a class with readonly properties. var is required because you can't name the type. In .NET 6+, prefer records or tuples for types that need to cross method boundaries.

csharp
// Anonymous type projection
var summary = products
    .Where(p => p.IsActive)
    .Select(p => new
    {
        p.Name,                          // property name inferred
        p.Price,
        Category = p.Category.Name,      // renamed
        Discounted = p.Price * 0.9m      // computed
    })
    .OrderBy(x => x.Price)
    .ToList();

// Each item: { Name, Price, Category, Discounted }
foreach (var item in summary)
    Console.WriteLine($"{item.Name}: {item.Discounted:C}");

// For returning from methods — use a named type or tuple
(string Name, decimal Price)[] prices = products
    .Select(p => (p.Name, p.Price))
    .ToArray();

// Or a record (C# 9+)
record ProductDto(string Name, decimal Price, string Category);
var dtos = products.Select(p => new ProductDto(p.Name, p.Price, p.Category.Name));
Filtering & Projection

Where yields only elements for which the predicate returns true. Multiple Where calls AND their conditions. You can also build a predicate dynamically and combine with && or use PredicateBuilder for OR logic.

csharp
// Basic filter
var adults = users.Where(u => u.Age >= 18);

// Multiple conditions — all must be true (AND)
var eligible = users.Where(u => u.Age >= 18 && u.IsVerified && !u.IsBanned);

// Chained Where calls — equivalent to above (both ANDed)
var eligible = users
    .Where(u => u.Age >= 18)
    .Where(u => u.IsVerified)
    .Where(u => !u.IsBanned);

// Where with index overload
var oddPositionItems = items.Where((item, index) => index % 2 != 0);

// Dynamic predicate composition
Func<Product, bool> filter = p => p.IsActive;
if (minPrice.HasValue)
    filter = p => filter(p) && p.Price >= minPrice.Value;

var filtered = products.Where(filter);

Select transforms every element 1-to-1. It's LINQ's map. Use it to extract a single property, build DTOs, or compute derived values. The (element, index) overload gives access to the element's position.

csharp
// Extract a single property
IEnumerable<string> names = users.Select(u => u.Name);

// Project to DTO
IEnumerable<UserDto> dtos = users.Select(u => new UserDto
{
    Id          = u.Id,
    DisplayName = $"{u.FirstName} {u.LastName}",
    Email       = u.Email.ToLower(),
    IsAdmin     = u.Roles.Contains("admin"),
});

// Select with index — add 1-based rank
var ranked = leaderboard
    .OrderByDescending(p => p.Score)
    .Select((player, index) => new
    {
        Rank   = index + 1,
        player.Name,
        player.Score,
    });

// Transform value type
var doubled = numbers.Select(n => n * 2);
var lengths  = words.Select(w => w.Length);

SelectMany is LINQ's flatMap. It projects each element to a sub-collection and flattens all sub-collections into one sequence. Essential for one-to-many relationships without nested loops.

csharp
// Each Order has a List<OrderItem> — flatten all items
var allItems = orders.SelectMany(o => o.Items);

// Flatten and keep parent reference
var itemsWithOrder = orders.SelectMany(
    o => o.Items,                           // collection selector
    (order, item) => new { order.Id, item } // result selector
);

// Flatten tags from multiple posts
var allTags = posts
    .SelectMany(p => p.Tags)
    .Distinct()
    .OrderBy(t => t);

// Equivalent using query syntax (multiple from clauses)
var allTags =
    (from p in posts
     from tag in p.Tags
     select tag)
    .Distinct()
    .OrderBy(t => t);

// Flatten string characters
string[] words = { "hello", "world" };
IEnumerable<char> chars = words.SelectMany(w => w);
// → h, e, l, l, o, w, o, r, l, d

First returns the first element (throws if empty). FirstOrDefault returns the default (null/0) if empty. Single asserts exactly one match (throws for 0 or 2+). SingleOrDefault returns default for 0, throws for 2+. Choose based on your data contract.

csharp
// First: "there is at least one, give me the first"
var latest = orders.OrderByDescending(o => o.Date).First();

// FirstOrDefault: "there might be none, that's OK"
var featured = products.FirstOrDefault(p => p.IsFeatured);
if (featured is null) ShowEmptyState();

// C# 6+ default literal for value types
int firstEven = numbers.FirstOrDefault(n => n % 2 == 0, -1); // -1 if none

// Single: "there must be exactly ONE" — great for lookups by unique key
var user = users.Single(u => u.Id == userId); // throws if 0 or 2+

// SingleOrDefault: "zero or one, never more"
var config = settings.SingleOrDefault(s => s.Key == "MaxRetries");

// Rule of thumb:
// - List could be empty, want first → FirstOrDefault
// - List must have items, want first → First
// - Expecting exactly one by unique key → Single / SingleOrDefault
// - Never use Single on large DB queries — use Find() or FirstOrDefault

Any short-circuits and returns true as soon as one element matches. All short-circuits and returns false as soon as one element fails. Both are O(n) worst case but often much faster. Prefer Any() over Count() > 0 for performance.

csharp
// Any() — short-circuits on first match
bool hasAdmins = users.Any(u => u.IsAdmin);
bool isEmpty   = !users.Any(); // preferred over Count() == 0

// ❌ Slow: iterates entire collection and counts
bool hasAdmins = users.Count(u => u.IsAdmin) > 0;
// ✅ Fast: stops at first match
bool hasAdmins = users.Any(u => u.IsAdmin);

// All() — short-circuits on first failure
bool allVerified  = users.All(u => u.IsVerified);
bool allPositive  = numbers.All(n => n > 0);
bool noneNegative = !numbers.Any(n => n < 0); // equivalent

// Nested Any for "at least one order with a premium item"
bool hasPremiumOrder = customers.Any(c =>
    c.Orders.Any(o =>
        o.Items.Any(i => i.Category == "Premium")));

// Guard pattern
if (!orders.Any())
{
    Console.WriteLine("No orders to process");
    return;
}

OfType<T> filters and casts in one step — it skips elements that are not of type T (safe, no exception). Unlike Cast<T> which throws if an element is the wrong type. Use OfType on heterogeneous collections or IEnumerable (non-generic).

csharp
// Mixed collection — filter to specific type
object[] mixed = { 1, "hello", 2.5, "world", 42, true };

IEnumerable<string> strings = mixed.OfType<string>(); // "hello", "world"
IEnumerable<int>    ints    = mixed.OfType<int>();    // 1, 42

// UI control tree — get only TextBoxes
var textBoxes = panel.Controls
    .OfType<TextBox>()
    .Where(tb => tb.Text.Length > 0);

// Non-generic IEnumerable from legacy API
System.Collections.IEnumerable legacyList = GetLegacyItems();
var typed = legacyList.OfType<Product>().Where(p => p.IsActive);

// OfType vs Cast — both filter, Cast throws on wrong type
// ❌ Cast throws InvalidCastException if any element isn't a string
mixed.Cast<string>().ToList(); // throws on int 1

// ✅ OfType silently skips non-strings
mixed.OfType<string>().ToList(); // ["hello", "world"]

Contains checks if a specific element exists in a sequence. For value membership checks in filtering, prefer .Contains on a HashSet (O(1)) rather than on an IEnumerable (O(n)). Pass a custom IEqualityComparer<T> for structural comparison.

csharp
// Simple value membership
bool hasThree = numbers.Contains(3);
bool hasAdmin = roles.Contains("admin", StringComparer.OrdinalIgnoreCase);

// ❌ Slow: O(n) check inside Where — O(n²) total
var allowedIds = new List<int> { 1, 3, 5, 7 };
var result = products.Where(p => allowedIds.Contains(p.Id));

// ✅ Fast: O(1) HashSet lookup — O(n) total
var allowed = new HashSet<int> { 1, 3, 5, 7 };
var result = products.Where(p => allowed.Contains(p.Id));

// Custom equality comparer
class ProductComparer : IEqualityComparer<Product>
{
    public bool Equals(Product? x, Product? y) => x?.Sku == y?.Sku;
    public int GetHashCode(Product obj) => obj.Sku.GetHashCode();
}

bool isDuplicate = catalog.Contains(newProduct, new ProductComparer());

// In EF Core, Contains on a list translates to SQL IN (...)
var ids = new[] { 1, 2, 3 };
var products = dbContext.Products
    .Where(p => ids.Contains(p.Id)) // SQL: WHERE Id IN (1, 2, 3)
    .ToList();

Both Where and Select have overloads that receive (element, index). This lets you make decisions based on position — keep every other element, add rank numbers, or interleave with a second collection.

csharp
var items = new[] { "a", "b", "c", "d", "e", "f" };

// Keep only even-indexed elements (0, 2, 4 ...)
var evens = items.Where((_, index) => index % 2 == 0);
// → "a", "c", "e"

// Add 1-based row numbers
var numbered = items.Select((item, index) => $"{index + 1}. {item}");
// → "1. a", "2. b", ...

// Highlight every third item
var highlighted = items.Select((item, i) => new
{
    Text      = item,
    Highlight = i % 3 == 0,
});

// Pair items from a list with an external index sequence
var leaderboard = topPlayers
    .OrderByDescending(p => p.Score)
    .Select((p, rank) => new
    {
        Position = rank + 1,
        p.Name,
        p.Score,
        Medal = rank switch { 0 => "🥇", 1 => "🥈", 2 => "🥉", _ => "" }
    });
Ordering & Grouping

OrderBy returns an IOrderedEnumerable<T>. ThenBy/ThenByDescending add secondary (and further) sort keys. Never chain multiple OrderBy calls — each resets the sort. Use ThenBy for composite sorting.

csharp
// Single-level sort
var byPrice = products.OrderBy(p => p.Price);
var byPriceDesc = products.OrderByDescending(p => p.Price);

// Multi-level: primary then secondary
var sorted = employees
    .OrderBy(e => e.Department)       // primary: department A-Z
    .ThenBy(e => e.LastName)          // secondary: surname A-Z
    .ThenByDescending(e => e.Salary); // tertiary: salary high to low

// ❌ Wrong: second OrderBy resets the first
var wrong = employees
    .OrderBy(e => e.Department)
    .OrderBy(e => e.LastName); // overrides department sort!

// Sort by computed expression
var byFullName = users
    .OrderBy(u => u.LastName)
    .ThenBy(u => u.FirstName);

// Sort with custom comparer (natural sort for strings like "file10" > "file9")
var files = fileNames.OrderBy(f => f, new NaturalSortComparer());

// Case-insensitive string sort
var names = words.OrderBy(w => w, StringComparer.OrdinalIgnoreCase);

GroupBy returns IEnumerable<IGrouping<TKey, TElement>>. Each IGrouping has a Key property and is itself an IEnumerable<TElement> of the matching elements. Iterate the outer sequence for groups, inner sequence for group members.

csharp
// Group products by category
var byCategory = products.GroupBy(p => p.Category);

foreach (var group in byCategory)
{
    Console.WriteLine($"Category: {group.Key} ({group.Count()} items)");
    foreach (var product in group)
        Console.WriteLine($"  - {product.Name}: {product.Price:C}");
}

// Project each group to a summary object
var categorySummary = products
    .GroupBy(p => p.Category)
    .Select(g => new
    {
        Category = g.Key,
        Count    = g.Count(),
        Total    = g.Sum(p => p.Price),
        Average  = g.Average(p => p.Price),
        Cheapest = g.Min(p => p.Price),
    });

// GroupBy with element selector — project what goes into the group
var namesByDept = employees
    .GroupBy(
        e => e.Department,          // key selector
        e => e.Name                 // element selector — only keep Name
    );

GroupBy is lazy — it re-groups on each iteration. ToLookup is eager — it builds a dictionary-like structure immediately. Use ToLookup when you need to query the groups multiple times, look up by key, or share the grouped result across methods.

csharp
// ToLookup — materialized, O(1) key access
ILookup<string, Product> byCategory = products.ToLookup(p => p.Category);

// O(1) lookup — no re-enumeration
var electronics = byCategory["Electronics"]; // IEnumerable<Product>
var clothing    = byCategory["Clothing"];

// Missing key returns empty sequence (unlike Dictionary which throws)
var missing = byCategory["NonExistent"]; // empty, no exception

// GroupBy — lazy, re-groups on each iteration:
var grouped = products.GroupBy(p => p.Category); // lazy
var grouped2 = products.GroupBy(p => p.Category); // same query, runs again

// ToLookup in practice: batch-loading related data
var ordersByCustomer = orders.ToLookup(o => o.CustomerId);
foreach (var customer in customers)
{
    var customerOrders = ordersByCustomer[customer.Id]; // O(1)
    ProcessOrders(customer, customerOrders);
}

Project the key to an anonymous type or tuple in the key selector. The composite key uses structural equality for grouping — elements with the same combination of values end up in the same group.

csharp
// Group by year + month (composite key as anonymous type)
var monthlySales = orders
    .GroupBy(o => new { o.Date.Year, o.Date.Month })
    .Select(g => new
    {
        g.Key.Year,
        g.Key.Month,
        Total  = g.Sum(o => o.Total),
        Count  = g.Count(),
    })
    .OrderBy(x => x.Year)
    .ThenBy(x => x.Month);

// Group by department + job level (tuple key)
var byDeptLevel = employees
    .GroupBy(e => (e.Department, e.Level))
    .Select(g => new
    {
        Department = g.Key.Department,
        Level      = g.Key.Level,
        HeadCount  = g.Count(),
        AvgSalary  = g.Average(e => e.Salary),
    });

// Query syntax with composite grouping
var result =
    from s in sales
    group s by new { s.Region, s.ProductCategory } into g
    select new { g.Key.Region, g.Key.ProductCategory, Total = g.Sum(s => s.Amount) };

Nullable value types (int?, DateTime?) sort naturally with nulls first in ascending order. For nullable reference types, project to a sortable fallback value using ?? or the null-conditional operator in the key selector.

csharp
// Nullable int? — nulls sort first (ascending)
var sorted = products.OrderBy(p => p.DiscountPrice); // nulls first

// Nulls last (put null at end by replacing with MaxValue)
var nullsLast = products
    .OrderBy(p => p.DiscountPrice ?? decimal.MaxValue);

// Nullable string — sort nulls to end using empty string fallback
var byName = users.OrderBy(u => u.Nickname ?? string.Empty);

// Sort by optional nested property
var byCity = employees
    .OrderBy(e => e.Address?.City ?? string.Empty)
    .ThenBy(e => e.LastName);

// Complex: active first, then by date (nulls last)
var prioritized = tasks
    .OrderByDescending(t => t.IsActive)
    .ThenBy(t => t.DueDate ?? DateTime.MaxValue);

After grouping and transforming each group, use SelectMany to flatten the groups back into a single sequence. This is the group-transform-ungroup pattern for operations like "top N per group".

csharp
// Top 3 products per category
var top3PerCategory = products
    .GroupBy(p => p.Category)
    .SelectMany(g => g
        .OrderByDescending(p => p.Sales)
        .Take(3));

// Most recent order per customer
var latestOrders = orders
    .GroupBy(o => o.CustomerId)
    .SelectMany(g => g
        .OrderByDescending(o => o.Date)
        .Take(1));

// Normalize outliers: cap values above the 95th percentile per group
var normalized = measurements
    .GroupBy(m => m.SensorId)
    .SelectMany(g =>
    {
        var sorted = g.OrderBy(m => m.Value).ToList();
        var cap = sorted[(int)(sorted.Count * 0.95)].Value;
        return g.Select(m => m with { Value = Math.Min(m.Value, cap) });
    });

Pass a custom IComparer<T> to OrderBy for logic that can't be expressed as a simple key selector — natural sort order, priority-based ordering, or locale-aware string comparison.

csharp
// Priority-based status ordering: Active first, then Pending, then Closed
class StatusComparer : IComparer<string>
{
    private static readonly Dictionary<string, int> _order = new()
    {
        ["Active"]  = 0,
        ["Pending"] = 1,
        ["Closed"]  = 2,
    };

    public int Compare(string? x, string? y) =>
        _order.GetValueOrDefault(x ?? "", 99)
            .CompareTo(_order.GetValueOrDefault(y ?? "", 99));
}

var tickets = issues.OrderBy(i => i.Status, new StatusComparer());

// Semantic version sorting (1.10.0 > 1.9.0)
class SemVerComparer : IComparer<string>
{
    public int Compare(string? x, string? y)
    {
        var vx = Version.Parse(x ?? "0.0.0");
        var vy = Version.Parse(y ?? "0.0.0");
        return vx.CompareTo(vy);
    }
}

var releases = versions.OrderByDescending(v => v, new SemVerComparer());

Use a switch/Dictionary to map sort column names to key selectors, or reflection for a fully dynamic approach. For EF Core, use System.Linq.Dynamic.Core to pass sort expressions as strings.

csharp
// Switch-based dynamic sort (type-safe)
IOrderedEnumerable<Product> ApplySort(
    IEnumerable<Product> source, string sortBy, bool desc)
{
    return (sortBy, desc) switch
    {
        ("name",  false) => source.OrderBy(p => p.Name),
        ("name",  true)  => source.OrderByDescending(p => p.Name),
        ("price", false) => source.OrderBy(p => p.Price),
        ("price", true)  => source.OrderByDescending(p => p.Price),
        ("date",  false) => source.OrderBy(p => p.CreatedAt),
        ("date",  true)  => source.OrderByDescending(p => p.CreatedAt),
        _                => source.OrderBy(p => p.Name),
    };
}

// EF Core with System.Linq.Dynamic.Core NuGet package
using System.Linq.Dynamic.Core;

string sortExpression = "Price descending, Name"; // from user input
var result = dbContext.Products
    .Where(p => p.IsActive)
    .OrderBy(sortExpression) // dynamic sort string
    .ToList();
Aggregation & Quantifiers

All aggregate functions accept an optional selector lambda that projects each element before computing. This avoids a separate Select call and makes intent clearer. They all throw InvalidOperationException on empty sequences (except Count and Sum which return 0).

csharp
var orders = GetOrders();

int    count   = orders.Count();                        // total orders
int    shipped = orders.Count(o => o.Status == "Shipped"); // with predicate
long   bigCount= orders.LongCount();                    // for very large sets

decimal total  = orders.Sum(o => o.Total);              // sum of all totals
decimal min    = orders.Min(o => o.Total);              // lowest order
decimal max    = orders.Max(o => o.Total);              // highest order
double  avg    = orders.Average(o => (double)o.Total);  // mean

// Safe aggregation on potentially empty sequences
decimal safeMin = orders.Any() ? orders.Min(o => o.Total) : 0;

// .NET 6+: MinBy/MaxBy returns the element, not just the value
Order? cheapest  = orders.MinBy(o => o.Total);
Order? mostItems = orders.MaxBy(o => o.Items.Count);

// Aggregate multiple values in one pass
var stats = orders.Aggregate(
    (Count: 0, Total: 0m, Min: decimal.MaxValue, Max: decimal.MinValue),
    (acc, o) => (
        acc.Count + 1,
        acc.Total + o.Total,
        Math.Min(acc.Min, o.Total),
        Math.Max(acc.Max, o.Total)
    ));

Aggregate is a general-purpose fold. Without a seed, it uses the first element as the initial accumulator (throws on empty). With a seed, you can accumulate into a completely different type and handle empty sequences safely.

csharp
// No seed — first element becomes the initial accumulator
// Computes product of all numbers: 1 * 2 * 3 * 4 * 5 = 120
int product = new[] { 1, 2, 3, 4, 5 }.Aggregate((acc, n) => acc * n);

// With seed — safe on empty, accumulate into different type
string csv = names.Aggregate(
    seed: string.Empty,
    func: (acc, name) => acc.Length == 0 ? name : $"{acc},{name}");
// Equivalent to: string.Join(",", names) — but shows the pattern

// Build a frequency map
var frequency = words.Aggregate(
    seed: new Dictionary<string, int>(),
    func: (dict, word) =>
    {
        dict[word] = dict.GetValueOrDefault(word) + 1;
        return dict;
    });

// With result selector — transform the final accumulator
string summary = orders.Aggregate(
    seed: (Count: 0, Total: 0m),
    func: (acc, o) => (acc.Count + 1, acc.Total + o.Total),
    resultSelector: acc => $"{acc.Count} orders totaling {acc.Total:C}");

Before .NET 6, getting the element with the minimum property required a two-step sort (OrderBy().First()) or custom code. MinBy/MaxBy do this in one linear pass without sorting, returning the element (or null for empty sequences).

csharp
// ❌ Old approach: O(n log n) sort just to get one element
Product? cheapest = products.OrderBy(p => p.Price).FirstOrDefault();

// ✅ .NET 6+: O(n) linear scan, returns the element
Product? cheapest = products.MinBy(p => p.Price);   // null if empty
Product? mostPop  = products.MaxBy(p => p.Reviews); // null if empty

// Safe access (MinBy returns null for empty sequences)
var lowestStock = inventory.MinBy(i => i.Quantity);
if (lowestStock is not null)
    Console.WriteLine($"Reorder: {lowestStock.Name} ({lowestStock.Quantity} left)");

// Combined with Where for conditional min/max
var cheapestInStock = products
    .Where(p => p.StockLevel > 0)
    .MinBy(p => p.Price);

// Multiple: top 3 cheapest (still need PartialSort or OrderBy+Take)
var top3Cheapest = products
    .OrderBy(p => p.Price)
    .Take(3)
    .ToList();

Min, Max, Average, First, and Single throw on empty sequences. Guard with Any(), use the OrDefault variants, or use nullable overloads (Min<T, decimal?>) that return null for empty sequences.

csharp
var scores = new List<int>(); // empty!

// ❌ Throws InvalidOperationException on empty
int min = scores.Min();
double avg = scores.Average();

// ✅ Guard with Any
double avg = scores.Any() ? scores.Average() : 0;

// ✅ Nullable overload — returns null for empty
decimal? min = products.Min(p => (decimal?)p.Price); // null if no products
double? avg = scores.Cast<int?>().Average(); // null if empty

// ✅ DefaultIfEmpty — injects a fallback element
double avg = scores.DefaultIfEmpty(0).Average();
int minScore = scores.DefaultIfEmpty(int.MaxValue).Min();

// ✅ .NET 6+ MinBy/MaxBy return null for empty (no exception)
Product? cheapest = products.MinBy(p => p.Price); // null if empty

// ✅ FirstOrDefault with default value
int first = scores.FirstOrDefault(defaultValue: -1);

Separate Sum(), Count(), Min() calls each iterate the sequence. Combine them in one Aggregate call or materialize with ToList() first if the source is expensive (DB query).

csharp
// ❌ 3 separate iterations (or 3 DB queries if IQueryable)
var count = orders.Count();
var total = orders.Sum(o => o.Total);
var avg   = orders.Average(o => (double)o.Total);

// ✅ Option 1: Materialize once, then aggregate in memory
var list  = orders.ToList();     // single DB query
var count = list.Count;
var total = list.Sum(o => o.Total);
var avg   = list.Average(o => (double)o.Total);

// ✅ Option 2: Single Aggregate pass (streaming, no materialization)
var stats = orders.Aggregate(
    seed: (Count: 0, Total: 0m, Min: decimal.MaxValue, Max: 0m),
    func: (acc, o) => (
        acc.Count + 1,
        acc.Total + o.Total,
        o.Total < acc.Min ? o.Total : acc.Min,
        o.Total > acc.Max ? o.Total : acc.Max));

Console.WriteLine($"Count: {stats.Count}");
Console.WriteLine($"Average: {stats.Total / stats.Count:C}");

GroupBy + Count is the standard pattern for frequency analysis. For sorted histograms or bucketing (group by range), use a computed key in the GroupBy selector.

csharp
// Word frequency
var wordFreq = words
    .GroupBy(w => w.ToLower())
    .Select(g => new { Word = g.Key, Count = g.Count() })
    .OrderByDescending(x => x.Count)
    .Take(10);

// Age histogram — bucket by decade
var ageHistogram = users
    .GroupBy(u => u.Age / 10 * 10) // 0, 10, 20, 30 ...
    .Select(g => new { AgeRange = $"{g.Key}–{g.Key + 9}", Count = g.Count() })
    .OrderBy(x => x.AgeRange);

// Score distribution
var distribution = scores
    .GroupBy(s => s switch { < 60 => "F", < 70 => "D", < 80 => "C",
                              < 90 => "B", _ => "A" })
    .ToDictionary(g => g.Key, g => g.Count());

Console.WriteLine($"A: {distribution.GetValueOrDefault("A")} students");

A generator-style custom LINQ operator using yield return is cleaner for running totals than Aggregate. In query contexts, you can simulate a scan with Select + accumulated state captured in a closure.

csharp
// Running total extension method
public static IEnumerable<decimal> RunningSum(this IEnumerable<decimal> source)
{
    decimal total = 0;
    foreach (var item in source)
    {
        total += item;
        yield return total;
    }
}

// Usage
decimal[] dailySales = { 100, 200, 150, 300, 250 };
var cumulative = dailySales.RunningSum().ToArray();
// → 100, 300, 450, 750, 1000

// Pair original with running total
var withCumulative = dailySales
    .Zip(dailySales.RunningSum(), (daily, running) =>
        new { Daily = daily, CumulativeTotal = running });

// Moving average (window of 3)
var movingAvg = Enumerable.Range(2, dailySales.Length - 2)
    .Select(i => dailySales[(i-2)..(i+1)].Average());

Chunk(size) splits a sequence into arrays of at most size elements. The last chunk may be smaller. This replaces custom batch implementations and works lazily on streaming sources.

csharp
// Process 100 records at a time
var ids = Enumerable.Range(1, 1000).ToList();
foreach (int[] batch in ids.Chunk(100))
{
    await dbContext.Products
        .Where(p => batch.Contains(p.Id))
        .ExecuteUpdateAsync(s => s.SetProperty(p => p.IsProcessed, true));
    Console.WriteLine($"Processed {batch.Length} items");
}

// Parallel batch processing
var batches = emails.Chunk(50).ToList();
var tasks = batches.Select(batch => SendBatchAsync(batch));
await Task.WhenAll(tasks);

// Chunk for display: 3 cards per row
@foreach (var row in products.Chunk(3))
{
    <div class="row">
        @foreach (var product in row)
        {
            <div class="col-4">...</div>
        }
    </div>
}
Joining & Set Operations

Join matches elements from two sequences on a common key and projects the pair. It's an inner join — unmatched elements from either side are excluded. For large sequences, LINQ to Objects Join uses a hash lookup internally (O(n+m)).

csharp
// Inner join: orders with their customer
var orderDetails = orders.Join(
    customers,
    o => o.CustomerId,    // outer key
    c => c.Id,            // inner key
    (o, c) => new         // result selector
    {
        OrderId      = o.Id,
        CustomerName = c.Name,
        o.Date,
        o.Total,
    });

// Query syntax (more readable for joins)
var orderDetails =
    from o in orders
    join c in customers on o.CustomerId equals c.Id
    select new { o.Id, CustomerName = c.Name, o.Total };

// Join on composite key
var matched = shipments.Join(
    orders,
    s => new { s.OrderId, s.WarehouseId },
    o => new { o.Id, o.WarehouseId },
    (s, o) => new { Shipment = s, Order = o });

GroupJoin produces each left element paired with all matching right elements (or an empty collection if none). SelectMany with DefaultIfEmpty flattens this into a left outer join — keeping left-side elements that have no match.

csharp
// Left outer join: all customers, with their orders (or null if none)
var customersWithOrders = customers.GroupJoin(
    orders,
    c => c.Id,
    o => o.CustomerId,
    (customer, customerOrders) => new { customer, customerOrders }
).SelectMany(
    x => x.customerOrders.DefaultIfEmpty(), // null for customers with no orders
    (x, order) => new
    {
        x.customer.Name,
        OrderId   = order?.Id,
        OrderDate = order?.Date,
    });

// Query syntax left outer join
var result =
    from c in customers
    join o in orders on c.Id equals o.CustomerId into customerOrders
    from order in customerOrders.DefaultIfEmpty()
    select new
    {
        c.Name,
        OrderId   = order?.Id,
        OrderTotal = order?.Total ?? 0,
    };

// All customers who have NEVER ordered
var noOrders = customers.GroupJoin(
    orders,
    c => c.Id,
    o => o.CustomerId,
    (c, ords) => new { c, HasOrders = ords.Any() }
).Where(x => !x.HasOrders)
 .Select(x => x.c);

Set operations use equality comparison (default Equals/GetHashCode) to deduplicate. Union = all unique from both. Intersect = in both. Except = in first but not second. All return distinct elements.

csharp
var setA = new[] { 1, 2, 3, 4, 5 };
var setB = new[] { 3, 4, 5, 6, 7 };

var union     = setA.Union(setB);     // 1,2,3,4,5,6,7 (deduped)
var intersect = setA.Intersect(setB); // 3,4,5 (common)
var except    = setA.Except(setB);    // 1,2   (in A but not B)

// With custom comparer
var newProducts = incomingProducts.Except(existingProducts, new ProductSkuComparer());

// .NET 6+ by-key variants (compare by property, not whole object)
var union     = listA.UnionBy(listB, p => p.Id);
var intersect = listA.IntersectBy(listB.Select(p => p.Id), p => p.Id);
var except    = listA.ExceptBy(listB.Select(p => p.Id), p => p.Id);

// Tags added and removed between two versions
var added   = newTags.Except(oldTags);
var removed = oldTags.Except(newTags);

Zip pairs the nth element of the first sequence with the nth element of the second. It stops when either sequence runs out. In .NET 6+, the three-sequence overload and tuple overload make it even more concise.

csharp
var names  = new[] { "Alice", "Bob", "Carol" };
var scores = new[] { 95, 87, 92 };

// Classic Zip with result selector
var results = names.Zip(scores, (name, score) => $"{name}: {score}");
// → "Alice: 95", "Bob: 87", "Carol: 92"

// .NET 6+: Zip returns tuples automatically
var pairs = names.Zip(scores); // IEnumerable<(string, int)>
foreach (var (name, score) in pairs)
    Console.WriteLine($"{name} scored {score}");

// Three-way Zip (.NET 6+)
var xs = new[] { 1, 2, 3 };
var ys = new[] { 4, 5, 6 };
var zs = new[] { 7, 8, 9 };
var triples = xs.Zip(ys, zs); // IEnumerable<(int, int, int)>

// Compute pairwise differences
double[] prices = { 100, 105, 98, 112, 108 };
var changes = prices.Zip(prices.Skip(1), (prev, curr) => curr - prev);
// → +5, -7, +14, -4

Chain multiple join clauses in query syntax, or chain Join calls in method syntax. For method syntax, intermediate anonymous types carry previously joined data through the chain.

csharp
// Query syntax — three-way join (most readable for joins)
var fullOrders =
    from o in orders
    join c in customers on o.CustomerId equals c.Id
    join p in payments  on o.Id equals p.OrderId
    select new
    {
        o.Id,
        CustomerName = c.Name,
        o.Total,
        PaymentMethod = p.Method,
        PaidAt = p.ProcessedAt,
    };

// Method syntax — equivalent
var fullOrders = orders
    .Join(customers,
        o => o.CustomerId,
        c => c.Id,
        (o, c) => new { o, c })
    .Join(payments,
        x => x.o.Id,
        p => p.OrderId,
        (x, p) => new
        {
            OrderId      = x.o.Id,
            CustomerName = x.c.Name,
            x.o.Total,
            PaymentMethod = p.Method,
        });

Concat appends all elements of the second sequence after the first, with no deduplication. Union deduplicates across both. Use Concat when you want every element (including duplicates) or when both sequences have no overlapping data.

csharp
var admins   = users.Where(u => u.IsAdmin);
var managers = users.Where(u => u.IsManager);

// Concat — keeps duplicates (admin who is also manager appears twice)
var combined = admins.Concat(managers);

// Union — deduplicates by equality
var unique = admins.Union(managers);

// Concat multiple sources into one pipeline
var allNotifications = inAppAlerts
    .Concat(emailAlerts)
    .Concat(smsAlerts)
    .OrderByDescending(n => n.CreatedAt)
    .Take(20);

// Append/Prepend single elements
var withDefault = products.Prepend(new Product { Name = "Select a product..." });
var withSummary = lineItems.Append(new LineItem { Description = "Total", Price = total });

Distinct() compares elements using their Equals/GetHashCode. For reference types, this is reference equality unless overridden. DistinctBy keeps the first occurrence of each unique key — great for deduplication without implementing IEqualityComparer.

csharp
// Distinct on value types / strings
var uniqueIds = allIds.Distinct();
var uniqueTags = posts.SelectMany(p => p.Tags).Distinct();

// Distinct with custom comparer (for reference types without Equals override)
var uniqueProducts = allProducts.Distinct(new ProductSkuComparer());

// DistinctBy (.NET 6+) — deduplicate by key without a comparer
// Keeps FIRST occurrence of each unique Email
var uniqueUsers = users.DistinctBy(u => u.Email);

// Deduplicate by composite key
var uniqueOrders = orders.DistinctBy(o => (o.CustomerId, o.ProductId));

// DistinctBy in practice: de-duplicate enriched data after multiple joins
var records = rawData
    .Join(enrichmentA, r => r.Id, e => e.RefId, (r, e) => new { r, e })
    .Join(enrichmentB, x => x.r.Id, e => e.RefId, (x, e2) => /* ... */ )
    .DistinctBy(x => x.r.Id); // remove duplicates from fan-out joins

When two sequences are positionally correlated (same order, same length), use Zip instead of a join. For sequences where one is a transformation of the other, Select with index can pair them without materializing both.

csharp
// Compare before/after arrays
var before = new[] { 100m, 200m, 150m };
var after  = new[] { 110m, 195m, 160m };

var comparison = before.Zip(after, (b, a) => new
{
    Before  = b,
    After   = a,
    Change  = a - b,
    ChangePercent = (a - b) / b * 100,
});

// Assert pairwise correctness in tests
var expected = new[] { 1, 4, 9, 16, 25 };
var actual   = input.Select(n => n * n);
var mismatches = expected
    .Zip(actual, (e, a) => (Expected: e, Actual: a))
    .Where(pair => pair.Expected != pair.Actual)
    .ToList();

// Offset diff — compare each element with the next
var prices = new[] { 10.0, 12.0, 11.5, 13.0 };
var diffs  = prices
    .Zip(prices.Skip(1), (prev, curr) => (Prev: prev, Curr: curr, Diff: curr - prev))
    .Select((d, i) => $"Day {i + 1}→{i + 2}: {d.Diff:+0.00;-0.00}");
Element & Range Operations

Skip(n) bypasses the first n elements; Take(n) returns at most n elements. Combine them for page-based paging. In EF Core, they translate to OFFSET/FETCH NEXT in SQL.

csharp
// Page-based paging
int pageSize   = 20;
int pageNumber = 3; // 1-based

var page = products
    .OrderBy(p => p.Name)
    .Skip((pageNumber - 1) * pageSize) // skip first 40
    .Take(pageSize)                     // take next 20
    .ToList();

// Skip/Take with Range (C# 8+ index/range syntax)
var thirdPage = products
    .OrderBy(p => p.Id)
    .Take(40..60)   // elements at indices 40–59
    .ToList();

// Get everything after the first premium item
var afterFirst = products
    .SkipWhile(p => p.Tier != "Premium")
    .Skip(1); // skip the first Premium itself

// Take last N elements (.NET 6+)
var lastFive = products.TakeLast(5);
var skipLast = products.SkipLast(5); // all but last 5

SkipWhile skips elements as long as the predicate is true, then returns all remaining (including later elements that would fail the predicate). TakeWhile returns elements as long as the predicate is true, then stops — even if later elements would pass.

csharp
var numbers = new[] { 1, 2, 3, 10, 4, 5 };

// TakeWhile — stops at first element that fails (10)
var small = numbers.TakeWhile(n => n < 10);
// → 1, 2, 3  (stops, does NOT include 4, 5 even though they pass)

// SkipWhile — skips until predicate fails, then takes all remaining
var rest = numbers.SkipWhile(n => n < 10);
// → 10, 4, 5

// Use case: process a log file, skip header lines
var logEntries = lines
    .SkipWhile(l => l.StartsWith("#")) // skip comment/header lines
    .Where(l => l.Contains("ERROR"));

// Use case: read until a sentinel value
var commands = input
    .TakeWhile(cmd => cmd != "quit");

// Real-time stream: take prices while below threshold
var affordable = priceStream
    .TakeWhile(p => p < maxBudget);

Enumerable.Range(start, count) generates count integers starting at start. It's lazy, so it's efficient even for large ranges. Use it to drive queries, generate test data, or create index sequences.

csharp
// Generate numbers 1-100
var nums = Enumerable.Range(1, 100);

// All even numbers from 2 to 50
var evens = Enumerable.Range(1, 25).Select(n => n * 2);

// Generate month labels for a year
var months = Enumerable.Range(1, 12)
    .Select(m => new DateTime(2024, m, 1).ToString("MMMM"));

// Fill missing months in a dataset (left join pattern)
var allMonths  = Enumerable.Range(1, 12);
var salesData  = orders.GroupBy(o => o.Date.Month)
                        .ToDictionary(g => g.Key, g => g.Sum(o => o.Total));
var fullYear   = allMonths.Select(m => new
{
    Month = m,
    Total = salesData.GetValueOrDefault(m, 0m),
});

// Generate test data
var testProducts = Enumerable.Range(1, 100)
    .Select(i => new Product { Id = i, Name = $"Product {i}", Price = i * 9.99m })
    .ToList();

Enumerable.Repeat(element, count) yields the same value n times. Useful for generating placeholder data, padding sequences, or building test inputs. The element is the same reference for reference types.

csharp
// Create a fixed-size array filled with a default value
var zeros     = Enumerable.Repeat(0, 10).ToArray();
var placeholders = Enumerable.Repeat("N/A", 5).ToList();

// Pad a sequence to a minimum length
var padded = data
    .Concat(Enumerable.Repeat(0m, Math.Max(0, 12 - data.Count)))
    .ToList(); // ensures at least 12 elements

// Generate a repeated pattern
var checkered = Enumerable.Range(0, 64)
    .Select(i => (i / 8 + i % 8) % 2 == 0 ? "⬛" : "⬜");

// Warning: for reference types, all elements share the SAME reference
var shared = Enumerable.Repeat(new List<int>(), 3).ToList();
// shared[0], [1], [2] all point to the same List!

// Fix: use Select to create independent instances
var independent = Enumerable.Range(0, 3)
    .Select(_ => new List<int>())
    .ToList();

Append and Prepend return new lazy sequences with the element added at the end or beginning. They don't modify the original collection — useful for building dropdown lists with a default option or adding summary rows.

csharp
// Add "All" option at the top of a filter dropdown
var filterOptions = categories
    .OrderBy(c => c.Name)
    .Prepend(new Category { Id = 0, Name = "— All Categories —" });

// Add a summary row at the bottom of a report
var reportRows = lineItems
    .Append(new LineItem
    {
        Description = "Total",
        Amount      = lineItems.Sum(l => l.Amount),
    });

// Chain multiple additions
var breadcrumbs = pagePath
    .Prepend(new Link { Label = "Home", Url = "/" })
    .Append(new Link { Label = "Current Page", Url = "#", Active = true });

// Note: original collection is NOT modified
var original = new[] { 1, 2, 3 };
var extended = original.Append(4).Prepend(0);
// original is still [1, 2, 3]
// extended lazily yields [0, 1, 2, 3, 4]

ElementAt(i) returns the element at index i (throws if out of range). ElementAtOrDefault(i) returns the default value. In .NET 8, these accept a System.Index (including ^1 for last element). For IList<T>, they use index access directly; for others, they iterate.

csharp
var items = Enumerable.Range(10, 10); // 10..19

// Access by index
int fifth = items.ElementAt(4);        // 14
int last  = items.ElementAt(^1);       // 19 (.NET 8+ Index support)
int safe  = items.ElementAtOrDefault(99); // 0 (default, no exception)

// Median of a sorted list
var sorted = scores.OrderBy(s => s).ToList();
double median = sorted.Count % 2 == 0
    ? (sorted.ElementAt(sorted.Count / 2 - 1) + sorted.ElementAt(sorted.Count / 2)) / 2.0
    : sorted.ElementAt(sorted.Count / 2);

// Prefer list indexer over ElementAt for IList — O(1) vs O(n)
if (source is IList<T> list)
    return list[index];   // O(1)
else
    return source.ElementAt(index); // O(n) for non-indexed

DefaultIfEmpty returns the sequence unchanged if it has any elements, or a single-element sequence containing the default (or a specified value) if empty. Essential for left outer joins and safe aggregations.

csharp
// Safe average — fallback to 0 if no scores
double avg = scores.DefaultIfEmpty(0).Average();

// Custom fallback object for empty sequences
var featured = products
    .Where(p => p.IsFeatured)
    .DefaultIfEmpty(new Product { Name = "No featured products", Price = 0 })
    .First();

// Left outer join pattern with DefaultIfEmpty
var customerOrders =
    from c in customers
    join o in orders on c.Id equals o.CustomerId into cOrders
    from order in cOrders.DefaultIfEmpty() // null for customers with no orders
    select new { c.Name, OrderId = order?.Id ?? -1 };

// Conditional display: show "None" in dropdown if empty
var options = tags
    .Select(t => new SelectListItem { Value = t.Id.ToString(), Text = t.Name })
    .DefaultIfEmpty(new SelectListItem { Value = "", Text = "(none)" });

SequenceEqual returns true if two sequences have the same elements in the same order. It uses Equals by default but accepts a custom IEqualityComparer<T>. It short-circuits on the first mismatch.

csharp
var a = new[] { 1, 2, 3 };
var b = new[] { 1, 2, 3 };
var c = new[] { 3, 2, 1 };

bool same     = a.SequenceEqual(b);              // true
bool reordered = a.SequenceEqual(c);             // false — order matters

// Case-insensitive string sequence comparison
var tags1 = new[] { "Flutter", "Dart", "Firebase" };
var tags2 = new[] { "flutter", "dart", "firebase" };
bool eqIgnoreCase = tags1.SequenceEqual(tags2, StringComparer.OrdinalIgnoreCase); // true

// Test assertion: verify LINQ result matches expected
var expected = new[] { 3, 6, 9 };
var actual   = Enumerable.Range(1, 9).Where(n => n % 3 == 0);
Assert.IsTrue(expected.SequenceEqual(actual));

// Order-insensitive comparison — sort both first
bool sameElements = a.OrderBy(x => x).SequenceEqual(c.OrderBy(x => x)); // true
LINQ to Objects — Dictionaries & Collections
ToDictionary, ToLookup, HashSets, and querying complex collection types

ToDictionary throws on duplicate keys. Use GroupBy then ToDictionary, or aggregate with Aggregate, or filter distinct before projecting to handle duplicates gracefully.

csharp
// Safe: take last value wins (avoids ArgumentException)
var dict = items
    .GroupBy(x => x.Key)
    .ToDictionary(g => g.Key, g => g.Last());

// Safe: take first occurrence
var dict2 = items
    .GroupBy(x => x.Id)
    .ToDictionary(g => g.Key, g => g.First());

// Aggregate approach — explicit control
var dict3 = items.Aggregate(
    new Dictionary<int, Item>(),
    (acc, item) => { acc[item.Id] = item; return acc; }
);

// Risky (throws on duplicates) — only safe when keys are unique
var dict4 = items.ToDictionary(x => x.Id);

ToLookup materializes the grouping immediately into a read-optimized structure (O(1) key lookup). GroupBy is lazy. Use ToLookup when you'll query the same grouping multiple times.

csharp
// ToLookup: materialized, O(1) key access, safe for repeated reads
ILookup<string, Product> byCategory =
    products.ToLookup(p => p.Category);

// O(1) lookup — no re-enumeration
IEnumerable<Product> electronics = byCategory["Electronics"];
IEnumerable<Product> books       = byCategory["Books"];

// Missing key returns empty sequence (no KeyNotFoundException)
var missing = byCategory["XYZ"]; // empty, not null

// GroupBy: lazy — re-enumerates source on each access
var grouped = products.GroupBy(p => p.Category); // deferred
// Each iteration of grouped re-reads the source

Dictionaries implement IEnumerable<KeyValuePair<TKey, TValue>>, so all LINQ operators work. Deconstruct with var (k, v) or use .Key / .Value properties.

csharp
var scores = new Dictionary<string, int>
{
    ["Alice"] = 95, ["Bob"] = 72, ["Carol"] = 88
};

// Filter by value
var highScorers = scores
    .Where(kvp => kvp.Value >= 85)
    .Select(kvp => kvp.Key);
// → ["Alice", "Carol"]

// Invert: swap keys and values
var inverted = scores.ToDictionary(kvp => kvp.Value, kvp => kvp.Key);

// Sort by value descending
var ranked = scores
    .OrderByDescending(kvp => kvp.Value)
    .Select((kvp, i) => $"{i + 1}. {kvp.Key}: {kvp.Value}")
    .ToList();

ToHashSet() materializes a sequence into O(1) membership-check collection. Pre-build the set when you'll perform many Contains checks inside a LINQ pipeline, avoiding O(n²) list searches.

csharp
var bannedIds = new[] { 3, 7, 12, 99 }.ToHashSet(); // O(1) Contains

// O(n) not O(n²) — HashSet.Contains is O(1)
var allowed = users.Where(u => !bannedIds.Contains(u.Id)).ToList();

// Custom comparer for case-insensitive string set
HashSet<string> tagsSet = tags.ToHashSet(StringComparer.OrdinalIgnoreCase);

var matched = posts.Where(p => p.Tags.Any(t => tagsSet.Contains(t)));

// Deduplicate while preserving order
var seen = new HashSet<int>();
var distinct = items
    .Where(x => seen.Add(x.Id)) // Add returns false on duplicate
    .ToList();

Combine Select (transform) with SelectMany (flatten) to reshape hierarchical data into flat projections, carrying parent context via the two-argument overload.

csharp
// Departments → Employees → Projects (3 levels deep)
var assignments = departments
    .SelectMany(dept => dept.Employees,
        (dept, emp) => new { dept, emp })
    .SelectMany(x => x.emp.Projects,
        (x, proj) => new
        {
            Department = x.dept.Name,
            Employee   = x.emp.Name,
            Project    = proj.Title
        });

// Flatten 2-D array
int[][] matrix = { new[]{1,2}, new[]{3,4}, new[]{5,6} };
int[] flat = matrix.SelectMany(row => row).ToArray(); // [1,2,3,4,5,6]

Enumerable.Range, Repeat, and Empty create sequences without allocating arrays. Arrays work natively with LINQ; Spans require conversion via AsEnumerable() or ToArray().

csharp
// Generate sequences
var squares   = Enumerable.Range(1, 10).Select(n => n * n);
var zeros     = Enumerable.Repeat(0, 5).ToArray(); // [0,0,0,0,0]
var empty     = Enumerable.Empty<string>();

// Infinite sequence via iterator + Take
IEnumerable<int> Fibonacci()
{
    int a = 0, b = 1;
    while (true) { yield return a; (a, b) = (b, a + b); }
}
var first10Fib = Fibonacci().Take(10).ToList();

// Span → LINQ (must materialise first)
Span<int> span = stackalloc int[] { 3, 1, 4, 1, 5 };
var sorted = span.ToArray().OrderBy(x => x).ToList();

Concatenate both dictionaries as KVP sequences, then group and resolve conflicts with your chosen strategy (last-wins, first-wins, sum, max, etc.).

csharp
var a = new Dictionary<string, int> { ["x"] = 1, ["y"] = 2 };
var b = new Dictionary<string, int> { ["y"] = 99, ["z"] = 3 };

// Last-wins (b overrides a on collision)
var merged = a
    .Concat(b)
    .GroupBy(kvp => kvp.Key)
    .ToDictionary(g => g.Key, g => g.Last().Value);
// { x=1, y=99, z=3 }

// Sum values on collision
var summed = a
    .Concat(b)
    .GroupBy(kvp => kvp.Key)
    .ToDictionary(g => g.Key, g => g.Sum(kvp => kvp.Value));
// { x=1, y=101, z=3 }

All three implement IEnumerable<T>, so LINQ works directly. However, enumeration order follows each collection's natural order (LIFO for Stack, FIFO for Queue).

csharp
var stack = new Stack<int>(new[] { 1, 2, 3 }); // Push order: 1,2,3
// Stack enumerates LIFO: 3, 2, 1
var top2 = stack.Take(2).ToList(); // [3, 2]

var queue = new Queue<string>(new[] { "a", "b", "c" });
var hasB  = queue.Any(s => s == "b"); // true
var upper = queue.Select(s => s.ToUpper()).ToList();

var list = new LinkedList<int>(new[] { 10, 20, 30 });
var evens = list.Where(n => n % 2 == 0).Sum(); // 60

// Convert back after LINQ transformation
var newStack = new Stack<int>(stack.Where(n => n > 1).OrderBy(n => n));
LINQ to SQL & Entity Framework Core
EF Core query translation, eager loading, projections, and raw SQL

EF Core parses the LINQ expression tree and generates SQL. It fails when you call C# methods it cannot map to SQL. The fix is to either use supported EF functions or switch to client evaluation after AsEnumerable().

csharp
// ✅ Translatable — EF knows how to convert this
var users = await ctx.Users
    .Where(u => u.Email.Contains("@gmail"))
    .OrderBy(u => u.Name)
    .ToListAsync();

// ❌ Breaks: custom C# method is opaque to EF
bool IsVip(User u) => u.Spend > 1000; // EF can't translate this
var vips = ctx.Users.Where(u => IsVip(u)); // throws at runtime

// ✅ Fix A: inline the expression
var vips = ctx.Users.Where(u => u.Spend > 1000);

// ✅ Fix B: switch to LINQ to Objects after fetching
var vips = await ctx.Users.ToListAsync(); // load first
vips = vips.Where(u => IsVip(u)).ToList(); // then filter in memory

Each Include on a collection navigation adds a SQL JOIN that can multiply rows. With multiple collection includes EF Core 5+ uses split queries to avoid exponential row counts.

csharp
// Eager load with nested Include
var orders = await ctx.Orders
    .Include(o => o.Customer)
    .Include(o => o.Items)
        .ThenInclude(i => i.Product)
    .ToListAsync();

// ⚠️ Cartesian explosion: two collection Includes → rows multiply
// Fix: use AsSplitQuery() to issue separate SQL queries
var orders = await ctx.Orders
    .Include(o => o.Items)
    .Include(o => o.Tags)
    .AsSplitQuery()          // separate SELECT per collection
    .ToListAsync();

// Global split query option (in DbContext configuration)
// optionsBuilder.UseSqlServer(conn, o => o.UseQuerySplittingBehavior(
//     QuerySplittingBehavior.SplitQuery));

AsNoTracking() skips the change tracker, reducing memory and improving read performance by ~20-30%. Use it for read-only queries; avoid it when you intend to update the loaded entities.

csharp
// Read-only report — no tracking needed
var report = await ctx.Orders
    .AsNoTracking()
    .Where(o => o.Date >= DateTime.Today.AddMonths(-1))
    .Select(o => new { o.Id, o.Total, o.Customer.Name })
    .ToListAsync();

// AsNoTrackingWithIdentityResolution: deduplicates related entities
// (avoids multiple object instances for the same DB row)
var products = await ctx.Products
    .AsNoTrackingWithIdentityResolution()
    .Include(p => p.Category)
    .ToListAsync();

// Default tracking (required for SaveChanges to detect changes)
var user = await ctx.Users.FindAsync(userId); // tracked
user.Name = "New Name";
await ctx.SaveChangesAsync();                 // detects change, UPDATEs

Projecting to an anonymous type or a DTO with Select before materializing causes EF Core to emit a SELECT with only the needed columns rather than SELECT *.

csharp
// ❌ Loads all columns including large BLOB fields
var all = await ctx.Products.ToListAsync();
var names = all.Select(p => p.Name).ToList();

// ✅ Only fetches Name and Price columns
var dtos = await ctx.Products
    .Where(p => p.IsActive)
    .Select(p => new ProductDto
    {
        Name  = p.Name,
        Price = p.Price
    })
    .ToListAsync();

// Projecting computed values from DB functions
var summaries = await ctx.Orders
    .GroupBy(o => o.CustomerId)
    .Select(g => new
    {
        CustomerId = g.Key,
        Count      = g.Count(),
        Total      = g.Sum(o => o.Amount)
    })
    .ToListAsync();

FromSqlRaw / FromSqlInterpolated lets you start a query from a raw SQL fragment and then chain LINQ operators that EF composes into the outer SQL.

csharp
// FromSqlInterpolated: parameterized, safe from injection
string term = "shirt";
var products = await ctx.Products
    .FromSqlInterpolated($"SELECT * FROM Products WHERE Name LIKE '%' + {term} + '%'")
    .Where(p => p.IsActive)        // composed into outer WHERE
    .OrderBy(p => p.Price)
    .Take(20)
    .ToListAsync();

// Non-query raw SQL (inserts/updates/stored procs)
await ctx.Database.ExecuteSqlInterpolatedAsync(
    $"UPDATE Products SET Stock = Stock - {qty} WHERE Id = {id}");

// Keyless entity for arbitrary result sets
var results = await ctx.Database
    .SqlQueryRaw<SalesSummary>("EXEC sp_MonthlySales @Year = {0}", year)
    .ToListAsync();

Cursor (keyset) pagination uses a WHERE id > lastId predicate rather than OFFSET, avoiding expensive table scans on large datasets and maintaining stable pages under concurrent writes.

csharp
// Offset pagination — degrades at large offsets
var pageBad = await ctx.Orders
    .OrderBy(o => o.Id)
    .Skip(page * pageSize)   // ⚠️ expensive at high pages
    .Take(pageSize)
    .ToListAsync();

// Cursor pagination — O(log n) via index seek
async Task<List<Order>> GetPage(int? afterId, int pageSize)
{
    var q = ctx.Orders.OrderBy(o => o.Id).AsQueryable();
    if (afterId.HasValue)
        q = q.Where(o => o.Id > afterId.Value); // seek, not scan
    return await q.Take(pageSize).ToListAsync();
}

// Usage
var page1 = await GetPage(null,       20);
var page2 = await GetPage(page1.Last().Id, 20);

EF Core translates GroupBy only when the projection uses aggregate functions (Count, Sum, etc.) directly. Accessing group elements in a non-aggregated projection falls back to client evaluation.

csharp
// ✅ Translates: pure aggregation projection
var counts = await ctx.Orders
    .GroupBy(o => o.Status)
    .Select(g => new { Status = g.Key, Count = g.Count() })
    .ToListAsync();
// → SELECT Status, COUNT(*) FROM Orders GROUP BY Status

// ❌ Does NOT translate: accessing individual group items
var grouped = await ctx.Orders
    .GroupBy(o => o.CustomerId)
    .Select(g => new { g.Key, Orders = g.ToList() }) // client eval
    .ToListAsync();

// ✅ Workaround: fetch flat, then group in memory
var flat = await ctx.Orders.AsNoTracking().ToListAsync();
var grouped = flat.GroupBy(o => o.CustomerId)
                  .ToDictionary(g => g.Key, g => g.ToList());

EF Core compiles expression trees to SQL on every invocation. EF.CompileAsyncQuery pre-compiles the query once, skipping repeated parsing/translation overhead on hot paths.

csharp
// Define once (static field)
private static readonly Func<AppDbContext, int, Task<User?>>
    GetUserById = EF.CompileAsyncQuery(
        (AppDbContext ctx, int id) =>
            ctx.Users.FirstOrDefault(u => u.Id == id));

// Call many times — no re-compilation overhead
var user = await GetUserById(dbContext, userId);

// Compiled query with multiple parameters
private static readonly Func<AppDbContext, string, int, IAsyncEnumerable<Product>>
    SearchProducts = EF.CompileAsyncQuery(
        (AppDbContext ctx, string term, int maxPrice) =>
            ctx.Products.Where(p => p.Name.Contains(term) && p.Price <= maxPrice));
Advanced LINQ Patterns
Custom operators, expression trees, PLINQ, dynamic queries, and debugging

Extension methods on IEnumerable<T> that use yield return create lazy custom operators that compose naturally into LINQ pipelines with deferred execution.

csharp
public static class LinqExtensions
{
    // Emit only items that differ from the previous
    public static IEnumerable<T> DistinctConsecutive<T>(
        this IEnumerable<T> source, IEqualityComparer<T>? comparer = null)
    {
        comparer ??= EqualityComparer<T>.Default;
        T? prev = default; bool first = true;
        foreach (var item in source)
        {
            if (first || !comparer.Equals(prev!, item))
            { yield return item; prev = item; first = false; }
        }
    }

    // Emit items in batches of size n
    public static IEnumerable<IEnumerable<T>> Batch<T>(
        this IEnumerable<T> source, int size)
    {
        var batch = new List<T>(size);
        foreach (var item in source)
        {
            batch.Add(item);
            if (batch.Count == size) { yield return batch; batch = new(size); }
        }
        if (batch.Count > 0) yield return batch;
    }
}

// Usage
var batches = Enumerable.Range(1, 10).Batch(3).ToList();
// [[1,2,3],[4,5,6],[7,8,9],[10]]

When a lambda is typed as Expression<Func<T,bool>> instead of Func<T,bool>>, the C# compiler builds a data structure (AST) you can walk, inspect, and rewrite before execution.

csharp
// Inspect an expression tree
Expression<Func<int, bool>> expr = x => x > 5;

var binary = (BinaryExpression)expr.Body;
Console.WriteLine(binary.NodeType);  // GreaterThan
Console.WriteLine(binary.Right);     // 5

// Combine two predicates (AND)
static Expression<Func<T, bool>> And<T>(
    Expression<Func<T, bool>> left,
    Expression<Func<T, bool>> right)
{
    var param = left.Parameters[0];
    var body  = Expression.AndAlso(
        left.Body,
        Expression.Invoke(right, param));
    return Expression.Lambda<Func<T, bool>>(body, param);
}

Expression<Func<Product, bool>> isActive = p => p.IsActive;
Expression<Func<Product, bool>> isCheap  = p => p.Price < 50;
var both = And(isActive, isCheap);
var list = ctx.Products.Where(both).ToList(); // translated to SQL

The Specification pattern encapsulates a query predicate in a reusable object. Combining specifications with AndAlso / OrElse expression composition lets EF Core translate the combined filter to SQL.

csharp
public abstract class Specification<T>
{
    public abstract Expression<Func<T, bool>> ToExpression();

    public Specification<T> And(Specification<T> other) =>
        new AndSpec<T>(this, other);

    private class AndSpec<TT> : Specification<TT>
    {
        readonly Specification<TT> _l, _r;
        public AndSpec(Specification<TT> l, Specification<TT> r)
            => (_l, _r) = (l, r);
        public override Expression<Func<TT, bool>> ToExpression()
        {
            var l = _l.ToExpression();
            var r = _r.ToExpression();
            var p = l.Parameters[0];
            var body = Expression.AndAlso(l.Body,
                Expression.Invoke(r, p));
            return Expression.Lambda<Func<TT, bool>>(body, p);
        }
    }
}

public class ActiveProductSpec : Specification<Product>
{
    public override Expression<Func<Product, bool>> ToExpression()
        => p => p.IsActive;
}

// Usage
var spec = new ActiveProductSpec().And(new CheapProductSpec());
var products = await ctx.Products
    .Where(spec.ToExpression())
    .ToListAsync();

PLINQ (Parallel LINQ) partitions the source across threads via AsParallel(). It helps for CPU-bound, independent work on large collections but adds overhead for small datasets or I/O-bound operations.

csharp
// CPU-bound: parallel processing benefits
var results = largeList
    .AsParallel()
    .WithDegreeOfParallelism(4)
    .Where(x => IsCpuIntensive(x))
    .Select(x => Transform(x))
    .ToList(); // unordered by default (faster)

// Preserve input order (adds merge cost)
var ordered = largeList
    .AsParallel()
    .AsOrdered()
    .Select(x => Transform(x))
    .ToList();

// ⚠️ Bad fit: I/O-bound work (use async instead)
// ⚠️ Bad fit: tiny collections (partitioning overhead dominates)
// ⚠️ Bad fit: operations with shared mutable state (race conditions)

// Sequential fallback via AsSequential()
var seq = largeList.AsParallel().Where(x => x > 0).AsSequential().FirstOrDefault();

Build Expression<Func<T, object>> dynamically using reflection and Expression.Property, then pass to OrderBy/Where to generate SQL-translatable dynamic queries without string-eval.

csharp
public static IQueryable<T> OrderByField<T>(
    IQueryable<T> query, string fieldName, bool ascending = true)
{
    var param    = Expression.Parameter(typeof(T), "x");
    var property = Expression.Property(param, fieldName);
    var cast     = Expression.Convert(property, typeof(object));
    var lambda   = Expression.Lambda<Func<T, object>>(cast, param);

    return ascending
        ? query.OrderBy(lambda)
        : query.OrderByDescending(lambda);
}

// Usage — field name comes from a UI dropdown
var sorted = OrderByField(ctx.Products.AsQueryable(), "Price", ascending: false);
var page   = await sorted.Skip(0).Take(20).ToListAsync();

// Dynamic filter: contains check on any string property
public static IQueryable<T> Search<T>(IQueryable<T> q, string field, string value)
{
    var param  = Expression.Parameter(typeof(T));
    var prop   = Expression.Property(param, field);
    var method = typeof(string).GetMethod("Contains", new[] { typeof(string) })!;
    var body   = Expression.Call(prop, method, Expression.Constant(value));
    return q.Where(Expression.Lambda<Func<T, bool>>(body, param));
}

Insert a Do / tap operator via a custom extension that calls a side-effect action (like logging) for each item while passing it through unchanged.

csharp
// Tap extension — inspect without breaking the chain
public static IEnumerable<T> Tap<T>(
    this IEnumerable<T> source, Action<T> action)
{
    foreach (var item in source)
    { action(item); yield return item; }
}

// Usage: log after each step
var result = numbers
    .Where(n => n > 0)
    .Tap(n => Console.WriteLine($"After filter: {n}"))
    .Select(n => n * 2)
    .Tap(n => Console.WriteLine($"After select: {n}"))
    .ToList();

// For EF Core: log generated SQL
ctx.Database.Log = sql => Console.WriteLine(sql); // EF Core 1.x
// EF Core 5+: configure via DbContextOptionsBuilder logging
// optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information)

// Materialize mid-chain to inspect
var filtered = ctx.Orders.Where(o => o.Total > 100).ToList(); // breakpoint here
var result2  = filtered.Select(o => o.Id).ToList();

PredicateBuilder (or a manual equivalent) starts from a base predicate and accumulates additional predicates with OrElse / AndAlso, producing a single EF-translatable expression.

csharp
// Simple manual PredicateBuilder
public static class PredicateBuilder
{
    public static Expression<Func<T, bool>> True<T>()  => _ => true;
    public static Expression<Func<T, bool>> False<T>() => _ => false;

    public static Expression<Func<T, bool>> Or<T>(
        this Expression<Func<T, bool>> left,
        Expression<Func<T, bool>> right)
    {
        var p    = left.Parameters[0];
        var body = Expression.OrElse(left.Body, Expression.Invoke(right, p));
        return Expression.Lambda<Func<T, bool>>(body, p);
    }
}

// Build OR filter from a list of status codes
var statuses = new[] { "Pending", "Processing" };
var predicate = PredicateBuilder.False<Order>();
foreach (var s in statuses)
{
    var captured = s; // avoid closure capture bug
    predicate = predicate.Or(o => o.Status == captured);
}
var orders = await ctx.Orders.Where(predicate).ToListAsync();
// → WHERE Status = 'Pending' OR Status = 'Processing'

EF Core's IQueryExpressionInterceptor (EF 9+) lets you rewrite every query's expression tree before SQL generation — useful for global soft-delete filters or tenant isolation without per-query code.

csharp
// Alternative: HasQueryFilter on the model (EF Core 2+, preferred)
public class AppDbContext : DbContext
{
    public DbSet<Post> Posts { get; set; }

    protected override void OnModelCreating(ModelBuilder mb)
    {
        // Global soft-delete filter — applies to ALL queries on Post
        mb.Entity<Post>().HasQueryFilter(p => !p.IsDeleted);
    }
}

// Queries automatically include WHERE IsDeleted = 0
var posts = await ctx.Posts.ToListAsync(); // only non-deleted

// Bypass the global filter when needed
var allPosts = await ctx.Posts.IgnoreQueryFilters().ToListAsync();

// Per-tenant filter (inject tenant ID via service)
mb.Entity<Order>().HasQueryFilter(o => o.TenantId == _tenantService.CurrentId);
Async LINQ & IAsyncEnumerable
ToListAsync, await foreach, System.Linq.Async, and async streaming patterns

ToListAsync awaits the database round-trip without blocking a thread. ToList blocks the calling thread for the full duration of the I/O, wasting thread-pool resources under load.

csharp
// ❌ Blocks thread — bad in ASP.NET Core controllers
var products = ctx.Products.Where(p => p.IsActive).ToList();

// ✅ Frees thread during DB round-trip
var products = await ctx.Products
    .Where(p => p.IsActive)
    .ToListAsync(cancellationToken);

// Other async materializers
int    count   = await ctx.Orders.CountAsync();
bool   any     = await ctx.Users.AnyAsync(u => u.IsAdmin);
User?  first   = await ctx.Users.FirstOrDefaultAsync(u => u.Id == id);
double average = await ctx.Orders.AverageAsync(o => o.Total);

// Multiple queries in parallel (different DbContext instances!)
var (users, orders) = await (
    ctx1.Users.ToListAsync(),
    ctx2.Orders.ToListAsync()
).WhenAll();

IAsyncEnumerable<T> yields items as they arrive from the database without buffering the entire result set. Use AsAsyncEnumerable() in EF Core + await foreach to process rows as they stream.

csharp
// Stream rows one at a time — no full materialization
await foreach (var order in ctx.Orders
    .Where(o => o.Status == "Pending")
    .AsAsyncEnumerable())   // key: no ToListAsync
{
    await ProcessOrderAsync(order);  // process each row as it arrives
}

// Custom async generator
async IAsyncEnumerable<int> GenerateAsync(
    [EnumeratorCancellation] CancellationToken ct = default)
{
    for (int i = 0; i < 100; i++)
    {
        await Task.Delay(10, ct);
        yield return i;
    }
}

// Consume
await foreach (var n in GenerateAsync(cancellationToken))
    Console.Write(n);

The System.Linq.Async NuGet package provides async equivalents of all LINQ operators (WhereAwait, SelectAwait, ToListAsync, etc.) for use on IAsyncEnumerable<T> streams.

csharp
// Package: dotnet add package System.Linq.Async

IAsyncEnumerable<Product> stream = GetProductStreamAsync();

// Async LINQ operators
var cheapActive = stream
    .WhereAwait(async p => await IsActiveAsync(p) && p.Price < 100)
    .SelectAwait(async p => await EnrichAsync(p))
    .OrderBy(p => p.Name); // synchronous sort after async transforms

// Materialize
var list  = await stream.ToListAsync();
var count = await stream.CountAsync();
var first = await stream.FirstOrDefaultAsync(p => p.Price < 10);

// Non-async predicates still work
var filtered = stream.Where(p => p.IsActive).Select(p => p.Name);

Pass the token to ToListAsync(ct), FirstOrDefaultAsync(pred, ct), and into your own async iterators via [EnumeratorCancellation]. EF Core async methods also accept a token.

csharp
// EF Core — pass token to materializer
public async Task<List<Order>> GetOrdersAsync(CancellationToken ct)
{
    return await ctx.Orders
        .Where(o => o.IsOpen)
        .OrderBy(o => o.CreatedAt)
        .ToListAsync(ct); // cancels DB query on token trigger
}

// Async iterator — honour token during generation
async IAsyncEnumerable<int> CountUpAsync(
    [EnumeratorCancellation] CancellationToken ct = default)
{
    for (int i = 0; ; i++)
    {
        ct.ThrowIfCancellationRequested();
        await Task.Delay(50, ct);
        yield return i;
    }
}

// Consumer passes token via WithCancellation
await foreach (var n in CountUpAsync().WithCancellation(cts.Token))
    Console.WriteLine(n);

Channel<T> (System.Threading.Channels) is a thread-safe async queue. Producers write with WriteAsync; consumers read via ReadAllAsync which returns IAsyncEnumerable<T> compatible with LINQ.

csharp
var channel = Channel.CreateBounded<int>(capacity: 100); // backpressure at 100

// Producer task
async Task ProduceAsync()
{
    for (int i = 0; i < 1000; i++)
    {
        await channel.Writer.WriteAsync(i); // blocks when full (backpressure)
    }
    channel.Writer.Complete();
}

// Consumer using LINQ on the IAsyncEnumerable
async Task ConsumeAsync()
{
    var results = channel.Reader.ReadAllAsync()   // IAsyncEnumerable<int>
        .Where(n => n % 2 == 0)                  // filter evens (sync)
        .SelectAwait(async n => await ProcessAsync(n)); // async transform

    await foreach (var item in results)
        Console.WriteLine(item);
}

await Task.WhenAll(ProduceAsync(), ConsumeAsync());

An async iterator that yields pages one at a time avoids loading all data upfront while still giving callers the ability to stop early via cancellation.

csharp
async IAsyncEnumerable<List<Order>> StreamPagesAsync(
    int pageSize,
    [EnumeratorCancellation] CancellationToken ct = default)
{
    int? lastId = null;
    while (true)
    {
        var q = ctx.Orders.OrderBy(o => o.Id).AsQueryable();
        if (lastId.HasValue) q = q.Where(o => o.Id > lastId.Value);
        var page = await q.Take(pageSize).AsNoTracking().ToListAsync(ct);
        if (page.Count == 0) yield break;
        yield return page;
        lastId = page.Last().Id;
    }
}

// Consumer
await foreach (var page in StreamPagesAsync(100, cts.Token))
{
    await BulkInsertAsync(page);
    Console.WriteLine($"Processed {page.Count} orders");
}

Use System.Linq.Async's Merge operator or produce items from multiple sources into a single Channel<T> with concurrent writers to interleave streams.

csharp
// System.Linq.Async: Merge (interleaves as items arrive)
IAsyncEnumerable<int> stream1 = GetStreamA();
IAsyncEnumerable<int> stream2 = GetStreamB();

await foreach (var item in AsyncEnumerableEx.Merge(stream1, stream2))
    Console.WriteLine(item);

// Manual Channel-based merge
async IAsyncEnumerable<T> MergeAsync<T>(
    IEnumerable<IAsyncEnumerable<T>> sources,
    [EnumeratorCancellation] CancellationToken ct = default)
{
    var channel = Channel.CreateUnbounded<T>();
    var tasks = sources.Select(async src =>
    {
        await foreach (var item in src.WithCancellation(ct))
            await channel.Writer.WriteAsync(item, ct);
    }).ToList();

    _ = Task.WhenAll(tasks).ContinueWith(_ => channel.Writer.Complete());

    await foreach (var item in channel.Reader.ReadAllAsync(ct))
        yield return item;
}

Bridge async streams and Reactive Extensions by wrapping IAsyncEnumerable<T> in an observable using Observable.Create or the System.Reactive.Linq.AsyncEnumerableEx helpers.

csharp
// Convert IAsyncEnumerable → IObservable
IAsyncEnumerable<int> asyncSeq = GetDataAsync();

IObservable<int> observable = Observable.Create<int>(async (observer, ct) =>
{
    try
    {
        await foreach (var item in asyncSeq.WithCancellation(ct))
            observer.OnNext(item);
        observer.OnCompleted();
    }
    catch (Exception ex) { observer.OnError(ex); }
});

// Now use full Rx operators
observable
    .Where(n => n % 2 == 0)
    .Buffer(TimeSpan.FromSeconds(1))
    .Subscribe(batch => Console.WriteLine($"Batch: {batch.Count}"));

// Convert back: IObservable → IAsyncEnumerable (System.Reactive 5+)
IAsyncEnumerable<int> back = observable.ToAsyncEnumerable();
Performance & Best Practices
N+1 detection, multiple enumeration, indexed lookups, and profiling LINQ pipelines

N+1 occurs when you enumerate a list then access a navigation property per item, firing one query per element. Fix with Include (eager load) or projection.

csharp
// ❌ N+1: 1 query for orders + N queries for each customer
var orders = await ctx.Orders.ToListAsync(); // 1 SQL
foreach (var o in orders)
    Console.WriteLine(o.Customer.Name); // N SQL — lazy load per item

// ✅ Fix A: eager load with Include
var orders = await ctx.Orders
    .Include(o => o.Customer)
    .ToListAsync();        // 1 SQL with JOIN

// ✅ Fix B: project only what you need (no navigation required)
var orders = await ctx.Orders
    .Select(o => new { o.Id, CustomerName = o.Customer.Name })
    .ToListAsync();        // 1 SQL, no extra round-trips

// Detect: enable EF Core warning (throws on N+1 patterns)
optionsBuilder.ConfigureWarnings(w =>
    w.Throw(RelationalEventId.MultipleCollectionIncludeWarning));

Enumerating an IEnumerable<T> more than once re-runs the entire pipeline (including database queries or HTTP calls). Materializing once with ToList() / ToArray() prevents this.

csharp
// ❌ Enumerates twice — DB query runs twice
IEnumerable<Order> orders = ctx.Orders.Where(o => o.IsOpen);
int count = orders.Count();          // first DB round-trip
var list  = orders.ToList();         // second DB round-trip!

// ✅ Materialise once
var orders = ctx.Orders.Where(o => o.IsOpen).ToList(); // single query
int count  = orders.Count;           // in-memory, instant
var first  = orders.First();

// Roslyn analyzer: install Microsoft.EntityFrameworkCore.Analyzers
// or use ReSharper — both flag IEnumerable multiple enumeration

// Pattern: take IReadOnlyList<T> not IEnumerable<T> in methods
void Process(IReadOnlyList<Order> orders) // caller must materialise

LINQ has delegate-call overhead per element and allocates enumerator objects. For tight inner loops over large arrays on a hot path, a for loop can be 2-5x faster and allocation-free.

csharp
int[] data = Enumerable.Range(0, 1_000_000).ToArray();

// LINQ: readable, but allocates enumerator + delegate per call
long sumLinq = data.Where(n => n % 2 == 0).Sum();

// for loop: zero allocations, vectorisable by JIT
long sumFor = 0;
for (int i = 0; i < data.Length; i++)
    if (data[i] % 2 == 0) sumFor += data[i];

// Span + for: cache-friendly, bounds-check eliminated
Span<int> span = data.AsSpan();
long sumSpan = 0;
for (int i = 0; i < span.Length; i++)
    if (span[i] % 2 == 0) sumSpan += span[i];

// Use BenchmarkDotNet to measure; don't optimise without data

Calling list.Contains(x) inside a Where is O(n) per item → O(n²) total. Build a HashSet or Dictionary once for O(1) lookups, reducing total complexity to O(n).

csharp
var allowedIds = new List<int> { 1, 2, 3, /* ... thousands */ };

// ❌ O(n²): allowedIds.Contains is O(n) per element in users
var filtered = users.Where(u => allowedIds.Contains(u.Id)).ToList();

// ✅ O(n): HashSet.Contains is O(1)
var allowedSet = allowedIds.ToHashSet();
var filtered   = users.Where(u => allowedSet.Contains(u.Id)).ToList();

// ✅ Dictionary lookup for enrichment
var productMap = products.ToDictionary(p => p.Id);
var enriched = orderLines
    .Where(ol => productMap.ContainsKey(ol.ProductId))
    .Select(ol => new
    {
        ol.Qty,
        Product = productMap[ol.ProductId]
    })
    .ToList();

Use EF Core's built-in logging, MiniProfiler, or a database profiler like SQL Server Profiler / pgBadger to capture generated SQL and execution times.

csharp
// Log all SQL with parameters to console
services.AddDbContext<AppDbContext>(opt =>
    opt.UseSqlServer(conn)
       .LogTo(Console.WriteLine, LogLevel.Information)
       .EnableSensitiveDataLogging());  // shows parameter values

// Inspect generated SQL without executing
var query = ctx.Orders.Where(o => o.Total > 100).OrderBy(o => o.Id);
string sql = query.ToQueryString(); // EF Core 5+

// Slow query warning threshold
opt.ConfigureWarnings(w =>
    w.Throw(CoreEventId.QueryPossiblyUnintendedUseOfEqualsWarning));

// MiniProfiler (web apps) — shows SQL per request in dev toolbar
// dotnet add package MiniProfiler.EntityFrameworkCore
// services.AddMiniProfiler().AddEntityFramework();

Lambdas capture the variable, not its value. In a loop, all lambdas share the same variable and see its final value. Capture a local copy per iteration to freeze the current value.

csharp
// ❌ Bug: all lambdas capture the same 'i' variable
var funcs = new List<Func<int>>();
for (int i = 0; i < 5; i++)
    funcs.Add(() => i);               // captures reference to i
funcs.ForEach(f => Console.Write(f())); // prints "55555" not "01234"

// ✅ Fix A: capture a local copy
for (int i = 0; i < 5; i++)
{
    int captured = i;
    funcs.Add(() => captured);        // each lambda gets its own copy
}
funcs.ForEach(f => Console.Write(f())); // prints "01234"

// ✅ Fix B: foreach — loop variable is per-iteration in C# 5+
foreach (var item in items)
    tasks.Add(Task.Run(() => Process(item))); // safe in modern C#

// LINQ predicate example — same fix
var predicates = statuses.Select(s => { var c = s; return (Func<Order, bool>)(o => o.Status == c); });

Lazy loading fires a separate query per navigation access; eager loading (Include) fetches everything in one query. Choose based on whether navigation data is always needed.

csharp
// Enable lazy loading (requires proxy package + virtual navigations)
// dotnet add package Microsoft.EntityFrameworkCore.Proxies
opt.UseLazyLoadingProxies().UseSqlServer(conn);

public class Order
{
    public virtual Customer Customer { get; set; } = null!; // virtual = proxy
}

// Access Customer fires a DB query if not already loaded
var name = order.Customer.Name; // SELECT ... WHERE CustomerId = @id

// Eager loading (explicit) — one JOIN, always loaded
var orders = await ctx.Orders.Include(o => o.Customer).ToListAsync();

// Explicit loading (manual, on demand)
await ctx.Entry(order).Reference(o => o.Customer).LoadAsync();
await ctx.Entry(customer).Collection(c => c.Orders).LoadAsync();

AsQueryable() keeps the query on the DB side (server-side filtering). AsEnumerable() switches to LINQ to Objects — subsequent operators run in memory on already-fetched data.

csharp
// AsQueryable: all operators translate to SQL
var q = ctx.Products.AsQueryable();
q = q.Where(p => p.Price < 50); // SQL WHERE clause
var list = q.ToList();           // one SQL query with filter

// AsEnumerable: SWITCH — loads ALL rows then filters in .NET
var all = ctx.Products
    .AsEnumerable()              // fetches everything first!
    .Where(p => p.Price < 50)   // C# filter on loaded data
    .ToList();

// Intentional use of AsEnumerable: use a C# method EF can't translate
var result = ctx.Products
    .AsEnumerable()
    .Where(p => MyCustomFilter(p)) // EF can't translate this
    .ToList();

// ⚠️ Never call AsEnumerable before Where on large tables
Real-World Patterns & Architecture
Repository pattern, CQRS, generic pagination, unit testing LINQ, and collection diffing

Returning IQueryable<T> from a repository lets callers compose additional LINQ operators that translate to SQL, avoiding the need to define every possible filter combination in the repository.

csharp
public interface IRepository<T> where T : class
{
    IQueryable<T> Query();
    Task<T?>      GetByIdAsync(int id, CancellationToken ct = default);
    void           Add(T entity);
    Task           SaveAsync(CancellationToken ct = default);
}

public class EfRepository<T> : IRepository<T> where T : class
{
    private readonly AppDbContext _ctx;
    public EfRepository(AppDbContext ctx) => _ctx = ctx;

    public IQueryable<T>  Query()          => _ctx.Set<T>().AsNoTracking();
    public Task<T?>       GetByIdAsync(int id, CancellationToken ct)
        => _ctx.Set<T>().FindAsync(new object[] { id }, ct).AsTask();
    public void            Add(T entity)   => _ctx.Set<T>().Add(entity);
    public Task            SaveAsync(CancellationToken ct)
        => _ctx.SaveChangesAsync(ct);
}

// Caller composes freely — still translates to SQL
var result = await repo.Query()
    .Where(p => p.Category == "Books")
    .OrderBy(p => p.Title)
    .Take(10)
    .ToListAsync();

A generic PagedQuery<T> record holding sort field, direction, filter, and page parameters can be applied to any IQueryable<T> through extension methods that build expressions dynamically.

csharp
public record PagedQuery(
    string? SortBy, bool Ascending, string? Search, int Page, int PageSize);

public record PagedResult<T>(List<T> Items, int Total, int Page, int Pages);

public static class QueryExtensions
{
    public static async Task<PagedResult<T>> ToPagedAsync<T>(
        this IQueryable<T> query, PagedQuery q, CancellationToken ct = default)
    {
        if (q.SortBy is not null)
            query = query.OrderByField(q.SortBy, q.Ascending);

        int total = await query.CountAsync(ct);
        var items = await query
            .Skip((q.Page - 1) * q.PageSize)
            .Take(q.PageSize)
            .ToListAsync(ct);

        return new PagedResult<T>(items, total, q.Page,
            (int)Math.Ceiling(total / (double)q.PageSize));
    }
}

// Usage
var paged = await ctx.Products.AsQueryable()
    .ToPagedAsync(new PagedQuery("Price", false, null, 1, 20));

Project with Select before ToListAsync so EF Core emits a minimal SELECT for only the DTO fields. Use AutoMapper's ProjectTo<TDto>(config) to avoid manual projection boilerplate.

csharp
// Manual projection inside query
var dtos = await ctx.Orders
    .Where(o => o.CustomerId == customerId)
    .Select(o => new OrderDto
    {
        Id           = o.Id,
        Total        = o.Total,
        CustomerName = o.Customer.Name, // JOIN generated by EF
        ItemCount    = o.Items.Count()  // COUNT() in SQL
    })
    .ToListAsync();

// AutoMapper ProjectTo (no intermediate entity loading)
// dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection
var config = new MapperConfiguration(cfg => cfg.CreateMap<Order, OrderDto>());
var dtos2 = await ctx.Orders
    .ProjectTo<OrderDto>(config)
    .ToListAsync();

Set operations (Except, Intersect) on key projections compute the diff efficiently. Pair with ToDictionary lookups to retrieve the full objects.

csharp
var oldItems = new[] { new Tag(1,"A"), new Tag(2,"B"), new Tag(3,"C") };
var newItems = new[] { new Tag(2,"B"), new Tag(3,"X"), new Tag(4,"D") };

var oldMap = oldItems.ToDictionary(t => t.Id);
var newMap = newItems.ToDictionary(t => t.Id);

var oldIds = oldMap.Keys.ToHashSet();
var newIds = newMap.Keys.ToHashSet();

var added   = newIds.Except(oldIds).Select(id => newMap[id]).ToList();
var removed = oldIds.Except(newIds).Select(id => oldMap[id]).ToList();
var changed = oldIds.Intersect(newIds)
    .Where(id => oldMap[id].Name != newMap[id].Name)
    .Select(id => (Old: oldMap[id], New: newMap[id]))
    .ToList();

Console.WriteLine($"Added: {added.Count}, Removed: {removed.Count}, Changed: {changed.Count}");

Flatten a hierarchy with a recursive iterator, then use GroupBy / ToLookup to rebuild the tree structure in memory — since LINQ has no native recursive operator.

csharp
record Category(int Id, int? ParentId, string Name, List<Category> Children = null!);

// Flatten tree → list (BFS)
IEnumerable<Category> Flatten(IEnumerable<Category> nodes)
{
    foreach (var n in nodes)
    {
        yield return n;
        if (n.Children is { Count: > 0 })
            foreach (var child in Flatten(n.Children))
                yield return child;
}
}

// Rebuild tree from flat DB rows using Lookup
List<Category> BuildTree(List<Category> flat)
{
    var lookup = flat.ToLookup(c => c.ParentId);
    Category Attach(Category c) => c with
    {
        Children = lookup[c.Id].Select(Attach).ToList()
    };
    return lookup[null].Select(Attach).ToList(); // roots
}

Use EF Core's in-memory provider or MockQueryable to create testable IQueryable<T> sources. Prefer in-memory SQLite for more realistic SQL translation testing.

csharp
// EF Core In-Memory (no SQL translation — good for logic tests)
var options = new DbContextOptionsBuilder<AppDbContext>()
    .UseInMemoryDatabase("TestDb")
    .Options;

using var ctx = new AppDbContext(options);
ctx.Products.AddRange(new Product { Name="A", Price=10 },
                      new Product { Name="B", Price=200 });
await ctx.SaveChangesAsync();

var cheap = await ctx.Products.Where(p => p.Price < 50).ToListAsync();
Assert.Single(cheap);

// SQLite in-memory (tests SQL translation)
var options2 = new DbContextOptionsBuilder<AppDbContext>()
    .UseSqlite("DataSource=:memory:")
    .Options;

// MockQueryable (test async methods without EF)
// dotnet add package MockQueryable.Moq
var mockData = new List<Product> { new() { Name="Test" } }.AsQueryable();
var mockSet  = mockData.BuildMockDbSet();

In CQRS, queries are side-effect-free. A MediatR query handler wraps a LINQ projection, applies paging, and returns a DTO — keeping query logic isolated and testable.

csharp
// Query definition
public record GetProductsQuery(string? Category, int Page, int PageSize)
    : IRequest<PagedResult<ProductDto>>;

// Handler
public class GetProductsHandler : IRequestHandler<GetProductsQuery, PagedResult<ProductDto>>
{
    private readonly AppDbContext _ctx;
    public GetProductsHandler(AppDbContext ctx) => _ctx = ctx;

    public async Task<PagedResult<ProductDto>> Handle(
        GetProductsQuery q, CancellationToken ct)
    {
        var query = _ctx.Products.AsQueryable();
        if (q.Category is not null)
            query = query.Where(p => p.Category == q.Category);

        int total = await query.CountAsync(ct);
        var items = await query
            .OrderBy(p => p.Name)
            .Skip((q.Page - 1) * q.PageSize)
            .Take(q.PageSize)
            .Select(p => new ProductDto(p.Id, p.Name, p.Price))
            .ToListAsync(ct);

        return new PagedResult<ProductDto>(items, total, q.Page,
            (int)Math.Ceiling(total / (double)q.PageSize));
    }
}

Accumulate validation errors via Aggregate or SelectMany over a list of validation rules, avoiding multiple passes and collecting all failures at once.

csharp
record ValidationRule<T>(Func<T, bool> IsValid, string Error);

List<ValidationRule<Order>> rules = new()
{
    new(o => o.Total > 0,           "Total must be positive"),
    new(o => o.Items.Any(),         "Order must have items"),
    new(o => o.CustomerId != 0,     "Customer is required"),
    new(o => o.Items.All(i => i.Qty > 0), "All quantities must be positive"),
};

// Single pass — collect all failures
List<string> errors = rules
    .Where(r => !r.IsValid(order))
    .Select(r => r.Error)
    .ToList();

if (errors.Any())
    throw new ValidationException(errors);

// Cross-item validation: duplicate SKUs in an import batch
var duplicateSKUs = items
    .GroupBy(i => i.SKU)
    .Where(g => g.Count() > 1)
    .Select(g => $"Duplicate SKU: {g.Key}")
    .ToList();

Use EF.Functions.Contains for SQL Server CONTAINS full-text search or FreeText for semantic matching. Both translate to SQL Server's full-text index operations.

csharp
// Requires SQL Server full-text index on the column
// CREATE FULLTEXT INDEX ON Products(Description) KEY INDEX PK_Products;

// CONTAINS — exact word/phrase/prefix matching
var results = await ctx.Products
    .Where(p => EF.Functions.Contains(p.Description, "\"machine learning\""))
    .ToListAsync();

// FREETEXT — semantic/inflectional matching ("run" matches "running", "ran")
var semantic = await ctx.Products
    .Where(p => EF.Functions.FreeText(p.Description, "machine learning algorithms"))
    .ToListAsync();

// LIKE fallback (no full-text index needed, slower)
string term = "machine";
var like = await ctx.Products
    .Where(p => EF.Functions.Like(p.Description, $"%{term}%"))
    .ToListAsync();

// PostgreSQL: use EF.Functions.ToTsVector / ToTsQuery via Npgsql

Wrap the LINQ materialisation step inside a Polly retry policy. The policy re-executes the entire async delegate on transient failures, effectively re-running the LINQ-to-SQL query.

csharp
// dotnet add package Polly

var retryPolicy = Policy
    .Handle<SqlException>(e => e.IsTransient)
    .Or<TimeoutException>()
    .WaitAndRetryAsync(3,
        attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)),
        (ex, delay, attempt, _) =>
            logger.LogWarning("Retry {Attempt} after {Delay}s", attempt, delay.TotalSeconds));

// Wrap LINQ materialisation in the policy
var orders = await retryPolicy.ExecuteAsync(async ct =>
    await ctx.Orders
        .Where(o => o.IsOpen)
        .OrderBy(o => o.CreatedAt)
        .ToListAsync(ct),
    cancellationToken);

// EF Core execution strategy (built-in retry for SQL Server transients)
opt.UseSqlServer(conn, b => b.EnableRetryOnFailure(
    maxRetryCount: 3,
    maxRetryDelay: TimeSpan.FromSeconds(10),
    errorNumbersToAdd: null));

ASP.NET Core OData translates $filter, $orderby, $top, $skip, and $select URL parameters into LINQ expressions applied to your IQueryable<T>.

csharp
// dotnet add package Microsoft.AspNetCore.OData

// Program.cs
builder.Services.AddControllers()
    .AddOData(opt => opt
        .Select().Filter().OrderBy().Count().SetMaxTop(100)
        .AddRouteComponents("odata", GetEdmModel()));

// Controller
[ApiController, Route("odata/[controller]")]
public class ProductsController : ODataController
{
    private readonly AppDbContext _ctx;
    public ProductsController(AppDbContext ctx) => _ctx = ctx;

    [HttpGet, EnableQuery]  // ← magic attribute
    public IQueryable<Product> Get() => _ctx.Products.AsNoTracking();
}

// Client queries translated to SQL automatically:
// GET /odata/Products?$filter=Price lt 50&$orderby=Name&$top=10
// → SELECT TOP 10 * FROM Products WHERE Price < 50 ORDER BY Name

A production data layer layers specification pattern (reusable predicates), compiled queries (hot paths), AsNoTracking (reads), projection DTOs (minimal columns), cursor pagination (scale), and compiled queries for throughput-critical operations.

csharp
// Compiled query for hot-path lookup
private static readonly Func<AppDbContext, int, Task<ProductDto?>> GetById =
    EF.CompileAsyncQuery((AppDbContext ctx, int id) =>
        ctx.Products
           .Where(p => p.Id == id && p.IsActive)
           .Select(p => new ProductDto(p.Id, p.Name, p.Price))
           .FirstOrDefault());

// Generic search: specification + projection + cursor pagination
public async Task<CursorPage<ProductDto>> SearchAsync(
    Specification<Product> spec, int? afterId, int pageSize, CancellationToken ct)
{
    var query = _ctx.Products
        .AsNoTracking()
        .Where(spec.ToExpression());

    if (afterId.HasValue)
        query = query.Where(p => p.Id > afterId.Value);

    var items = await query
        .OrderBy(p => p.Id)
        .Take(pageSize + 1)
        .Select(p => new ProductDto(p.Id, p.Name, p.Price))
        .ToListAsync(ct);

    bool hasNext = items.Count > pageSize;
    return new CursorPage<ProductDto>(
        Items:      items.Take(pageSize).ToList(),
        NextCursor: hasNext ? items[pageSize - 1].Id : null);
}
Prev
Next
Drag
Map
HTML Snippets Powered By : XYZScripts.com
Select the fields to be shown. Others will be hidden. Drag and drop to rearrange the order.
  • Image
  • SKU
  • Rating
  • Price
  • Stock
  • Availability
  • Add to cart
  • Description
  • Content
  • Weight
  • Dimensions
  • Additional information
Click outside to hide the comparison bar
Compare