# A list or table with no bound

> osy lint's ui-unbounded-list-does-not-page (SHOULD) fires when a page draws a list with foreach from a query that reads every row of an entity with no Take, and nothing pages it. A page query is sent about 2 MB of rows per answer, so past one answer's rows the list ends and looks complete. Page it — LoadMoreButton under the list, or a DataGrid, which pages by itself — or choose the bound: OrderBy plus Take for the rows the page is for.

<!-- id: ui-unbounded-lists · area: ui · stability: stable · html: https://osysharp.com/reference/ui/unbounded-lists/ -->

## Summary        {#summary}

`ui-unbounded-list-does-not-page` is a **SHOULD** finding: a page draws a list from a read of **every** row of an
entity, and nothing on the page pages it. A page query is sent about 2 MB of rows an answer ([How many rows a page is sent](https://osysharp.com/reference/ui/read-limits/)) —
about a thousand whole rows of a wide entity, several thousand of a narrow one — so once the table passes that the list
stops where the answer does — and nothing on the page says so. The finding quotes the number for your entity.

Page it — `LoadMoreButton(rows)` under the list offers the rest ([Lists that load more](https://osysharp.com/reference/ui/load-more/)):

```osy title="every job, and the rest one press away" test app=ui-unbounded-lists
[Route("/jobs/all")] [Render(CSR)] [AllowAnonymous]
component AllJobsPage() {
  live var jobs = Job.OrderByDescending(j => j.PostedAt).ToList();

  render {
    Stack(gap: 2, p: 6) {
      foreach (var j in jobs) { Text(j.Title); }
      LoadMoreButton(jobs);
    }
  }
}
```

…or write the bound the page means:

```osy title="the open rows, newest first, fifty of them — and a search for the rest" test app=ui-unbounded-lists
entity Job {
  [Required, MaxLength(120)] string Title;
  DateTime PostedAt;
  bool Filled = false;
  security { allow read, create, update when IsAnonymous || IsAuthenticated; }
}

[Route("/jobs")] [Render(CSR)] [AllowAnonymous]
component JobsPage() {
  string search = "";
  live var jobs = Job.Where(j => !j.Filled && j.Title.Contains(search))
                     .OrderByDescending(j => j.PostedAt).Take(50).ToList();

  render {
    Stack(gap: 2, p: 6) {
      SearchField("Search jobs", value: search);
      foreach (var j in jobs) { Text(j.Title); }
    }
  }
}
```

## Signature      {#signature}

```osy syntax
live var rows = Entity.Where(…).OrderBy(x => …).Take(n).ToList();   // what the rule asks for
live var rows = Entity.ToList();                                      // what it flags, when drawn as a list
```

## Description    {#description}

### When does it fire?   {#fires}

On a component field whose read is rooted at an entity, has no `Take`, and is drawn as a list: the source of a
`foreach` in the render, or a table control's rows. A member derived from it counts as the read itself, so
`var shown = rows.Where(…)` drawn in a `foreach` (or a `DataGrid`) is found through `shown` — a list computed from the
read is not a window of anything, so nothing can page it.

### When is it silent?   {#silent}

When the page **pages** the read: it is drawn by a `DataGrid` or under a `LoadMoreButton`, which offer the rows past
the first answer by themselves, or the component asks it `.HasMore` / `.LoadMore()` itself ([Lists that load more](https://osysharp.com/reference/ui/load-more/)). Or when
the read is bounded by what the author wrote: a `Take`; a one-row read (`First`, `Single`, a filter on the id or
on a `[Unique]` column) or a few named keys (`p.Slug == a || p.Slug == b`, `slugs.Contains(p.Slug)`); an aggregate
(`Count`, `Sum`, a `GroupBy`); or a **child collection** — a filter pinning a reference to one parent
(`Line.Where(l => l.Order == order)`, through a reference the row inherits too), to the parents the page already
holds (`shownIds.Contains(l.Order.Id)`), or a parent's collection (`order.Lines`). A chart, a map or a dropdown over
the read reports the more specific rule instead ([A chart over every row](https://osysharp.com/reference/ui/chart-aggregates/), [A picker over every row — the SearchPicker](https://osysharp.com/reference/ui/search-pickers/)).

### What about an editor?   {#editors}

A page that **adds rows** of the entity it lists — `new Tier { … }` in an action, or in a method of the component's own,
saved later — is an editor, and for an editor a `Take` is the wrong advice: an unsaved row joins a list only while the
list is every row of its type, so a windowed list would hide the row just added until Save. Such a list is reported by
[A page query with no bound](https://osysharp.com/reference/ui/unbounded-page-queries/) at its CONSIDER tier instead, with the editor's choices: say that the set is a handful by
construction, edit it a parent at a time (a parent's collection takes a new child too), or commit each row as it is
made and bound the list.

### What can it not tell?   {#limits}

Whether the table can grow. A list of a few dozen reference rows is flagged too; a `.Take(n)` naming its size says
what the page means and costs nothing. Nor how many children a parent has: a pinned read is trusted as bounded. Nor
whether an editor's row is made in a server function the page calls — that row is saved where it is made, so the
page's list takes it from its re-read, and the advice for a list stands.

### Why SHOULD and not MUST?   {#tier}

Because the page works — for the rows the first answer holds. The finding is a list that quietly stops being complete as the data
grows, which a pre-ship gate should not fail on, and which an author should fix before the data gets there.

## See also        {#see-also}

- [How many rows a page is sent](https://osysharp.com/reference/ui/read-limits/) — what a page is sent, and what is never capped
- [A page query with no bound](https://osysharp.com/reference/ui/unbounded-page-queries/) — the catch-all for any other unbounded page query
