C# LINQ Method Syntax

LINQ method syntax is the style of querying data by chaining methods such as Where, Select, OrderBy, and GroupBy. It matters because these methods are the core LINQ API: even query syntax is translated into method calls by the compiler. Once you understand method syntax, you can read documentation, combine operators fluently, and build practical data pipelines over arrays, lists, and other sequences.

Overview: How Method Syntax Works

Method syntax treats a collection as a sequence and applies one operation after another. The source is usually an IEnumerable<T>, such as an array, a List<T>, or the result of another LINQ query. Each LINQ method is strongly typed. If the source is an IEnumerable<Product>, the lambda passed to Where receives a Product; if Select returns a string, the next operator sees an IEnumerable<string>.

Most method syntax operators are extension methods from System.Linq.Enumerable. An extension method looks like an instance method when you call it, so numbers.Where(n => n > 0) is compiled as a static method call to Enumerable.Where(numbers, n => n > 0). The first argument is the source sequence, and the second argument is a delegate created from the lambda expression.

Many operators use deferred execution. Calling Where or Select usually does not loop through the collection immediately. Instead, the call returns an iterator object that stores the source and your lambda. The query runs later when something enumerates it: a foreach loop, ToList(), ToArray(), Count(), First(), Sum(), or string.Join. This lets simple filters and projections stream one value at a time without creating intermediate lists.

Not every operator streams in the same way. Where, Select, Skip, and Take can usually pass values through as needed. OrderBy must buffer all input before it can return the first sorted item. GroupBy also builds groupings. Method syntax is still ordinary C# running on the CLR: generic methods, delegates, iterator objects, and enumerators do the work.

Syntax

string[] source = { "ant", "bee", "cat", "deer" };

var results = source
    .Where(item => item.Length >= 3)
    .OrderBy(item => item)
    .Select(item => item.ToUpper())
    .ToList();
Part Meaning
source The sequence being queried, commonly an array, list, or another IEnumerable<T>.
.Where(...) Filters items. The lambda must return true for items to keep.
.OrderBy(...) Sorts by a key. Use ThenBy for secondary sorting and OrderByDescending for reverse order.
.Select(...) Projects each item into a new value or shape.
.ToList() Materializes the query so it runs now and stores a snapshot in a list.

Method chains are normally formatted one operator per line. This makes the pipeline readable: filter first, sort or group when needed, then project the final result. A complete program should include the System.Linq namespace.

Examples

Example 1: Filter, Sort, And Project

using System;
using System.Linq;

class Program
{
    static void Main()
    {
        string[] languages = { "C#", "Python", "Go", "Java", "Rust", "JavaScript" };

        var names = languages
            .Where(language => language.Length >= 4)
            .OrderBy(language => language)
            .Select(language => language.ToUpper());

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

Output:

JAVA
JAVASCRIPT
PYTHON
RUST

The source is an array of strings. Where keeps names with at least four characters, OrderBy sorts the remaining strings alphabetically, and Select changes each result to uppercase. The query is not executed when names is assigned; it runs when the foreach loop asks for items.

Example 2: Project Objects Into A Report Shape

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

record Course(string Title, string Level, int Minutes, double Rating);

class Program
{
    static void Main()
    {
        var courses = new List<Course>
        {
            new Course("C# Basics", "Beginner", 90, 4.6),
            new Course("LINQ Method Syntax", "Intermediate", 55, 4.8),
            new Course("Async C#", "Intermediate", 70, 4.7),
            new Course("Reflection", "Advanced", 65, 4.1)
        };

        var recommended = courses
            .Where(course => course.Level == "Intermediate" && course.Rating >= 4.7)
            .Select(course => new
            {
                course.Title,
                Label = $"{course.Minutes} min, {course.Rating:F1} stars"
            });

        foreach (var course in recommended)
        {
            Console.WriteLine($"{course.Title}: {course.Label}");
        }
    }
}

Output:

LINQ Method Syntax: 55 min, 4.8 stars
Async C#: 70 min, 4.7 stars

This example uses a record as the input type and an anonymous object as the output type. The lambda in Where can use normal C# boolean logic. The lambda in Select creates a smaller report object containing only the values needed by the display code.

Example 3: Group And Aggregate

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

record Sale(string Region, string Product, decimal Amount);

class Program
{
    static void Main()
    {
        var sales = new List<Sale>
        {
            new Sale("East", "Keyboard", 120.00m),
            new Sale("West", "Mouse", 45.50m),
            new Sale("East", "Monitor", 210.00m),
            new Sale("West", "Cable", 12.25m),
            new Sale("North", "Keyboard", 115.00m)
        };

        var totals = sales
            .GroupBy(sale => sale.Region)
            .Select(group => new
            {
                Region = group.Key,
                Count = group.Count(),
                Total = group.Sum(sale => sale.Amount)
            })
            .OrderByDescending(row => row.Total);

        foreach (var row in totals)
        {
            Console.WriteLine($"{row.Region}: {row.Count} sales, {row.Total:F2}");
        }
    }
}

Output:

East: 2 sales, 330.00
North: 1 sales, 115.00
West: 2 sales, 57.75

GroupBy creates one group per region. Each group has a Key and contains the matching Sale objects. Inside Select, aggregate methods such as Count and Sum summarize each group. The final OrderByDescending sorts the report rows by total revenue.

Example 4: Paging With Skip And Take

using System;
using System.Linq;

class Program
{
    static void Main()
    {
        int[] scores = { 98, 72, 87, 91, 65, 84, 79 };

        var secondPage = scores
            .OrderByDescending(score => score)
            .Skip(3)
            .Take(3);

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

Output:

84, 79, 72

This is a common method-syntax pattern for paging. First sort into a stable order, then skip the items from earlier pages, then take the page size. Sorting happens before paging because a page without a defined order is not predictable.

How It Works Step By Step

  1. The compiler finds extension methods from System.Linq that match the source type and lambda types.
  2. Each lambda is compiled into a delegate, such as Func<Course, bool> for a filter or Func<Course, string> for a projection.
  3. Deferred operators return objects that remember the source sequence and delegate. They do not normally allocate a full result collection.
  4. When enumeration begins, the outer operator asks the previous operator for values. In a chain, values flow through the pipeline from the source toward the final consumer.
  5. Terminal operators consume the sequence. ToList stores all results, First stops after the first match, Any stops as soon as it can answer, and Sum scans the values it needs to total.
  6. Buffering operators such as OrderBy and GroupBy may read the full source before producing output.

Because method syntax is strongly typed, operator order can change the type of the next lambda parameter. After Select(course => course.Title), the sequence contains strings, so a later lambda no longer has access to course.Rating. This is one reason to usually filter and sort on the original object before projecting to a smaller shape.

Common Mistakes

Projecting Too Early

var query = courses
    .Select(course => course.Title)
    .Where(course => course.Rating >= 4.5);

This does not compile. After Select(course => course.Title), the sequence contains strings, not course objects, so Rating is no longer available. Filter first, then project.

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

record Course(string Title, double Rating);

class Program
{
    static void Main()
    {
        var courses = new List<Course>
        {
            new Course("C# Basics", 4.6),
            new Course("Old APIs", 3.9)
        };

        var titles = courses
            .Where(course => course.Rating >= 4.5)
            .Select(course => course.Title);

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

Output:

C# Basics

Forgetting That Queries Are Deferred

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(number => number % 2 == 0);

        numbers.Add(6);

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

Output:

2, 4, 6

The query was defined before 6 was added, but it ran afterward. If you need the values as they were at query creation time, call ToList() immediately.

Repeating Expensive Work

using System;
using System.Linq;

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

        Console.WriteLine(doubled.Count());
        Console.WriteLine(doubled.Sum());
    }
}

Output:

Doubling 1
Doubling 2
Doubling 3
3
Doubling 1
Doubling 2
Doubling 3
12

Count() enumerates the query, then Sum() enumerates it again. If the projection is expensive or has side effects, materialize once with ToList() and reuse the list.

Best Practices

  • Use one LINQ operator per line for chains longer than one or two calls.
  • Filter early with Where so later operators process fewer items.
  • Project with Select after filters and sorts that need the original object.
  • Use Any() to check whether at least one item exists; avoid Count() > 0 for that purpose.
  • Use FirstOrDefault() only when a missing item is acceptable, and handle the default value.
  • Materialize with ToList() or ToArray() when you need a snapshot or will enumerate results multiple times.
  • Avoid changing external state inside LINQ lambdas. Prefer expressions that return values without side effects.
  • Remember that OrderBy starts a new sort; use ThenBy for a secondary sort key.
  • For database-backed IQueryable<T> queries, keep lambdas translatable by the provider and avoid custom methods inside the query.

Practice Exercises

  1. Create an array of city names. Use method syntax to print names with at least six characters, sorted alphabetically.
  2. Create a list of products with name, category, and price. Print the three most expensive products as Name: Price.
  3. Create a list of orders with customer name and total. Group by customer and print each customer’s total spending, highest first.

Summary

  • LINQ method syntax uses chained extension methods from System.Linq.
  • Lambdas tell operators how to filter, sort, project, group, or aggregate items.
  • Most IEnumerable<T> queries are deferred until they are enumerated.
  • Streaming operators process values on demand, while sorting and grouping usually buffer data.
  • Operator order matters because each method changes the sequence seen by the next method.
  • Materialize results when you need a stable snapshot or plan to reuse an expensive query.