Summary#
A Query<T> is a query that has not run yet — and it is what a LINQ chain already is, whether or not you spell
it. Writing the type down matters in one place: when the query has to cross a function boundary, where a parameter
needs a type.
entity Film {
[Required, MaxLength(200)] string Title;
int Year;
decimal Rating;
security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}
string BestOfYear(int year) {
Query<Film> candidates = Film.Where(f => f.Year == year); // the filter …
return candidates.OrderByDescending(f => f.Rating).First().Title; // … and the pick, in ONE query
}Signature#
var q = Film.Where(…); // inferred — holds the query, does not run it
Query<Film> q = Film.Where(…); // the same thing, written down
Film Best(Query<Film> q) { … } // a parameter — the caller supplies the query
q.OrderBy(…).Take(…) // a clause JOINS the query
foreach (var f in q) { … } // read as rows: HERE the query runs
q.ToList() // …and this is how you say "run it now" on purposeDescription#
C# spells it IQueryable<T>, and that works too#
If you reach for C#'s name, write it — IQueryable<Film> is Query<Film>, in every position, and there is no
conversion or preference between them:
Film PickBestOf(IQueryable<Film> candidates) { return candidates.OrderBy(f => f.Rating).First(); }⚠ Its SIBLING interfaces are a different answer, and the reason is worth knowing: IEnumerable<T>,
IReadOnlyList<T>, IList<T> are refused, because Osy# spells those T[] and List<T> — which are themselves
C#, so the refusal leaves you writing C# and costs one round trip. IQueryable<T> has no such alternative
spelling, so refusing it would push you off C# onto an Osy#-only word. That is why one is accepted and the others
are taught.
A plain var is the same thing#
You do not have to write the type. var infers it, exactly as var q = db.Orders.Where(…) infers IQueryable in
C#, so a chain split across a binding is still one statement:
```osy title="var and the written type are the same query" test app=query-deferred
// Both are ONE statement: WHERE, ORDER BY and LIMIT 1 together.
string BestInferred(int year) {
var candidates = Film.Where(f => f.Year == year);
return candidates.OrderByDescending(f => f.Rating).First().Title;
}
string BestSpelled(int year) { Query<Film> candidates = Film.Where(f => f.Year == year); return candidates.OrderByDescending(f => f.Rating).First().Title; }
### What ends the deferral — `.ToList()`, and a terminal {#ending}
The chain is deferred until something asks for the answer. Two things do, and they are how you say "read it here":
```osy title="`.ToList()` is `run it now`" test app=query-deferred
// `.ToList()` reads the rows HERE. The sort below runs over the list, in memory — which is what you want when
// you are going to ask the same rows several questions.
string BestThenCount(int year) {
var rows = Film.Where(f => f.Year == year).ToList();
var best = rows.OrderByDescending(f => f.Rating).First().Title;
return best + " of " + rows.Count.ToString();
}A terminal — First, Single, Count, Any, Sum and friends — ends it too, because it has produced the
answer. And a declared row type asks for the rows at the binding: Film[] rows = Film.Where(…);.
Passing a query to a function#
A Query<T> parameter is the half that has no other spelling: the caller decides which rows, the helper decides
what to do with them, and it is still one statement.
string[] TopTitles(Query<Film> src, int take) {
return src.OrderByDescending(f => f.Rating).Take(take).Select(f => f.Title);
}
string[] BestOf(int year) { return TopTitles(Film.Where(f => f.Year == year), 3); }
string[] BestEver() { return TopTitles(Film.Where(f => f.Rating > 0m), 10); }The helper is composed into each call rather than called, so its body must be a single return <query>; (or an
=> <query> expression body). A body that does more than that has no expression to compose, and says so.
Reading the rows#
Used anywhere rows are wanted — a foreach, a return, an argument — a Query<T> is the rows, and that is
where it runs. It is only special at the head of a chain.
int CountOfYear(int year) {
Query<Film> candidates = Film.Where(f => f.Year == year);
return candidates.Count(); // SELECT count(*) … WHERE Year = @year
}
string[] TitlesOfYear(int year) {
Query<Film> candidates = Film.Where(f => f.Year == year);
var titles = new List<string>();
foreach (var f in candidates) { titles.Add(f.Title); } // runs here
return titles.ToArray();
}Using one twice runs it twice#
⚠ This is the one thing to know, and it applies to the inferred var as much as to the written type. A deferred
query is a description, not a result — so each use goes to the database, exactly as enumerating a C# IQueryable
twice is two round trips. When you want the rows once and then several answers from them, say so with
ToList:
string Report(int year) {
var rows = Film.Where(f => f.Year == year).ToList(); // read ONCE …
var howMany = rows.Count(); // … then ask it twice, in memory
var best = rows.Max(f => f.Rating);
return howMany.ToString() + " films, best " + best.ToString();
}What it will not hold#
The initializer has to be a query that has not run. The two ways to get that wrong are different mistakes, and each is named:
| You wrote | Why it is refused |
|---|---|
Query<Film> q = Film.Where(…).First(); | First runs it — what you have is the answer, not the query. Write the terminal where you use it. |
Query<Film> q = rows.Where(…); over a Film[] | those rows have already been read. To hold rows, declare the list type: Film[], List<Film>. |
Query<Film> t = Film.Select(f => f.Title); | the query yields string. The declared element is checked against what the query yields, which a projection changes — write Query<string>. |
Why it is not a value you can store#
A Query<T> is resolved where it is used, at compile time — it is not an object that exists while the program runs,
so it cannot be put in a field, returned from a function, or held in a list. That is a security boundary before
it is an economy: a query becomes SQL, and a query that could travel as a value could travel to a function running
in the browser. Composing in the compiler means nothing new crosses the wire.
To hand rows to something that outlives the expression, materialise them with ToList.
Do the write terminals compose too?#
Yes — a Query<T> may end in .Delete(), .Update(…) or
.Insert(…) exactly as it ends in .Count(): the helper's chain composes into the caller's
statement at compile time, so int PurgeVia(Query<Order> doomed) { return doomed.Delete(); } is still one
statement, with the caller's security floor.
See also#
- Where / Single / Count — the clauses a
Query<T>composes - ToList — reading the rows once, when you want several answers from them
- Skip / Take (paging) —
Skip/Take, the clause most worth composing rather than filtering - LINQ over a local list — the other side: LINQ over rows you already hold