C# Introduction to LINQ

LINQ, short for Language Integrated Query, is C# syntax and library support for asking questions about data. It lets you filter, transform, sort, group, and search arrays, lists, dictionaries, and many other data sources with one consistent style. LINQ matters because real programs rarely just store data; they need to find the right data and shape it into useful results.

Overview: How LINQ Works

LINQ is not one single method. It is a set of features that work together: extension methods such as Where and Select, lambda expressions such as n => n > 10, interfaces such as IEnumerable<T>, and optional query syntax using keywords such as from, where, and select. This lesson focuses on method syntax because it is compact, common, and maps directly to the LINQ methods you will see in documentation.

Most beginner LINQ queries run on IEnumerable<T>. An array, a List<T>, and many collection types can be treated as an enumerable sequence: something that can produce one item at a time. A LINQ query usually does not copy all the data immediately. Instead, many operators build a small query object that remembers what should happen later. The actual work happens when the query is enumerated, such as by a foreach loop, ToList(), Count(), First(), or string.Join.

This delayed behavior is called deferred execution. It is powerful because LINQ can avoid work until the result is needed, and chained operations can stream values through the pipeline one at a time. It is also a common source of surprises: if the source collection changes before the query is enumerated, the query may see the changed data.

Internally, operators such as Where and Select return iterator objects. These objects store the source sequence and the delegate created from your lambda expression. When enumeration begins, the iterator asks the source for items, tests or transforms each item, and yields matching results. The CLR does not create a new language runtime just for LINQ; it executes ordinary generic methods, delegates, and enumerators.

Syntax

int[] numbers = { 1, 2, 3, 4, 5 };
IEnumerable<int> query = numbers
    .Where(n => n > 2)
    .Select(n => n * 10);
List<int> results = query.ToList();
Part Meaning
numbers The source sequence. It can be an array, list, or another enumerable collection.
Where Filters items. The lambda returns true to keep an item and false to skip it.
Select Projects each item into another value, possibly of a different type.
n => n > 2 A lambda expression. Read it as: for each n, check whether n is greater than 2.
ToList() Materializes the query, forcing it to run now and storing the result in a new list.

To use LINQ method syntax in a complete program, include the System.Linq namespace. Many modern project templates include common namespaces automatically, but explicit using directives make examples clear and portable.

Examples

Example 1: Filter, Transform, And Sort

using System;
using System.Linq;

class Program
{
    static void Main()
    {
        string[] names = { "Mina", "Ava", "Noah", "Ivy", "Omar" };

        var result = names
            .Where(name => name.Length == 4)
            .OrderBy(name => name)
            .Select(name => name.ToUpper());

        foreach (string name in result)
        {
            Console.WriteLine(name);
        }
    }
}

Output:

MINA
NOAH
OMAR

The query starts with an array of names. Where keeps only four-letter names, OrderBy sorts those names alphabetically, and Select converts each remaining name to uppercase. The variable uses var because the exact LINQ iterator type is not important; what matters is that it can be enumerated to produce strings.

Example 2: Deferred Execution

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        List<int> numbers = new List<int> { 1, 2, 3, 4 };
        var evens = numbers.Where(n => n % 2 == 0);

        numbers.Add(6);

        Console.WriteLine("Deferred query:");
        foreach (int number in evens)
        {
            Console.WriteLine(number);
        }

        List<int> snapshot = evens.ToList();
        numbers.Add(8);

        Console.WriteLine($"Snapshot count: {snapshot.Count}");
        Console.WriteLine($"Deferred count: {evens.Count()}");
    }
}

Output:

Deferred query:
2
4
6
Snapshot count: 3
Deferred count: 4

The query evens is defined before 6 is added, but it is not executed until the foreach loop starts. Because the source list contains 6 by then, the query includes it. ToList() creates a snapshot at that moment. When 8 is added later, the snapshot still has three items, while the deferred query sees four even numbers.

Example 3: Group And Summarize Objects

using System;
using System.Collections.Generic;
using System.Linq;

record InventoryItem(string Name, string Category, int Count, decimal Price);

class Program
{
    static void Main()
    {
        var inventory = new List<InventoryItem>
        {
            new InventoryItem("Bread", "Bakery", 4, 3.25m),
            new InventoryItem("Muffin", "Bakery", 8, 2.00m),
            new InventoryItem("Apples", "Fruit", 10, 1.20m),
            new InventoryItem("Pears", "Fruit", 6, 1.50m),
            new InventoryItem("Rice", "Pantry", 5, 4.40m)
        };

        var summaries = inventory
            .GroupBy(item => item.Category)
            .Select(group => new
            {
                Category = group.Key,
                Items = group.Sum(item => item.Count),
                Value = group.Sum(item => item.Count * item.Price)
            })
            .OrderByDescending(summary => summary.Value);

        foreach (var summary in summaries)
        {
            Console.WriteLine($"{summary.Category}: {summary.Items} items, {summary.Value:F2} value");
        }
    }
}

Output:

Bakery: 12 items, 29.00 value
Pantry: 5 items, 22.00 value
Fruit: 16 items, 21.00 value

This example uses LINQ for a more realistic reporting task. GroupBy collects inventory items by category. Each group has a Key, which is the category name, and the grouped items. Sum calculates totals inside each group. The anonymous object created in Select is useful when you need a temporary result shape without declaring a separate class.

How It Works Step By Step

  1. The compiler resolves LINQ method calls as extension methods from System.Linq.Enumerable.
  2. Each lambda expression is compiled into a delegate or a reusable method-like target that LINQ can call for each item.
  3. Deferred operators such as Where, Select, OrderBy, and GroupBy return query objects instead of immediately printing or changing the source.
  4. When enumeration begins, the query requests an enumerator from the source collection.
  5. Filtering operators test each item, projection operators transform items, and terminal operators such as ToList, Count, First, and Sum consume the sequence.
  6. Some operators stream one item at a time, such as Where and Select. Others must buffer data first, such as OrderBy because it cannot know the sorted order until it has inspected the input.

LINQ does not mutate ordinary collections unless your own lambda mutates something. A query describes how to produce a new sequence of results. If you want to store the results, assign them to a new variable or materialize them with ToArray() or ToList().

Common Mistakes

Expecting LINQ To Change The Original List

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        List<int> numbers = new List<int> { 3, 1, 2 };
        numbers.OrderBy(n => n);

        Console.WriteLine(string.Join(", ", numbers));
    }
}

Output:

3, 1, 2

OrderBy returns a sorted sequence; it does not rearrange the original list. Store or materialize the result when you need the sorted values.

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        List<int> numbers = new List<int> { 3, 1, 2 };
        List<int> sorted = numbers.OrderBy(n => n).ToList();

        Console.WriteLine(string.Join(", ", sorted));
    }
}

Output:

1, 2, 3

Enumerating The Same Query More Than Once

using System;
using System.Linq;

class Program
{
    static void Main()
    {
        int[] numbers = { 1, 2, 3 };
        var query = numbers.Select(n =>
        {
            Console.WriteLine($"Mapping {n}");
            return n * 10;
        });

        Console.WriteLine(query.Count());
        Console.WriteLine(query.First());
    }
}

Output:

Mapping 1
Mapping 2
Mapping 3
3
Mapping 1
10

Count() enumerates the query, then First() starts enumeration again. If a query performs expensive work, reads external data, or has side effects, repeated enumeration can be slow or surprising. Materialize once when you need to reuse the same results.

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        int[] numbers = { 1, 2, 3 };
        List<int> values = numbers.Select(n =>
        {
            Console.WriteLine($"Mapping {n}");
            return n * 10;
        }).ToList();

        Console.WriteLine(values.Count);
        Console.WriteLine(values[0]);
    }
}

Output:

Mapping 1
Mapping 2
Mapping 3
3
10

Best Practices

  • Use LINQ when it makes the intent clearer: filtering, selecting, sorting, grouping, or aggregating data.
  • Keep lambdas small. If the condition or transformation becomes complicated, move it into a named method.
  • Remember deferred execution. Use ToList() or ToArray() when you need a stable snapshot.
  • Avoid side effects inside LINQ lambdas. Prefer lambdas that calculate and return values.
  • Do not call Count() just to check whether anything exists; use Any().
  • Use FirstOrDefault() or SingleOrDefault() only when a missing result is acceptable, and handle the default value carefully.
  • Be aware that sorting and grouping usually buffer data, while simple filtering and selecting can stream.
  • For very performance-sensitive code, measure. LINQ is expressive, but a hand-written loop can sometimes allocate less.

Practice Exercises

  1. Create a list of test scores. Use LINQ to print only scores greater than or equal to 80, sorted from highest to lowest.
  2. Given an array of words, use Where and Select to print the lengths of words that contain the letter a.
  3. Create a small list of products with category and price. Group by category and print the average price for each category.

Summary

  • LINQ provides a consistent way to query arrays, lists, and other sequences.
  • Where filters, Select transforms, OrderBy sorts, and GroupBy creates grouped sequences.
  • Most LINQ queries over IEnumerable<T> use deferred execution.
  • ToList() and ToArray() force a query to run and store its results.
  • LINQ usually returns new sequences; it does not modify the original collection.
  • Repeated enumeration can repeat work, so materialize results when they will be reused.