Aggregation and statistical queries
Sum, Average, Min and Max throw on an empty sequence of non-nullable values, and the aggregate operators are the ones that surprise people in production.
The standard aggregates
var orders = Array.Empty<Order>();
// These throw InvalidOperationException on an empty sequence
// var total = orders.Sum(o => o.Total);
// var avg = orders.Average(o => o.Total);
// var max = orders.Max(o => o.Total);
// Safe forms
var total = orders.Sum(o => (decimal?)o.Total) ?? 0m;
var avg = orders.Average(o => (decimal?)o.Total); // null when empty
var max = orders.Select(o => (decimal?)o.Total).Max(); // null when empty
// Count versus LongCount: Count() is an int and overflows past 2.1 billion
var count = orders.LongCount();
// Any is cheaper than Count for an existence test: it can stop at the first hit
var hasAny = orders.Any();
var hasOpen = orders.Any(o => o.Status == OrderStatus.Open);
// MinBy and MaxBy return the element, not the key
var newest = orders.MaxBy(o => o.PlacedAt);
var cheapest = orders.MinBy(o => o.Total);
// Aggregate is the general fold
var csv = orders.Aggregate(
new System.Text.StringBuilder(),
(sb, o) => sb.Append(o.Id).Append(',')).ToString();
var runningTotal = orders.Aggregate(
seed: 0m,
func: (acc, o) => acc + o.Total,
resultSelector: acc => Math.Round(acc, 2));- Sum, Average, Min and Max throw on an empty sequence when the element type is a non-nullable value type. Cast the selector to a nullable to get a null instead.
- Empty
MinandMaxon a reference type return null rather than throwing, which is an inconsistency worth remembering. Count()on anIEnumerablethat is not a collection enumerates the whole thing. On anICollectionit uses theCountproperty, which is free.- For a decimal average, do the division yourself if you care about rounding:
Averageuses the type's own arithmetic.
Aggregation over groups
// One pass instead of one query per group
var report = orders
.GroupBy(o => o.Currency)
.Select(g => new
{
Currency = g.Key,
Count = g.Count(),
Total = g.Sum(o => o.Total),
Average = g.Average(o => o.Total),
Largest = g.Max(o => o.Total),
First = g.MinBy(o => o.PlacedAt)!.PlacedAt,
})
.OrderByDescending(x => x.Total)
.ToList();
// A percentile, computed from the ordered values
decimal Percentile(IEnumerable<decimal> values, double p)
{
var sorted = values.OrderBy(v => v).ToArray();
if (sorted.Length == 0) return 0m;
var rank = (p / 100.0) * (sorted.Length - 1);
var low = (int)Math.Floor(rank);
var high = (int)Math.Ceiling(rank);
if (low == high) return sorted[low];
var weight = (decimal)(rank - low);
return sorted[low] * (1 - weight) + sorted[high] * weight;
}
// What EF Core can translate: Count, Sum, Average, Min, Max per group
var perDay = await db.Orders
.Where(o => o.PlacedAt >= since)
.GroupBy(o => o.PlacedAt.Date)
.Select(g => new { Day = g.Key, Count = g.Count(), Total = g.Sum(o => o.Total) })
.ToListAsync(ct);
// A conditional aggregate, which the provider translates to a filtered count
var stats = await db.Orders
.GroupBy(o => o.Currency)
.Select(g => new
{
g.Key,
Open = g.Count(o => o.Status == OrderStatus.Open),
Fulfilled = g.Count(o => o.Status == OrderStatus.Fulfilled),
})
.ToListAsync(ct);⚠️
A custom aggregate from a library, such as a percentile or a standard deviation, will not translate to SQL. Either compute it in the database with a raw query, or fetch only the values you need and aggregate in memory — never fetch whole entities to run one arithmetic function.
FAQ
How do I sum a nullable decimal without a cast?
If the property is already decimal?, Sum returns decimal? and gives null on an empty sequence. The cast is only needed when the source property is a non-nullable decimal.
Why is Count() slow on my query?
Because it may be enumerating the whole sequence, and on a database it issues a second round trip. If you only need to know whether anything exists, use Any.
Related
Common LINQ operators Grouping, joining and set operations
Last refreshed 2026-09-18.