LINQ and Collections

LINQ query and method syntax, deferred execution, key operators, and the trade-offs between core .NET collection types.

LINQ (Language Integrated Query) is a powerful feature in C# that provides a unified syntax for querying different data sources including collections, databases, XML, and more.

Advantages:

  • Unified Syntax: Single query syntax works across multiple data sources (objects, databases, XML, etc.)
  • Type Safety: Compile-time checking prevents runtime errors
  • IntelliSense Support: IDE provides autocomplete and suggestions
  • Readable Code: Declarative syntax is more intuitive than traditional loops
  • Less Code: Reduces boilerplate code significantly
  • Strongly Typed: Leverages C#'s type system for safety
  • Extensible: Can create custom LINQ operators
  • Deferred Execution: Queries execute only when enumerated, improving performance
// Traditional approach
List evenNumbers = new List();
foreach (int num in numbers)
{
    if (num % 2 == 0)
        evenNumbers.Add(num);
}

// LINQ approach
var evenNumbers = numbers.Where(n => n % 2 == 0);

Query Syntax (Comprehension Syntax):

  • SQL-like syntax using keywords like from, where, select
  • More readable for complex queries with multiple operations
  • Limited to common operations

Method Syntax (Fluent Syntax):

  • Uses extension methods with lambda expressions
  • More flexible and supports all LINQ operators
  • Better for simple operations and chaining
// Query Syntax
var queryResult = from student in students
                  where student.Age > 18
                  orderby student.Name
                  select student.Name;

// Method Syntax
var methodResult = students
    .Where(s => s.Age > 18)
    .OrderBy(s => s.Name)
    .Select(s => s.Name);

// Both produce identical results
// Query syntax is converted to method syntax by the compiler

Key Differences:

  • Query syntax requires from and select/group clauses
  • Method syntax can access all LINQ operators
  • You can mix both syntaxes in a single query
  • Method syntax is more commonly used in practice

These methods retrieve elements from a collection but differ in their expectations and error handling:

First()

  • Returns the first element
  • Throws InvalidOperationException if sequence is empty
  • Use when you expect at least one element

FirstOrDefault()

  • Returns the first element or default value (null for reference types, 0 for numbers)
  • Never throws exception for empty sequences
  • Use when the sequence might be empty

Single()

  • Returns the only element
  • Throws if sequence is empty OR has more than one element
  • Use when you expect exactly one element

SingleOrDefault()

  • Returns the only element or default value
  • Throws if sequence has more than one element (but not if empty)
  • Use when you expect zero or one element
var numbers = new List { 1, 2, 3, 4, 5 };

// First() - returns 1
var first = numbers.First();

// FirstOrDefault() - returns 1, or 0 if empty
var firstOrDefault = numbers.FirstOrDefault();

// Single() - throws exception (more than one element)
var single = numbers.Single(); // InvalidOperationException!

// With predicate
var firstEven = numbers.First(n => n % 2 == 0); // returns 2
var singleFive = numbers.Single(n => n == 5); // returns 5

// Empty sequence
var empty = new List();
// empty.First(); // throws InvalidOperationException
var result = empty.FirstOrDefault(); // returns 0

Related Resources

Deferred Execution means LINQ queries are not executed when they are defined, but only when the results are actually enumerated.

How it works:

  • Query definition creates an execution plan
  • Actual execution happens when you iterate (foreach, ToList(), Count(), etc.)
  • Query is re-executed each time you enumerate it
  • Benefits: improved performance, fresh data on each execution
var numbers = new List { 1, 2, 3, 4, 5 };

// Query is DEFINED but NOT executed
var query = numbers.Where(n => n > 2);
Console.WriteLine("Query defined");

// NOW the query executes (deferred execution)
foreach (var num in query)
{
    Console.WriteLine(num); // Outputs: 3, 4, 5
}

// Modifying source affects query results
numbers.Add(6);
numbers.Add(7);

// Query executes AGAIN with new data
foreach (var num in query)
{
    Console.WriteLine(num); // Outputs: 3, 4, 5, 6, 7
}

// Force immediate execution
var immediateResult = numbers.Where(n => n > 2).ToList();
numbers.Add(8); // This won't affect immediateResult

Operations with Immediate Execution:

  • ToList(), ToArray(), ToDictionary()
  • Count(), Sum(), Average(), Max(), Min()
  • First(), Single(), Last()
  • Any(), All()

Operations with Deferred Execution:

  • Where(), Select(), OrderBy()
  • Skip(), Take(), GroupBy()
  • Join(), SelectMany()

Select()

  • Projects each element into a new form (1-to-1 transformation)
  • Returns IEnumerable<T>
  • Maintains the structure of the collection

SelectMany()

  • Flattens nested collections into a single sequence (1-to-many transformation)
  • Returns flattened IEnumerable<T>
  • Useful for hierarchical or nested data
// Sample data
var schools = new List
{
    new School 
    { 
        Name = "Lincoln High", 
        Students = new[] { "Alice", "Bob" } 
    },
    new School 
    { 
        Name = "Jefferson High", 
        Students = new[] { "Charlie", "Diana" } 
    }
};

// Select() - Returns IEnumerable
var selectResult = schools.Select(s => s.Students);
// Result: [ ["Alice", "Bob"], ["Charlie", "Diana"] ]
// You get a collection of collections

// SelectMany() - Returns IEnumerable
var selectManyResult = schools.SelectMany(s => s.Students);
// Result: ["Alice", "Bob", "Charlie", "Diana"]
// You get a flattened single collection

// Another example
var numbers = new List<List>
{
    new List { 1, 2, 3 },
    new List { 4, 5, 6 },
    new List { 7, 8, 9 }
};

// Select - nested structure preserved
var selected = numbers.Select(list => list);
// Type: IEnumerable<List>

// SelectMany - flattened
var flattened = numbers.SelectMany(list => list);
// Type: IEnumerable
// Result: [1, 2, 3, 4, 5, 6, 7, 8, 9]

// Practical example: Get all words from sentences
var sentences = new[] { "Hello world", "LINQ is powerful" };
var allWords = sentences.SelectMany(s => s.Split(' '));
// Result: ["Hello", "world", "LINQ", "is", "powerful"]

Related Resources