LINQ Fundamentals

Master Language Integrated Query (LINQ) to write expressive, efficient queries over collections and data sources in C#

C#
LINQ
Data Queries
Functional Programming
20 min read

Overview

LINQ revolutionized C# development by bringing functional programming concepts to .NET. Understanding LINQ deeply is essential for writing clean, efficient code and is a frequent interview topic for mid-level and above positions.

Core Concepts

Query Syntax vs Method Syntax

var products = GetProducts();

// Query syntax - more readable for complex queries
var queryResult = from p in products
                  where p.Price > 50
                  orderby p.Name
                  select new { p.Name, p.Price };

// Method syntax - more flexible and composable
var methodResult = products
    .Where(p => p.Price > 50)
    .OrderBy(p => p.Name)
    .Select(p => new { p.Name, p.Price });

Deferred vs Immediate Execution

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

// Deferred execution - query stored, not executed
var query = numbers.Where(n => n > 1);
numbers.Add(4);  // Affects query results!

foreach (var n in query)  // Executes here
    Console.WriteLine(n);  // Output: 2, 3, 4

// Immediate execution with .ToList(), .ToArray(), .Count()
var list = numbers.Where(n => n > 1).ToList();  // Executes immediately
numbers.Add(5);  // Doesn't affect list

Common LINQ Operations

var products = GetProducts();

// Filtering
var expensive = products.Where(p => p.Price > 100);

// Projection
var names = products.Select(p => p.Name);
var summary = products.Select(p => new { p.Name, Tax = p.Price * 0.1m });

// Ordering
var sorted = products.OrderBy(p => p.Price).ThenBy(p => p.Name);

// Grouping
var byCategory = products.GroupBy(p => p.Category);
foreach (var group in byCategory)
{
    Console.WriteLine($"Category: {group.Key}");
    foreach (var product in group)
        Console.WriteLine($"  {product.Name}");
}

// Aggregation
var total = products.Sum(p => p.Price);
var average = products.Average(p => p.Price);
var maxPrice = products.Max(p => p.Price);

Joining Data

var customers = GetCustomers();
var orders = GetOrders();

// Inner join
var customerOrders = from c in customers
                     join o in orders on c.Id equals o.CustomerId
                     select new { c.Name, o.OrderDate, o.Total };

// Left join (group join)
var customersWithOrders = from c in customers
                          join o in orders on c.Id equals o.CustomerId into orderGroup
                          select new { Customer = c, Orders = orderGroup };

When to Use vs. When to Avoid

| Scenario | Use It? | Why | |----------|---------|-----| | Filtering/sorting in-memory collections | ✅ Yes | Clean, readable syntax | | Database queries with EF Core | ✅ Yes | Translates to efficient SQL | | Simple one-time filter | ⚠️ Depends | Loop may be clearer for trivial cases | | Performance-critical tight loops | ❌ No | Slight overhead vs hand-written loops | | Operations not supported by IQueryable | ❌ No | Forces client-side evaluation |

Common Patterns

Filter-Transform-Sort Pattern

var result = products
    .Where(p => p.IsActive && p.Price > 10)
    .OrderBy(p => p.Name)
    .Select(p => new { p.Name, p.Price });

GroupBy Pattern

var byCategory = products
    .GroupBy(p => p.Category)
    .Select(g => new { Category = g.Key, Count = g.Count(), Total = g.Sum(p => p.Price) });

Common Mistakes

Mistake: Multiple Enumerations

// ❌ Bad - enumerates collection 3 times
var items = GetExpensiveItems();
var count = items.Count();
var sum = items.Sum(x => x.Price);
var avg = items.Average(x => x.Price);

// ✅ Better - enumerates once
var items = GetExpensiveItems().ToList();  // Materialize once
var count = items.Count;
var sum = items.Sum(x => x.Price);
var avg = items.Average(x => x.Price);

Why: Each LINQ method call on IEnumerable re-enumerates the source. For database queries, this means multiple round-trips.

Mistake: Filtering After Select

// ❌ Bad - projects all, then filters strings
var result = products
    .Where(p => p.IsActive)
    .Select(p => p.Name)
    .Where(name => name.StartsWith("A"));

// ✅ Better - filter once before projection
var result = products
    .Where(p => p.IsActive && p.Name.StartsWith("A"))
    .Select(p => p.Name);

Why: Filter early to reduce the data being processed in subsequent operations.

Practical Example

Scenario: Build a product search with filtering, sorting, and paging.

public List<ProductDto> SearchProducts(
    string? searchTerm,
    decimal? minPrice,
    string? sortBy,
    int page,
    int pageSize)
{
    var query = products.AsQueryable();

    // Apply filters conditionally
    if (!string.IsNullOrWhiteSpace(searchTerm))
        query = query.Where(p => p.Name.Contains(searchTerm));

    if (minPrice.HasValue)
        query = query.Where(p => p.Price >= minPrice.Value);

    // Apply sorting
    query = sortBy?.ToLower() switch
    {
        "price" => query.OrderBy(p => p.Price),
        "name" => query.OrderBy(p => p.Name),
        _ => query.OrderBy(p => p.Id)
    };

    // Apply paging and project
    return query
        .Skip((page - 1) * pageSize)
        .Take(pageSize)
        .Select(p => new ProductDto { Name = p.Name, Price = p.Price })
        .ToList();
}

Interview Tips

Tip 1: Always mention deferred execution when discussing LINQ. It's a common "gotcha" question.

Tip 2: Know when to use .ToList() or .ToArray() to materialize results, especially before multiple enumerations.

Tip 3: Understand that LINQ to Entities (EF Core) translates to SQL - some operations force client-side evaluation.

Common Interview Questions

  1. What's the difference between IEnumerable and IQueryable?

    • IEnumerable<T>: In-memory queries, client-side execution. IQueryable<T>: Expression tree-based, allows provider (like EF) to translate to SQL.
  2. Explain deferred execution in LINQ.

    • Query doesn't execute when defined, only when enumerated (foreach, ToList, Count, etc.). Allows query composition but can cause unexpected behavior if source changes.
  3. What's the difference between Select and SelectMany?

    • Select: One-to-one projection. SelectMany: Flattens nested collections (one-to-many), like SQL JOIN.
  4. When should you use ToList() or ToArray()?

    • When you need to enumerate multiple times, when you want a snapshot of current data, or when you need indexed access.
  5. How does LINQ improve code readability?

    • Declarative syntax (what, not how), chainable operations, less boilerplate than loops, compile-time checking.
  6. What are the performance implications of LINQ?

    • Slight overhead vs hand-written loops, but negligible in most cases. Real issues: multiple enumerations, inefficient queries, client-side evaluation in EF.
  7. Can you modify a collection while enumerating it with LINQ?

    • No, throws InvalidOperationException. Must materialize first (.ToList()) or use different approach.

Related Topics

  • Entity Framework Core: Uses LINQ with IQueryable for database queries
  • C# Collections: Understanding List, Array, Dictionary enhances LINQ usage
  • Lambda Expressions: Foundation for LINQ method syntax
  • Async/Await: Combine with LINQ using ToListAsync, FirstOrDefaultAsync