Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Reference / Query

LINQ over a local list

list.Where(x => …) · .Select(x => new T { … }) · .GroupBy(x => key) · .First(…) · .Any(…) · .Count(…) · .Sum(x => …) · .OrderBy(…) · .Take(n) · .IndexOf(item) · .FindIndex(x => …)

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.

stable12 examples compiled by CIlinqquerycollectionslist

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

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#

Related

Skip / Take (paging)

Page a query with Skip(n) (OFFSET) and Take(m) (LIMIT). The count can be a compile-time constant OR a runtime integer —…

Union / Concat / Intersect / Except

Combines two row-queries over the same entity with SQL set semantics: Union dedups, Concat keeps duplicates (UNION…

class methods

Behaviour attached to a class — a method with a receiver, called as value.Method(). Classes are in-memory values, so a…

Sum / Average / Min / Max / Count

Fold rows down to a single number — a query or a `List<T>` you already hold. The one thing to know before you use them:…