Summary#
Write the read where you need the answer.
action Refresh() {
var rows = Order.Where(o => o.Code == code);
found = rows.Count();
}The read happens on the server, when the click happens. The body waits for it and carries on — the same way it waits for a server function it calls.
Signature#
action <Name>(…) {
var <rows> = <Entity>.Where(…).OrderBy(…); // the rows
var <one> = <Entity>.SingleOrDefault(…); // one row, or null
var <n> = <Entity>.Count(…); // how many
var <any> = <Entity>.Any(…); // whether any
var <total> = <Entity>.Sum(x => x.<Column>); // a computed value
}The same is true in a method, an on mount, an on change and every other imperative member. A render
expression is a different question — see component.
Description#
Why this is not the same as a page-load query#
A component member is a binding: it is re-evaluated as the page renders, so declaring one makes you choose what
should happen when its inputs change — var fetches once and can go stale, live var re-reads and pays a round trip
each time. That choice is real, and it is why a read cannot simply be hoisted for you.
A body is not a binding. It runs once, at the moment the user triggers it. There is no "when should this happen again?", nothing can go stale, and there is no decision for anyone to take. So the read simply runs, there, then.
That distinction is the whole feature, and it decides where to put a read:
| you want | write it |
|---|---|
| something the page SHOWS | a member — var for a snapshot, live var to follow changes |
| something an action NEEDS in order to act | the read, in the action |
Putting an action's read on a member is not merely more code. The member is fetched with the page — before the user did the thing that made them want the answer — so the action would act on a picture from earlier.
What it costs#
A round trip, at that point in the body. That is what you asked for by writing it there, and an action is already a multi-request thing. Two reads in a body are two round trips, in order; if you need them together, ask once.
Only VALUES cross to the read#
The read runs on the server. Values you hold travel to it — a local, a component member, a route parameter, the id of a row you are showing:
action Look(Customer c) {
var theirs = Order.Where(o => o.Customer.Id == c.Id); // ✓ a Guid crosses
}A whole row does not travel. Comparing against one is refused, and the compiler names the row and shows you the id form:
action Look(Customer c) {
var theirs = Order.Where(o => o.Customer == c); // ✗ refused: `c` is a row
}This is the same rule everywhere in the language, said once: a row on the client is its identity, and its fields live in the store. Nothing about it is special to a body.
What comes back#
Rows come back as rows: read their fields, loop them, filter them further in memory.
action Look() {
var rows = Order.Where(o => o.Total > 100);
var big = rows.Where(o => o.Priority); // in memory, no second round trip
foreach (var o in rows) { Log.Information("{Code}", o.Code); }
}They also land in the page's own store, so a row you then edit is the row the page is already showing — not a detached copy of it.
Security is not a question here#
The read goes out under your own principal and comes back through the same secured read every other query uses. A body cannot ask for more than the page could, and there is nothing to check or arrange: see security { }.
Examples#
Look something up on a click and show the answer:
entity Note {
[Required, MaxLength(80)] string Title;
int Rank;
security { allow read when IsAnonymous || IsAuthenticated; }
}
[Page("/look")]
[Render(CSR)]
[AllowAnonymous]
component LookPage() {
int total = -1;
action Look() {
var rows = Note.Where(n => n.Rank > 1);
total = rows.Count();
}
render {
Stack(gap: 2) {
Text("total:" + total);
Button("Look", onPress: Look);
}
}
}Key the read off something the page holds — the value is read at the click, not at page load:
[Page("/search")]
[Render(CSR)]
[AllowAnonymous]
component SearchPage() {
string wanted = "";
string found = "-";
action Search() {
var one = Note.SingleOrDefault(n => n.Title == wanted);
found = one == null ? "none" : one.Title;
}
render {
Stack(gap: 2) {
Input(value: wanted, placeholder: "Title");
Text("found:" + found);
Button("Search", onPress: Search);
}
}
}Ask a question rather than fetching rows to count them:
[Page("/tally")]
[Render(CSR)]
[AllowAnonymous]
component TallyPage() {
int total = 0;
string state = "-";
action Tally() {
total = Note.Sum(n => n.Rank);
state = Note.Any(n => n.Rank > 2) ? "has-high" : "all-low";
}
render {
Stack(gap: 2) {
Text("total:" + total);
Text("state:" + state);
Button("Tally", onPress: Tally);
}
}
}See also#
- component — declaring a component, and its
var/live varmembers: the read a page SHOWS, and the choice that comes with it - Session.CurrentUser — the one read that is fetched with the page, because it has no inputs that can change
- creating & saving data — writing in an action, and when the write is committed
- security { } — who may read what, declared once on the entity