C# from the ground up

LINQ: asking questions of a collection

Where, Select and OrderBy as one chain of questions, Sum, Any and Count for the answers, and why the calls read like the sentence you meant.

Part 11 of 13

Advanced17 min readBack to Courses

A collection can answer questions

LINQ is the part of C# that lets a collection answer questions in the words of the question. prices.Where(p => p > 10m) keeps the prices over ten, prices.Sum() adds them up, and prices.Any(p => p < 5m) answers true or false. One using System.Linq; at the top of the file brings the whole vocabulary in.

The little arrow is a function written in place: p => p > 10m reads as "a price goes in, true or false comes out". It has no name because it is used once, right there, which is exactly the right amount of ceremony for a test.

Filter, project, sort

Three calls do most of the work. Where keeps the values that pass a test, so the sequence it hands back is shorter. Select turns each value into something else, so the sequence it hands back is a different shape: prices become lines of text. OrderBy sorts, and OrderByDescending sorts the other way round.

The calls chain, because each one takes a sequence and hands back a sequence. Read a chain from the top: take the prices, keep the ones over ten, sort them, turn each into text. That is a sentence in the same order as the loops it replaces — with no counter, no temporary list and no chance of an index running off the end.

Numbers, and the one trap worth knowing

Sum adds, Average divides by the count, and Min and Max pick an end. Count() counts the values in a sequence, and Any answers whether there is at least one — the question to ask when all you want to know is that the sequence is not empty.

The trap is that a List already has a Count property, while LINQ has a Count() method. On the original list, prices.Count is the property and needs no parentheses. After a Where, what comes back is a sequence rather than a list, so counting it means calling Count(). Using the property on a filtered sequence is a compile error with a clear message; the method works on both, which is why it is the safer habit once a query is involved.

Two edge cases are worth knowing before they appear in a report. Sum on an empty sequence answers zero, which is usually the sensible total, while Average on an empty sequence throws, because there is no count to divide by. Min and Max throw for the same reason. When the data may be empty, check with Any first or supply a default.

One collection, five questions

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

class Program
{
    static void Main()
    {
        var prices = new List<decimal> { 4.5m, 12m, 30.25m };

        Console.WriteLine(prices.Count);              // 3
        Console.WriteLine(prices.Sum());              // 46.75
        Console.WriteLine(prices.Any(p => p > 100m)); // False
        Console.WriteLine(prices.First());            // 4.5

        // Where keeps what passes the test.
        var big = prices.Where(p => p > 10m);

        // Select turns each value into something else.
        var text = big.Select(p => p.ToString("F2"));

        // OrderByDescending sorts the other way round.
        var biggest = prices.OrderByDescending(p => p);

        Console.WriteLine(big.Count());               // 2
        Console.WriteLine(string.Join(", ", text));
        Console.WriteLine(biggest.First());           // 30.25

        // A chain, read from the top down.
        var lines = prices
            .Where(p => p > 5m)
            .OrderBy(p => p)
            .Select(p => p.ToString("F2"));
        Console.WriteLine(string.Join(" | ", lines));
    }
}

Four prices, six answers

Run it and read the eight lines. Then add a line with the average price, and one that counts how many prices are under ten using Where and Count().

Output
Click "Run" to execute your code

Read a chain out loud before writing it. If the sentence is right — keep these, sort them, turn them into text — the calls are almost always in the same order.

A query is a plan, not a result

A LINQ call such as Where does not walk the list when you write it. It builds a small object that remembers the source and the test, and hands that object back. The walk happens when something enumerates it: a foreach, a Count(), a Sum().

This is deferred execution, and it is why a query can be built in pieces and only run at the end. It is also why a query that reads a variable uses the value that variable has when the query runs, not the value it had when the query was written.

Each enumeration runs the plan again

Calling foreach on the same query twice walks the source twice. If the source is a list in memory, that is merely wasteful. If the work behind it were expensive, it would be worse, which is why a query used more than once is often materialised with ToList().

OrderBy does not sort the original list either. It returns a new sequence that yields the items in the new order, leaving the list exactly as it was. That is the same rule as strings: the call hands back a result, and the source is untouched.

Three LINQ mistakes: using Count without parentheses on a filtered sequence, where the LINQ method is the only form available; enumerating the same deferred query several times and wondering why the work repeats; and expecting OrderBy to change the list in place, when it returns a sorted sequence and leaves the original alone.

Try these next

  • Build a query with Where, run it twice with foreach, and add a ToList() after the first run to see the difference.
  • Sort a list with OrderBy, print the result, then print the original list to confirm it is unchanged.
  • Replace a foreach that builds a list of names with Where and Select, and compare the line count.

Names, filtered and shaped

Run it and read the six lines. Then add two names of your own, one short and one long, and check how the four answers change.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What does Where(p => p > 10m) hand back?