Summary#
The standard library is the set of pure calls available in any function body — text, numbers, dates, encoding,
hashing. No use, no capability, no ceremony.
The thing worth knowing about it is where each call runs. Osy# decides that for you: a function whose body the browser can execute runs in the browser, and never makes a network call. 126 of the 155 stdlib methods can. So
var label = Text.Upper(name); // runs in the browser
var price = Convert.ToString(total, "F2"); // runs in the browser
var due = Date.AddDays(start, 30); // runs in the browser…are all free. You do not opt in, and there is no annotation. Write the code; the compiler works out where it can run.
Where each call runs#
The default is the client, and the goal is that it always is. A method is kept on the server only when running it in the browser would give a different or unsafe answer — never because it was hard to port. There is one reason that is a true floor, and a shrinking frontier of methods on their way to the client.
1 — It needs a secret the browser must never hold#
This is the floor — a handful of Security.* and Crypto.* operations, and the only ones that can never move. The
client is the user's own machine, so the JWT signing key, the KMS encryption key, an HMAC key and a password
hash's cost/salt are precisely the things it must not see. Running these in the browser would not be slow — it would
either leak the secret or let the client forge authority (a client-minted token is self-issued). The round
trip is the security boundary, not a performance cost.
Security.HashPassword(pw) // server — the cost factor and the salt RNG are the host's
Security.IssueJwt(claims) // server — signed with the host's key; a client could forge one
Crypto.Encrypt(secret) // server — the KMS key never reaches the browserHost-authoritative randomness sits here too: Security.RandomId / RandomHex must come from the host's RNG, or an
"unguessable" id is guessable.
The clock is not one of these, and it is worth saying so directly because it looks like it should be. DurableClock.Now,
DurableClock.UtcNow and DateTime.UtcNow run on the client. A DateTime here is a UTC instant, not a wall-clock
reading, so a browser and the server name the same value — 9am in Frankfurt is 5pm in Tokyo, the same instant — and
reading it in the browser costs no round trip. (Test pinning still works, and a resumed durable function still sees a
consistent instant, because this engine resumes from a saved point rather than re-running your function from the top.)
var now = DurableClock.Now; // client — a UTC instant, read in the browser
var due = Date.AddDays(order.Placed, 30); // client — pure arithmetic on a value in hand
var year = Date.Year(order.Placed); // client2 — The browser cannot yet reproduce it faithfully#
This has shrunk to essentially one case, and it is a specific fidelity gap, not a law of nature: a Double handed to
Convert.ToString(value, format) still renders on the server, because .NET produces its digits from the value's exact
binary expansion. (Decimal — this platform's money type, and what a format specifier is almost always for — already
formats in the browser.)
Everything that used to sit here has moved to the client: WebUtility.HtmlDecode now ports .NET's own algorithm and
entity table (not the browser's, which decodes a different set — it accepts © without the semicolon where .NET
does not); WebUtility.HtmlEncode ships its five-character escape set; and the unkeyed hashes Crypto.Sha256Hex /
Crypto.Md5Hex run in the browser too, since neither holds a secret.
Which two calls decide per call site?#
Two methods are not client-or-server by name — the arguments decide, so the same method may run in the browser at one call site and on the server at the next.
Convert.ToString(value, format) — and the $"{total:F2}" that lowers onto it. It runs in the browser when the
format is a literal — either a standard specifier (F2, N0, C, P1) or a custom digit pattern
("#,##0.00", "0.00", "00000") — over a Decimal or Int32. A format built at runtime, or a Double, still
goes to the server.
var a = Convert.ToString(total, "F2"); // client — literal, standard, decimal
var b = Convert.ToString(total, "#,##0.00"); // client — the compiler parses the pattern into a plan and ships it
var c = Convert.ToString(total, fmt); // server — a runtime format string the compiler cannot seeA literal custom pattern (b) is a compile-time constant, so the compiler parses it once into a plan — how many
digits, whether to group — and ships the plan; the browser executes arithmetic, never a parser (the same trick regular
expressions already use). A pattern that reaches past the digit-placeholder core — section separators, scaling,
percent, scientific, embedded literals — stays on the server, where .NET renders it correctly.
A v1 boundary worth stating plainly: client-side custom-pattern formatting works only for a compile-time literal
pattern, because the mechanism is the compiler parsing it into a plan. A runtime-built format string (c — where
the format is a variable) the compiler cannot see, so it stays on the server; today translating it would mean shipping
a second .NET-format parser to every browser. That is a v1 limitation, not a permanent one: a future version could ship
a small client-side format interpreter so a runtime-built pattern also runs in the browser. Until then, c is a
round trip and returns the right string.
JsonSerializer.Serialize(value) runs in the browser for a scalar or a list of scalars. Hand it a class or an
entity and it goes to the server: naming and ordering the members requires the model, which the browser does not hold.
Guessing at a member order would produce JSON that parses and is wrong.
Does this change my code?#
No. Where a call runs never changes its answer — the client arms are pinned against the real server by an oracle
suite, down to the details C# gets opinionated about (Math.Round(2.5) is 3, away from zero, on both sides;
Uri.EscapeDataString escapes !'()*, which encodeURIComponent does not).
It changes only how the app feels. A form that formats a price, validates a pattern and computes a due date now does all of it without touching the network. And if you write a function that mixes a client-runnable call with a server-only one, the function simply runs on the server — the answer is still right, it just costs a round trip.
Here is the surface actually running — and the two answers C# has an opinion about, pinned. The client arms are held to exactly these, so the assertions below are true wherever the function ends up running:
// Every call in this body is client-runnable, so the whole function runs in the browser. No annotation, no opt-in.
string PriceLabel(decimal total) {
return Text.Upper("total") + ": " + Convert.ToString(total, "F2");
}[Test]
void The_stdlib_gives_the_same_answer_wherever_it_runs() {
Assert.Equal("TOTAL: 12.50", PriceLabel(12.5m)); // two decimals, not "12.5"
Assert.Equal("ACME", Text.Upper("acme"));
// AWAY FROM ZERO, not banker's rounding — `Math.Round(2.5)` is 3. A client that used JS's Math.round
// would agree here and disagree on 3.5, which is how a half-cent walks into an invoice.
Assert.Equal(3m, Math.Round(2.5m));
Assert.Equal(4m, Math.Round(3.5m));
// A literal custom pattern — the money-grid case — runs in the browser too: the compiler parses "#,##0.00"
// into a plan and ships it. Grouped, two decimals, rounded away from zero on the exact digits.
Assert.Equal("1,234.57", Convert.ToString(1234.5678m, "#,##0.00"));
// The deterministic date surface: pure arithmetic on a value already in hand. Runs in the browser.
var due = Date.AddDays(DateTime.Parse("2026-01-01"), 30);
Assert.Equal(31, Date.Day(due));
Assert.Equal(2026, Date.Year(due));
}Every method, and where it runs#
Generated from the compiler's own allowlist and checked on every build — if a method moves, is added, or is removed, this table fails the build until it is right. It cannot go stale.
103 run on the client · 9 are server-only · 2 decide per call site.
Clock
| Method | Runs on | Why not the client |
|---|---|---|
Backoff.Cap | server | bounds a retry policy, which only the durable engine reads |
Backoff.Exponential | server | a retry policy is read only by the durable engine, which schedules the retry — a client holds nothing that acts on one |
Backoff.Fixed | server | a retry policy is read only by the durable engine, which schedules the retry — a client holds nothing that acts on one |
Backoff.Jitter | server | bounds a retry policy, which only the durable engine reads |
Backoff.Linear | server | a retry policy is read only by the durable engine, which schedules the retry — a client holds nothing that acts on one |
Backoff.MaxAttempts | server | bounds a retry policy, which only the durable engine reads |
Convert.AmbientAppField | client | ships as an appField node — the client-safe app identity (Slug / Domain / BaseUrl) from the boot app ambient (the internal carrier of App.Slug/Domain/BaseUrl) |
Convert.AmbientCulture | client | ships as an ambientCulture / cultureFormatAmbient node — the viewer's culture token from the metadata payload (the internal carrier of Session.CurrentCulture) |
Convert.AmbientUserField | client | ships as a currentUserField node — a scalar of the viewer's principal from the boot bag (the internal carrier of Session.CurrentUser fields) |
Convert.FromBase64String | client | — |
Convert.ToBase64String | client | — |
Convert.ToBool | client | — |
Convert.ToDecimal/1 | client | — |
Convert.ToDecimal/2 | conditional | gated per call site — a literal declared culture ships as a cultureParse node; a dynamic culture stays server |
Convert.ToDouble | client | — |
Convert.ToInt/1 | client | — |
Convert.ToInt/2 | conditional | gated per call site — a literal declared culture ships as a cultureParse (int profile) node; a dynamic culture stays server |
Convert.ToInt64 | client | — |
Convert.ToString/1 | client | — |
Convert.ToString/2 | conditional | gated per call site — a literal standard specifier or a plannable custom pattern over a Decimal/Int32 ships; a runtime format, a Double, or an unplannable pattern stays server |
Convert.ToString/3 | conditional | gated per call site — a literal supported format + a literal (declared) culture over a Decimal/Int32 ships as a cultureFormat node keyed by token; anything else stays server |
Crypto.Decrypt | server | decryption uses the app's platform-managed KMS key, which the client never holds |
Crypto.Encrypt | server | encryption uses the app's platform-managed KMS key, which the client never holds |
Crypto.FixedTimeEquals | server | a constant-time compare is only meaningful next to the secret it guards |
Crypto.HmacSha256Hex | server | an HMAC is keyed; the key is the host's and must not reach the browser |
Crypto.Md5Hex | client | — |
Crypto.Sha256Hex | client | — |
Date.AddDays | client | — |
Date.AddHours | client | — |
Date.AddMinutes | client | — |
Date.AddMonths | client | — |
Date.AddYears | client | — |
Date.Date | client | — |
Date.Day | client | — |
Date.DayOfWeek | client | — |
Date.Hour | client | — |
Date.Minute | client | — |
Date.Month | client | — |
Date.Second | client | — |
Date.Year | client | — |
DateOnly.FromDateTime | client | — |
DateOnly.New | client | — |
DateOnly.Parse | client | — |
DateTime.MaxValue | client | — |
DateTime.MinValue | client | — |
DateTime.New | client | — |
DateTime.Parse | client | — |
DateTime.ParseExact | conditional | gated per call site — a literal custom yyyy-pattern + a literal declared culture ships as a cultureDateParse node; a standard specifier / 2-digit year / dynamic pattern|culture stays server |
DateTime.Today | client | — |
DateTime.UtcNow | client | — |
DateTimeOffset.UtcNow | client | — |
DurableClock.Now | client | — |
DurableClock.Today | client | — |
DurableClock.UtcNow | client | — |
Enum.Description | client | model-backed; ships as std:Enum.Description, dispatched by the client's dedicated arm |
Enum.Label | client | model-backed; ships as std:Enum.Label, dispatched by the client's dedicated arm over the enum map |
Enum.Name | client | model-backed; ships as std:Enum.Name, dispatched by the client's dedicated arm |
Enumerable.Range | client | — |
File.SignedUrl | server | needs the session's app + the signing key to mint a signed URL — never leaves the server |
File.Url | client | ships as fileUrl — the render evaluator builds the URL in-process |
Guid.Empty | client | — |
Guid.NewGuid | client | — |
Guid.Parse | client | — |
JsonSerializer.Serialize | conditional | gated by CanJsonSerializeOnClient — a scalar or list of scalars ships; a class or entity needs the server's model |
Math.Abs | client | — |
Math.Acos | client | — |
Math.Asin | client | — |
Math.Atan | client | — |
Math.Atan2 | client | takes (y, x) — C#'s order. Atan(y / x) is not the same: it loses the sign and folds two quadrants onto two others |
Math.Ceil | client | — |
Math.Ceiling | client | — |
Math.Clamp | client | — |
Math.Cos | client | radians |
Math.Exp | client | — |
Math.Floor | client | — |
Math.Log | client | — |
Math.Log10 | client | — |
Math.Log2 | client | — |
Math.Max | client | — |
Math.Min | client | — |
Math.Pow | client | — |
Math.Round | client | — |
Math.Sign | client | — |
Math.Sin | client | radians |
Math.Sqrt | client | — |
Math.Tan | client | radians |
Math.Truncate | client | — |
Regex.IsMatch | client | ships as regexIsMatch, with the pattern translated to JS's dialect at compile time |
Regex.Replace | client | ships as regexReplace, with the pattern translated to JS's dialect at compile time |
Regex.Split | client | ships as regexSplit, with the pattern translated to JS's dialect at compile time |
Security.HashPassword | server | password hashing runs where the host's cost factor and salt RNG live |
Security.IssueJwt | server | a JWT is signed with the host's key; a client-minted token would be self-issued authority |
Security.LinkOAuthFromPending | server | it opens a server-sealed pending-oauth token and writes the identity link; the verified subject never crosses to the client |
Security.RandomHex | server | an unguessable value must come from the host's RNG, not the user's machine |
Security.RandomId | server | an unguessable id must come from the host's RNG, not the user's machine |
Security.VerifyPassword | server | password verification runs where the hash does — the client never sees a hash |
Security.VerifyPendingOAuthEmail | server | it opens a server-sealed pending-oauth token; the callback-verified email re-enters only inside the seal, never as a client argument |
Text.ByteSize | client | — |
Text.Capitalize | client | — |
Text.Concat | client | — |
Text.Contains | client | — |
Text.EndsWith | client | — |
Text.IndexOf | client | — |
Text.IsBlank | client | — |
Text.IsEmpty | client | — |
Text.Join | client | — |
Text.LastIndexOf | client | — |
Text.Left | client | — |
Text.Length | client | — |
Text.Like | client | — |
Text.Lower | client | — |
Text.PadEnd | client | — |
Text.PadStart | client | — |
Text.Repeat | client | — |
Text.Replace | client | — |
Text.Reverse | client | — |
Text.Right | client | — |
Text.Split | client | — |
Text.StartsWith | client | — |
Text.Substring | client | — |
Text.TitleCase | client | — |
Text.Trim | client | — |
Text.TrimEnd | client | — |
Text.TrimStart | client | — |
Text.Truncate | client | — |
Text.Upper | client | — |
TimeOnly.FromDateTime | client | — |
TimeOnly.New | client | — |
TimeOnly.Parse | client | — |
TimeSpan.Days | client | — |
TimeSpan.FromDays | client | — |
TimeSpan.FromHours | client | — |
TimeSpan.FromMilliseconds | client | — |
TimeSpan.FromMinutes | client | — |
TimeSpan.FromSeconds | client | — |
TimeSpan.Hours | client | — |
TimeSpan.Minutes | client | — |
TimeSpan.New | client | — |
TimeSpan.Parse | client | — |
TimeSpan.Seconds | client | — |
TimeSpan.TotalDays | client | — |
TimeSpan.TotalHours | client | — |
TimeSpan.TotalMilliseconds | client | — |
TimeSpan.TotalMinutes | client | — |
TimeSpan.TotalSeconds | client | — |
TimeSpan.Zero | client | — |
Uri.EscapeDataString | client | — |
Uri.UnescapeDataString | client | — |
WebUtility.HtmlDecode | client | — |
WebUtility.HtmlEncode | client | — |
Zone.InZone | conditional | ships for a LITERAL declared zone (client has its DST plan); a dynamic zone → server |
Zone.IsDst | conditional | ships for a LITERAL declared zone; a dynamic zone → server |
Zone.New | server | the zone factory is a compile-time literal fold — no runtime work to accelerate |
Zone.OffsetAt | conditional | ships for a LITERAL declared zone; a dynamic zone → server |
Zone.Resolve | conditional | gated per call site — a literal (declared) zone ships as a zoneResolve node; a dynamic zone stays server |
string.Concat | client | — |
string.Join | client | — |
See also#
- <span class="planned" title="this page is planned and not written yet">stdlib-text</span> · Regex · Encoding — Base64, URL, HTML · Uri — the individual surfaces
- Security.* — hashing, verifying, tickets, random ids — the server-only authority surface, and why it is