Summary#
.ToList() runs the query. Up to that point you have built a description — a set of conditions, an order, a page
— and nothing has touched the database. ToList is where it executes and you get rows back, typed as List<Entity>
exactly as in C#.
Signature#
<Entity>.Where(…).OrderBy(…).ToList() → List<Entity> // materialised, and mutable
<Entity>.Where(…).OrderBy(…).ToArray() → <Entity>[] // materialised, and fixed-size
<Entity>.Where(…).OrderBy(…) → <Entity>[] // a chain that simply endsDescription#
When does the query actually run?#
The chain composes without executing. Only ToList (and the scalar calls — Count, Any, Single) go to the
database:
entity Order {
[Required] string Code;
decimal Total;
}
List<Order> TopOrders(decimal min, int take) {
return Order
.Where(o => o.Total >= min) // nothing has run yet
.OrderByDescending(o => o.Total) // still nothing
.Take(take)
.ToList(); // NOW it runs — one query, one round trip
}What you get back#
A List<Entity> — materialised rows. It has a .Count, it can be indexed, and it can be walked:
decimal SumOfTop(decimal min, int take) {
var top = TopOrders(min, take);
var total = 0m;
foreach (var o in top) { total += o.Total; } // walk it
var first = top.Count > 0 ? top[0].Total : 0m; // index it
return total + first * 0m;
}Why does calling it twice do the work twice?#
Because ToList is the moment work happens, calling it twice does the work twice. Materialise into a local and use
that:
string Describe(decimal min) {
var orders = Order.Where(o => o.Total >= min).ToList(); // one query
var count = orders.Count;
var total = 0m;
foreach (var o in orders) { total += o.Total; }
return Convert.ToString(count) + " orders, " + Convert.ToString(total);
}Writing Order.Where(…).ToList() twice in that function would run two identical queries — and, if a row changed in
between, give you two different answers to the same question.
ToList or ToArray — which materialiser#
Both run the query and bring back the same rows. They differ only in the type you are left holding, and that is the same difference C# draws:
.ToList()answers aList<Entity>— you can.Addto it, so it is what you want when the rows are the start of something you are still assembling..ToArray()answers anEntity[]— fixed-size, and the plainer statement when the rows are the answer.
A List<T> is accepted anywhere a T[] is asked for, because giving up .Add is always safe. The reverse is not: a
T[] is not a List<T>, and a function that wants one says so by materialising with .ToList().
List<Order> Growing(decimal min) { return Order.Where(o => o.Total >= min).ToList(); }
Order[] Fixed(decimal min) { return Order.Where(o => o.Total >= min).ToArray(); }
// A List goes where an array is wanted — no conversion written, none needed.
Order[] Widened(decimal min) { return Order.Where(o => o.Total >= min).ToList(); }
// …and `.ToArray()` says it out loud, on a list you built yourself.
string[] Codes(decimal min) {
var codes = new List<string>();
foreach (var o in Order.Where(o => o.Total >= min).ToList()) { codes.Add(o.Code); }
return codes.ToArray();
}⚠ .ToArray() takes a COPY. Adding to the list afterwards does not change the array you already took — that is
what makes "fixed-size" mean anything.
When you only want a number#
If all you need is how many, do not materialise the rows to count them. Count() asks the database for the number and
brings back one integer instead of ten thousand rows. See Where / Single / Count.
See also#
- Where / Single / Count — building the query
ToListruns - Skip / Take (paging) —
Skip/Take, to materialise one page instead of everything - foreach — walking what came back