100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
C#

Aggregate Functions: Sum, Count, Average

Learn how LINQ's Sum, Count, and Average operators collapse sequences into single scalar values, and how execution timing and edge cases affect their use.

Aggregation and SetsBeginner9 min readJul 10, 2026
Analogies

What Aggregate Functions Do

LINQ's aggregate operators — Count(), Sum(), Average(), Min(), and Max() — reduce an entire IEnumerable<T> sequence down to a single scalar value instead of producing a new sequence. Unlike Select() or Where(), which build up a query that is only evaluated when you enumerate it, aggregate operators trigger immediate execution the moment you call them, walking the whole source right there.

🏏

Cricket analogy: Just as a scorecard reduces an entire innings of deliveries into one number like 'total runs 287', Sum() collapses a whole sequence of order amounts into a single total the instant you call it, rather than deferring like Where() does.

Count and LongCount

Count() enumerates the source to tally its elements, but if the underlying type already implements ICollection<T> (like List<T> or an array), LINQ's implementation short-circuits and reads the Count or Length property directly for an O(1) lookup instead of iterating. Count(predicate) counts only elements matching a condition without ever materializing a filtered intermediate list, and LongCount() exists for sequences whose element count could exceed int.MaxValue, returning a long instead of an int.

🏏

Cricket analogy: A scorer checking 'how many fours were hit' flips through the ball-by-ball log and tallies only boundary events, just as Count(predicate) counts matching elements without building a separate list of just the fours.

Sum and Average

Sum() requires a selector when the source isn't already a sequence of numbers, and it has overloads for int, long, decimal, double, float, and their nullable counterparts, always returning the same type family as the input (int in, int out; ignoring nulls in nullable overloads). Average() is different: even Average() on a sequence of ints returns a double (or you can select into decimal for currency), because an average of whole numbers can be fractional, and both Sum() and Average() will throw an InvalidOperationException on Average() over an empty sequence unless you use the nullable overload, which returns null instead.

🏏

Cricket analogy: A batter's strike rate is reported as a decimal like 135.42 even though runs and balls faced are both whole numbers, just as Average() promotes an int sequence to a double result rather than truncating.

Combining Where With Aggregates

It's common to filter with Where() before aggregating, such as orders.Where(o => o.Status == "Shipped").Sum(o => o.Total), but because Where() is deferred and Sum() is immediate, each call to Sum() or Average() on the same query re-enumerates the source from scratch; if the source is an expensive IEnumerable<T> like a database query or a generator, calling multiple aggregates back-to-back (Count(), then Sum(), then Average()) can trigger the query three separate times unless you materialize with ToList() first.

🏏

Cricket analogy: Re-running a video review for every single decision — LBW, run-out, boundary — instead of watching the footage once and noting all three outcomes wastes time, just as calling Count(), Sum(), and Average() separately re-enumerates a lazy source three times instead of materializing it once with ToList().

csharp
var orders = new List<Order>
{
    new Order { Id = 1, Status = "Shipped", Total = 120.50m },
    new Order { Id = 2, Status = "Pending", Total = 45.00m },
    new Order { Id = 3, Status = "Shipped", Total = 89.99m },
};

// Count with a predicate — no intermediate list is built
int shippedCount = orders.Count(o => o.Status == "Shipped"); // 2

// Sum requires a selector when the source isn't plain numbers
decimal shippedTotal = orders.Where(o => o.Status == "Shipped")
                              .Sum(o => o.Total); // 210.49

// Average always returns a floating type, and throws on empty sequences
double avgLineItems = orders.Select(o => o.Total).DefaultIfEmpty(0).Average();

// Materialize once to avoid re-enumerating three times
var shipped = orders.Where(o => o.Status == "Shipped").ToList();
var summary = new
{
    Count = shipped.Count,
    Total = shipped.Sum(o => o.Total),
    Average = shipped.Average(o => o.Total)
};

Calling Average() or Min()/Max() on an empty sequence throws an InvalidOperationException at runtime — LINQ has no sensible scalar to return. Guard with DefaultIfEmpty(), check Any() first, or use the nullable overloads (e.g., Average(x => (int?)x.Value)) which return null instead of throwing.

Count() is smart about its source: for any type implementing ICollection<T> (List<T>, arrays, HashSet<T>), calling .Count() is an O(1) property read rather than an O(n) walk — but the moment you add a predicate, e.g. Count(x => x.IsActive), it must enumerate because filtering can't be pre-computed.

  • Aggregate operators (Count, Sum, Average, Min, Max) trigger immediate execution and return a single scalar.
  • Count() short-circuits to an O(1) property read for ICollection<T> sources, but not when a predicate is supplied.
  • LongCount() is for sequences that may exceed int.MaxValue elements.
  • Sum() preserves the numeric type family of its selector; Average() always promotes to double or decimal.
  • Average() and Min()/Max() throw InvalidOperationException on empty sequences unless using nullable overloads.
  • Chaining multiple aggregates on a deferred query re-enumerates the source each time — materialize with ToList() first.
  • Where() is deferred and lazy; the aggregate call is what actually forces enumeration.

Practice what you learned

Was this page helpful?

Topics covered

#LINQStudyNotes#MicrosoftTechnologies#AggregateFunctionsSumCountAverage#Aggregate#Functions#Sum#Count#StudyNotes#SkillVeris#ExamPrep