Summary#
The LINQ verbs work over a local List<T> — where T is a class or a plain scalar like
string or int — not just over your entities. The same spellings, evaluated in memory:
class Line { public string Sku; public int Amount; }
int TopThreeTotal(List<Line> lines) {
var top = lines.Where(l => l.Amount > 0)
.OrderByDescending(l => l.Amount)
.Take(3);
return top.Sum(l => l.Amount);
}Signature#
List<T> list.Where(x => predicate) // filter
List<U> list.Select(x => new U { … }) // project into a class…
List<V> list.Select(x => x.Field) // …or into a scalar (→ List<string>, List<int>, …)
List<U> list.GroupBy(x => key).Select(g => new U { …g.Key, g.Count(), g.Sum(e => e.V)… })
T list.First(x => p) / .Single(x => p) // one element (First/Single fault when empty; …OrDefault → null)
bool list.Any(x => p) // any match?
bool list.All(x => p) // do they ALL match? (true for an empty list, as in C#)
int list.Count() / .Count(x => p) // how many
V list.Sum / Average / Min / Max(x => v) // aggregate a selected value
List<T> list.OrderBy / OrderByDescending(x => k) // sort
List<T> list.Skip(n) / .Take(n) / .Distinct() // page / dedupe
List<T> list.Union / Concat / Intersect / Except(other) // combine two lists (set operators)
List<T> list.ToList() // a real, indexable List<T>
int list.IndexOf(item) // WHERE is this element? -1 when it is not there
int list.FindIndex(x => p) // …and where is the first one that MATCHES? -1 for noneThe source may be a List<T>, a HashSet<T>, an array (T[]), or any collection you already hold — including
the rows a query returned; either way a collection result is a real, indexable List<T>. T may be a class or a
scalar.
Description#
Given a List<T> of class values, the query verbs behave exactly as in C#: Where filters, First/Single
select one element (the …OrDefault forms return null instead of faulting on empty; Single faults when more than
one matches), Any/All/Count answer set questions, and Sum/Average/Min/Max fold a selected value — Sum and
Min/Max keep the selected value's type, Average is decimal. Skip/Take page; Distinct de-duplicates. A
collection result (Where(…), ToList()) is a real List<T> — indexable, .Count, foreach. Predicates and
selectors may capture local variables.
The verbs compose in any order, matching C# LINQ:
List<Line> Page(List<Line> items) {
return items.Where(i => i.Amount > 0).OrderBy(i => i.Sku).Skip(20).Take(10);
}Find one element, with a safe fallback:
string SkuOrNone(List<Line> lines, int amount) {
var hit = lines.FirstOrDefault(l => l.Amount == amount); // null when there is none
if (hit == null) { return "none"; }
return hit.Sku;
}How do I reshape each element? — Select#
Select reshapes each element. Project into a class you declared (the C# spelling, new U { … }), or into a
scalar — which gives you a plain List<string> / List<int> of the selected values:
class LineDto { public string Code; public int Doubled; }
List<LineDto> ToDtos(List<Line> lines) {
return lines.Select(x => new LineDto { Code = x.Sku, Doubled = x.Amount * 2 });
}
List<string> SkusByAmount(List<Line> lines) {
return lines.OrderByDescending(l => l.Amount).Select(l => l.Sku); // → a List<string>
}
int DistinctAmounts(List<Line> lines) {
return lines.Select(l => l.Amount).Distinct().Count(); // dedupe the projected values
}A HashSet<T> is a source too — filter and project a set exactly as you would a list:
int BigOnesInSet(HashSet<Line> set) {
return set.Where(x => x.Amount >= 20).Count();
}Can I query a string[] or an int[]?#
T does not have to be a class. A string[], an int[], a List<decimal> — any sequence of scalars answers the
same verbs, and the range variable is simply the value:
int NamesStartingWithA(string[] names) {
return names.Where(n => n.StartsWith("a")).Count();
}
bool AllNamed(string[] names) {
return names.All(n => n != ""); // true when there are no names at all, exactly as in C#
}
string FirstAlphabetically(string[] names) {
return names.OrderBy(k => k).First(n => n != "");
}
List<int> Lengths(string[] names) {
return names.Select(n => n.Length); // → a List<int>
}
int TotalAndLargest(int[] amounts) {
return amounts.Sum(a => a) + amounts.Max(a => a);
}There is no separate scalar dialect to learn: Where, Select, OrderBy, GroupBy, Any/All, the aggregates,
the set operators and the First/Single family all read a scalar element exactly as they read a row. The one verb that
cannot apply is Include — it loads a related row, and a string has no relations, so it is refused by name.
Rows you already fetched are a source too#
Once a query has returned, its rows are just a collection you hold — so a second question about them is asked the same way, with no "copy it into a list first" step:
bool AnyLarge(List<Line> lines) {
var positive = lines.Where(l => l.Amount > 0).ToList();
return positive.Any(l => l.Amount >= 100); // over the rows already in hand — no second fetch
}This is the same rule as everywhere else on this page — what you query decides where it runs. The first chain
above touched a list you built; had it named an entity, that chain would have run in the database, and only the rows
it returned would be here to ask about. The live var a screen binds behaves identically: it holds
rows, so .Count, .Any(…), .Where(…), indexing and foreach all read it directly.
Where in the list is it? — IndexOf and FindIndex#
The LINQ verbs above answer which element; these two answer where it sits. Both are C#'s own, both count from
zero, and both answer -1 when there is no match — never a fault, so >= 0 is the guard you write:
int WhereIsIt(List<Line> lines, Line one) {
return lines.IndexOf(one); // you HOLD the element — compare it
}
int WhereIsTheSku(List<Line> lines, string sku) {
return lines.FindIndex(l => l.Sku == sku); // you can DESCRIBE it — run a predicate
}
bool IsFirst(List<Line> lines, string sku) {
return lines.FindIndex(l => l.Sku == sku) == 0; // a miss is -1, so this is false rather than a fault
}Which one you reach for is decided by what you are holding, and nothing else. IndexOf(item) takes the element
itself; FindIndex(x => …) takes a predicate, which is what you want whenever the thing you are looking for is
described rather than held — a value typed by a user, an id off the URL, a name off a form.
The two agree by construction: IndexOf compares elements the same way == does, so a row found by one is found by
the other at the same position.
⚠ A list of ENTITY ROWS is not a special case, and this is the one people talk themselves out of. == on two
entity references compares them by row identity, so IndexOf(row) finds the row even when the
list came from one query and the row from another — the two references are the same row. Writing
FindIndex(x => x.Id == row.Id) for that is not wrong, it is the same answer one lambda longer.
A position is what a reorder is written in terms of, and that is the commonest use — where a row is, what is above it, whether it can move:
Line Previous(List<Line> ordered, string sku) {
var at = ordered.FindIndex(l => l.Sku == sku);
if (at <= 0) { return null; } // not found (-1), or already first
return ordered[at - 1];
}FindIndex searches the list in order and stops at the first match, so a predicate that matches twice answers the
earlier position.
How do I total per group, and then filter the groups?#
GroupBy(x => key) buckets the elements by a key; the Select that follows reduces each bucket, reading the key as
g.Key and folding the group's elements with g.Count() / g.Sum(…) / g.Min(…) / g.Max(…) /
g.Average(…). Project each group into a class:
class GroupStat { public string Key; public int Count; public int Total; public int MaxAmount; }
List<GroupStat> PerSku(List<Line> lines) {
return lines.GroupBy(x => x.Sku)
.Select(g => new GroupStat {
Key = g.Key,
Count = g.Count(),
Total = g.Sum(e => e.Amount),
MaxAmount = g.Max(e => e.Amount) })
.OrderBy(s => s.Key);
}A Where before the GroupBy filters the elements that get grouped. A Where after the projection filters
the groups — it is SQL's HAVING, written as an ordinary predicate over the projected shape, and it composes with
OrderBy and Take like anything else:
List<GroupStat> TopSku(List<Line> lines) {
return lines.Where(x => x.Amount >= 10) // filters ELEMENTS, before grouping
.GroupBy(x => x.Sku)
.Select(g => new GroupStat {
Key = g.Key,
Count = g.Count(),
Total = g.Sum(e => e.Amount),
MaxAmount = g.Max(e => e.Amount) })
.Where(s => s.Total >= 35) // filters GROUPS — this is HAVING
.OrderByDescending(s => s.Total)
.Take(1);
}You never write the word having: which side of the GroupBy a Where sits on says what it filters, and that is the
whole rule.
Set operators — combine two lists#
Union, Concat, Intersect, and Except combine two local lists of the same class, exactly as in C#: Union is
the distinct elements of both, Concat keeps every element (duplicates and all), Intersect keeps the elements in
both, and Except keeps the left elements that are not in the right. The operand can be a plain list or its own filtered
chain:
List<Line> Both(List<Line> a, List<Line> b) {
return a.Union(b.Where(l => l.Amount > 0)).ToList();
}De-duplication (Union/Intersect/Except) uses the same equality as Distinct: a class value is compared by
identity (the same instance), a scalar by value — the C# default. Two lists that share an instance de-dupe it;
distinct instances stay distinct.
Where does this run, and what is refused in memory?#
The distinction matters, and it is decided by what you query, not by which verb you use. Naming an entity
(Order.Where(…)) compiles the predicate into the database query, so the rows you did not ask for are never
fetched — and the security rules are part of that query. Querying a List<T> you built
in code evaluates the predicate over the elements you already have, in the running function. Same spelling, and
that is the point: you do not learn two query languages. But a list of ten million elements is ten million elements
in memory, and no security rule applies to values you constructed yourself.
What is not available on a local list. The data-store-only operations are meaningless over values you already hold,
so they report a pointed error rather than pretending: full-text search (Matches/TextScore), vector Similarity,
Traverse, and Include. Use those over your entities, where they lower to the database. (Include is refused over a
scalar element for a second reason as well: a string has nothing related to load.)
Still coming in memory — each with its own "not supported yet" diagnostic, never a silent wrong answer — are
SelectMany and Join/LeftJoin. The set operators (Union/Concat/Intersect/Except) work over local lists as
well as over entities.
See also#
- Where / Single / Count — the same verbs over your entities, where they lower to the database
- Skip / Take (paging) —
Skip/Takeover your entities (the database form) - Union / Concat / Intersect / Except —
Union/Concat/Intersect/Except(the entity-query form; the spellings are identical) - Comparing entity rows — why
IndexOf/Contains/Removeidentify an entity row correctly, whatever query it came from - class methods — the
classtypes these lists hold