Osy#betaRun it on Osyrin: osyrin.com

Reference / UI

A list or table with no bound

live var rows = Entity.OrderBy(x => …).Take(n).ToList(); // not Entity.ToList()

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.

stable2 examples compiled by CIuilintquerylimits

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) — 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):

[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:

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#

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#

When does it fire?#

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

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). 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, A picker over every row — the SearchPicker).

What about an editor?#

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

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

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#

Related

How many rows a page is sent

A page's query is sent bounded. Its own .Take(n) is the ceiling: the browser may ask for fewer of those rows, never…

Lists that load more

A page's list over a big table holds the first answer's rows (about 2 MB of them). rows.HasMore says whether the set…

A page query with no bound

osy lint's ui-page-query-without-a-take (CONSIDER) is the catch-all: a page query reads every row of an entity with no…

Acting on data — a calm grid header, filters as chips, and a toolbar that follows the selection

A DataGrid reads calmly: its per-column filter row is shut until somebody asks to filter (the Filter toggle, or a…

OrderBy / ThenBy

Sort a query by one key or several. `OrderBy`/`OrderByDescending` start the sort, `ThenBy`/`ThenByDescending` add…