# 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)                            // 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 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 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:

```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
}
```

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.

### 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.

### 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.

### 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.

**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](https://osysharp.com/reference/query/set-operators/).

## 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)
- [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
