# LINQ over a local list

> Query a local `List<T>`, `HashSet<T>` or `T[]` — of your own `class` values OR of plain scalars like `string[]` and `int[]` — with the same LINQ verbs you use over data: `Where`, `Select` projections, `GroupBy` with per-group aggregates, `First`/`FirstOrDefault`/`Single`, `Any`, `Count`, `Sum`/`Average`/`Min`/`Max`, `Distinct`, `Skip`, `OrderBy`, `Take` — evaluated in memory. The data-store-only operations (full-text, similarity) don't apply to a plain list.

<!-- id: query-in-memory-linq · area: query · stability: stable · html: https://osysharp.com/reference/query/in-memory-linq/ -->

## Summary        {#summary}
The LINQ verbs work over a local **`List<T>`** — where `T` is a [`class`](https://osysharp.com/reference/class/methods/) *or* a plain scalar like
`string` or `int` — not just over your entities. The same spellings, evaluated in memory:

```osy title="filter, sort, total — over a list you built in code" test app=query-in-memory-linq
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      {#signature}
```osy syntax
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) / .LastIndexOf(item)       // WHERE is this element? first / last. -1 when absent
int     list.FindIndex(x => p) / .FindLastIndex(x => p)  // …and by PREDICATE: first / last match. -1 for none
```

The 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    {#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:

```osy title="compose in any order" test app=query-in-memory-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:

```osy title="the miss is a null, not a fault" test app=query-in-memory-linq
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}
`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:

```osy title="project into a class, or into a scalar" test app=query-in-memory-linq
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:

```osy title="a HashSet is a LINQ source as well" test app=query-in-memory-linq
int BigOnesInSet(HashSet<Line> set) {
  return set.Where(x => x.Amount >= 20).Count();
}
```

### Can I query a `string[]` or an `int[]`?   {#scalar-elements}
`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:

```osy title="the same verbs, over plain values" test app=query-in-memory-linq
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   {#fetched-rows}
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:

```osy title="ask a second question about rows you already have" test app=query-in-memory-linq
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`](https://osysharp.com/reference/ui/reactivity/) 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`   {#positions}
The LINQ verbs above answer *which* element; these answer **where it sits**. They are C#'s own, they count from
zero, and they answer **`-1`** when there is no match — never a fault, so `>= 0` is the guard you write.

There are **four**, and they are two pairs. One axis is what you are holding — the element itself, or a predicate
that describes it. The other is which end you want:

|                    | the FIRST match     | the LAST match          |
|--------------------|---------------------|-------------------------|
| you HOLD it        | `IndexOf(item)`     | `LastIndexOf(item)`     |
| you DESCRIBE it    | `FindIndex(x => …)` | `FindLastIndex(x => …)` |

```osy title="the position of an element, by value and by predicate" test app=query-in-memory-linq
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
}

int WhereIsTheLastSku(List<Line> lines, string sku) {
  return lines.FindLastIndex(l => l.Sku == sku);   // the LAST one that matches — "the most recent entry for this sku"
}
```

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](https://osysharp.com/reference/entity/equality/), 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:

```osy title="the row above this one" test app=query-in-memory-linq
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. **`FindLastIndex` searches from the END** and stops at the last, which is what you want for "the most
recent one", "the row below this one" and any reorder that walks downwards:

```osy title="first and last, over the same list" test app=query-in-memory-linq
class Ends { public int First; public int Last; }

Ends BothEnds(List<Line> lines, string sku) {
  return new Ends {
    First = lines.FindIndex(l => l.Sku == sku),       // 0, if the sku is first and also appears later
    Last = lines.FindLastIndex(l => l.Sku == sku),    // …and the position of its LAST appearance
  };
}
```

Both answer a position **in the list you asked** — `FindLastIndex` is a backwards scan, not a search over a reversed
copy, so the index it hands back indexes the original. And both **short-circuit**, from their own end: a predicate
with a side effect sees the elements up to the hit, and for `FindLastIndex` that means the tail.

### How do I total per group, and then filter the groups?   {#groupby}
`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`:

```osy title="group by a key, reduce each group" test app=query-in-memory-linq
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:

```osy title="filter elements, then filter groups (HAVING), then take the top" test app=query-in-memory-linq
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.

### Flatten, match, pair, fold and index — `SelectMany`, `Join`, `Zip`, `Aggregate`, `ToDictionary`   {#lambdas}
The verbs that take lambdas of their own work over a list in hand exactly as in C#, and a chain continues over what
they answer:

- **`SelectMany(x => seq)`** flattens one sequence per element into one list — in source order, then each element's own
  order. The selector may answer a list, an array or a list literal (`x => [x, x * 10]`). The two-selector form
  `SelectMany(x => seq, (x, item) => …)` sees each element *and* each of its items; `(x, i) => …` binds the position.
- **`Join(ys, x => key, y => key, (x, y) => …)`** pairs the elements whose keys are equal — in the outer list's order,
  then the inner list's. A null key matches nothing. The two keys must be the same type.
- **`LeftJoin(…)`** is `Join` that keeps an outer element with no match, pairing it with C#'s `default`: `0` for a
  number, `false` for a bool, and `null` for anything else — so the result selector's second parameter is nullable
  there, and the compiler asks you to guard it.
- **`Zip(ys, (a, b) => …)`** pairs elements by position and stops at the shorter list.
- **`Aggregate((acc, x) => …)`** folds the list into one value, starting from the first element — so over an **empty**
  list it throws, as C# does. `Aggregate(seed, (acc, x) => …)` starts from the seed (and never throws), and a third
  argument `acc => …` maps the final value.
- **`ToDictionary(x => key)`** / **`ToDictionary(x => key, x => value)`** builds a `Dictionary<K, V>`. A duplicate or a
  null key throws, with the same message `Add` gives.

```osy title="flatten, match, pair, fold and index — over lists you already hold" test app=query-in-memory-linq
class Crew { public string Name; public List<string> Skills; }

List<string> EverySkill(List<Crew> crews) {
  return crews.SelectMany(c => c.Skills, (c, s) => c.Name + ": " + s).ToList();
}

List<string> Assignments(List<string> tasks, List<Crew> crews) {
  // the task keeps its place even when nobody is named for it — `c` is null there
  return tasks.LeftJoin(crews, t => t, c => c.Name, (t, c) => c == null ? t + " (open)" : t + " → " + c.Name).ToList();
}

List<int> Totals(List<int> planned, List<int> extra) {
  return planned.Zip(extra, (p, e) => p + e).ToList();
}

int Longest(List<Crew> crews) {
  return crews.Aggregate(0, (widest, c) => c.Skills.Count > widest ? c.Skills.Count : widest);
}

int SkillCountOf(List<Crew> crews, string name) {
  var byName = crews.ToDictionary(c => c.Name, c => c.Skills.Count);
  return byName.ContainsKey(name) ? byName[name] : 0;
}
```

### Keep one of each — `DistinctBy`   {#distinct-by}
`DistinctBy(x => key)` keeps the **first** element of each key and drops the rest, in the order the list holds them —
.NET's own verb. The key is compared the way `Distinct` compares (a scalar by value, a `class` value by identity), and a
`null` key is a key like any other: the first element whose key is null is kept. `Distinct()` with no key is the
whole-element form.

```osy title="the first line of each SKU" test app=query-in-memory-linq
List<Line> FirstOfEachSku(List<Line> lines) {
  return lines.DistinctBy(l => l.Sku).ToList();
}

int SkusWithAPositiveFirstLine(List<Line> lines) {
  return lines.DistinctBy(l => l.Sku).Where(l => l.Amount > 0).Count();
}
```

The overload that takes an equality comparer is not supported. Over rows read from the data store `DistinctBy` is refused
and points at `….ToList()` first — the database has no "first" without an order, and C# on a database refuses it too.

### Set operators — combine two lists   {#set-operators}
`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:

```osy title="combine two lists" test app=query-in-memory-linq
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.

Either side may be **reshaped first**, and the chain goes on after the operator — `Select` before `Concat` combines
the projected values, and a `Where` or `Count` after it runs over the combined list:

```osy title="project, combine, keep going" test app=query-in-memory-linq
int BigOnes(List<Line> a, List<int> extraAmounts) {
  return a.Select(l => l.Amount).Concat(extraAmounts).Where(n => n > 10).Count();
}
```

### What else does `System.Linq` give me over a list in hand?   {#more-verbs}
The rest of C#'s `Enumerable` works over a list you hold, with C#'s answers:

| verb | answers |
|---|---|
| `Prepend(x)` · `Append(x)` | one element before / after the rest |
| `TakeLast(n)` · `SkipLast(n)` | the last `n` elements / all but them |
| `MinBy(x => key)` · `MaxBy(x => key)` | the **element** with the smallest / largest key — not the key itself, which is what `Min`/`Max` answer |
| `ExceptBy(keys, x => key)` · `IntersectBy(keys, x => key)` | the elements whose key is not / is in a sequence of **keys** |
| `UnionBy(other, x => key)` | both lists, keeping the first element of each key |
| `GroupJoin(inner, o => key, i => key, (o, matches) => …)` | each outer element with **all** its matches — none is an empty list, not a missing row |
| `RightJoin(inner, o => key, i => key, (o, i) => …)` | `LeftJoin` from the other side: every inner element, and `null` for an outer that has no match |
| `Order()` · `OrderDescending()` | sorted by the elements themselves |
| `Shuffle()` | the same elements in a random order (.NET 10) |
| `AsEnumerable()` | the sequence itself — a no-op here, kept so C# you paste compiles |
| `LongCount()` | `Count()` as a `long` |
| `SequenceEqual(other)` | whether the two hold equal elements in the same order |

Over rows read from the data store each of these but `LongCount` refuses by name and points at `….ToList()` first,
because none has a SQL form.

```osy title="the rest of Enumerable, pinned" run app=query-in-memory-linq
[Test]
void The_rest_of_enumerable_answers_what_csharp_answers() {
  var n = new List<int> { 3, 1, 4, 1, 5 };
  Assert.Equal(new List<int> { 9, 3, 1, 4, 1, 5 }, n.Prepend(9).ToList());
  Assert.Equal(new List<int> { 3, 1, 4 }, n.SkipLast(2).ToList());
  Assert.Equal(new List<int> { 1, 5 }, n.TakeLast(2).ToList());
  Assert.Equal(new List<int> { 5, 4, 3, 1, 1 }, n.OrderDescending().ToList());
  Assert.Equal(5L, n.LongCount());
  Assert.Equal(5, n.AsEnumerable().Count());
  Assert.Equal(5, n.Shuffle().Count());
  Assert.True(n.SequenceEqual(new List<int> { 3, 1, 4, 1, 5 }));
  Assert.False(n.SequenceEqual(new List<int> { 1, 3, 4, 1, 5 }));

  var lines = new List<Line> { new Line { Sku = "A", Amount = 3 }, new Line { Sku = "B", Amount = 9 }, new Line { Sku = "C", Amount = 1 } };
  Assert.Equal("C", lines.MinBy(l => l.Amount).Sku);
  Assert.Equal("B", lines.MaxBy(l => l.Amount).Sku);
  var gone = new List<string> { "B" };
  Assert.Equal(new List<string> { "A", "C" }, lines.ExceptBy(gone, l => l.Sku).Select(l => l.Sku).ToList());
  Assert.Equal(new List<string> { "B" }, lines.IntersectBy(gone, l => l.Sku).Select(l => l.Sku).ToList());
  var more = new List<Line> { new Line { Sku = "B", Amount = 0 }, new Line { Sku = "D", Amount = 2 } };
  Assert.Equal(new List<string> { "A", "B", "C", "D" }, lines.UnionBy(more, l => l.Sku).Select(l => l.Sku).ToList());

  var skus = new List<string> { "A", "B", "Z" };
  Assert.Equal(new List<string> { "A:0", "B:1", "Z:0" },
    skus.GroupJoin(more, s => s, l => l.Sku, (s, matches) => s + ":" + matches.Count()).ToList());
  Assert.Equal(new List<string> { "B=B", "?=D" },
    skus.RightJoin(more, s => s, l => l.Sku, (s, l) => (s ?? "?") + "=" + l.Sku).ToList());
}
```

### Where does this run, and what is refused in memory?   {#in-memory-vs-data}
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](https://osysharp.com/reference/security/entity-security/). 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.

**A grouping can answer one value per group, too.** `list.GroupBy(k).Select(g => g.Count())` (or `g => g.Key`, or
any aggregate) answers a plain list of those values, as in C#. And the groups themselves are values —
[`IGrouping<K, T>`](https://osysharp.com/reference/query/igrouping/), with `ToLookup` holding them by key.

```osy title="one number per group" test app=query-in-memory-linq
List<int> CountPerSku(List<Line> lines) {
  return lines.GroupBy(l => l.Sku).Select(g => g.Count()).ToList();
}
```

**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.)

`Zip` with no result selector pairs the elements as C# does, and `TakeWhile`/`SkipWhile` run over a list in hand.
`Zip`, `TakeWhile`, `SkipWhile`, `Aggregate`, `ToDictionary` and `DistinctBy` exist only over values in hand — over rows
read from the data store they refuse and point at `….ToList()` first; `SelectMany` and `Join`/`LeftJoin` also lower to the database
[over entities](https://osysharp.com/reference/query/joins/), and the set operators work [over entities](https://osysharp.com/reference/query/set-operators/) as well.

## See also       {#see-also}
- [Where / Single / Count](https://osysharp.com/reference/query/where/) — the same verbs over your entities, where they lower to the database
- [Skip / Take (paging)](https://osysharp.com/reference/query/paging/) — `Skip`/`Take` over your entities (the database form)
- [Union / Concat / Intersect / Except](https://osysharp.com/reference/query/set-operators/) — `Union`/`Concat`/`Intersect`/`Except` (the entity-query form; the spellings are identical)
- [Join / LeftJoin / SelectMany](https://osysharp.com/reference/query/joins/) — `SelectMany`, `Join` and `LeftJoin` over your entities, where they become SQL joins
- [Comparing entity rows](https://osysharp.com/reference/entity/equality/) — why `IndexOf`/`Contains`/`Remove` identify an entity row correctly, whatever query it came from
- [class methods](https://osysharp.com/reference/class/methods/) — the `class` types these lists hold
