Summary#
A route parameter arrives as a string. An entity's Id is a Guid. Guid.Parse is the call between them:
entity Doc {
[MaxLength(120)] string Title;
}
[Server, AllowAnonymous] Doc Fetch(Guid id) { return Doc.Single(d => d.Id == id); }
[Page("/doc/{id}"), AllowAnonymous]
component DocPage(string id) {
render { Button("Load", onPress: Load); }
action Load() { var doc = Fetch(Guid.Parse(id)); }
}⚠ You can often skip it entirely — declare the route parameter as a Guid and the platform parses it for you
before your component ever runs. Reach for Guid.Parse when the text comes from somewhere the platform has not
already typed: a form field, a value you built, a string you were handed.
Signature#
Guid Guid.Empty // the all-zero id — a static VALUE, written without parentheses
Guid Guid.NewGuid() // a fresh random id
Guid Guid.Parse(string s) // the string as a Guid — REFUSES text that is not oneDescription#
Guid.Parse refuses, it does not coerce#
Guid.Parse("nope") fails the call. It does not answer Guid.Empty, and that is the whole point: an all-zero id
silently substituted for a mistyped URL would be a confident lookup of a row that does not exist, which is a wrong
answer rather than a missing one. This is C#'s contract for Parse, and Osy# keeps it.
There is no Convert.ToGuid. The coercing Convert.To* family exists for the numeric types, where a blank form
field genuinely means zero; there is no value a Guid can fall back to that means "blank".
Which id formats does Guid.Parse accept?#
Thirty-two hex digits, with or without the four dashes, in either letter case, with surrounding whitespace trimmed. The answer is always the canonical lowercase dashed form, so a parsed id and one the server sent are the same string:
Guid.Parse("0f8fad5b-d9cb-469f-a165-70867728950e") // ✅ the canonical form
Guid.Parse("0F8FAD5B-D9CB-469F-A165-70867728950E") // ✅ → lowercased
Guid.Parse("0f8fad5bd9cb469fa16570867728950e") // ✅ bare → comes back DASHED
Guid.Parse(" 0f8fad5b-…-70867728950e ") // ✅ trimmed
Guid.Parse("{0f8fad5b-…-70867728950e}") // ❌ braces are not accepted
Guid.Parse("(0f8fad5b-…-70867728950e)") // ❌ nor parentheses
Guid.Parse("") // ❌ empty is not Guid.Empty⚠ This is narrower than .NET's Guid.Parse, on purpose. .NET also takes the brace, parenthesis and
{0x…,0x…,{0x…}} forms. Osy# runs your code on several engines — the server, the browser, and compiled JavaScript —
and the accepted set has to be identical on all of them or the same string parses in one place and fails in
another. A set that large cannot be matched honestly across all of them, so the contract is the part every engine
can meet exactly. Nothing the platform produces is ever in one of the refused forms.
A string never becomes a Guid on its own#
void Load(Guid id) { … }
Load(routeParam); // ❌ refused — cannot pass 'string' to a parameter of type 'Guid'
Load(Guid.Parse(routeParam)); // ✅The same holds in every position — a return, a field write, an array element, a class initializer — and in the
other direction too: a Guid where a string is wanted needs .ToString() or an interpolation ($"{id}").
⚠ A comparison inside a query is a different rule and still works. Doc.Single(d => d.Id == someString) resolves
— the query engine compares the underlying values — so the shape a page is built from is unaffected.
Guid.TryParse — write it as you would in C##
Guid.TryParse(s, out var id) compiles. Osy# has no out parameter of its own — a call here can suspend and resume
elsewhere, so no caller frame is guaranteed to still be waiting for a write-back — but the compiler recognises this
one shape and rewrites it into the lines it means, ahead of the statement that holds it:
if (Guid.TryParse(text, out var id)) { … }
Guid? id = null; // ← the rewrite
try { id = Guid.Parse(text); } catch { }
if (id != null) { … }So id is a Guid?, in scope for the rest of the block exactly as C# scopes an out var.
⚠ Two positions are refused, and each says so with the form that works there: a loop CONDITION (the parse would
run once instead of per iteration — test inside the body and break) and a lambda body (it belongs to the element —
put it in a function and call that). out into a variable that ALREADY exists is also refused: C# writes
default(T) there on failure and this language has no spelling for the zero of every type.
Where the text should already be valid, Guid.Parse on its own is the honest call — let the failure be a failure.
Where a route parameter may be junk, declare it a Guid and let the platform reject the request before your code
runs.
Where these run#
All three run in the browser — no round trip. Guid.NewGuid() is deliberately non-deterministic and is memoized
across a durable resume, so a re-entered function does not mint a second id after handing out the first.
Guid.Parse has no SQL push-down: using it inside a query predicate over a database column is refused at compile
time rather than translated, because Postgres accepts text this contract does not.
Examples#
entity Invite {
Guid Token;
[MaxLength(60)] string Label;
}
Invite Open(string label) {
return new Invite { Token = Guid.NewGuid(), Label = label };
}
string Show(Guid token) { return token.ToString(); }
Invite Redeem(string token) {
var t = Guid.Parse(token);
return Invite.Single(i => i.Token == t);
}See also#
- The standard library — and where each call runs — the whole standard library, and where each call runs
- component — a routed component and its parameters, including typing one as a
Guidso no parse is needed - entity members — the properties an entity has,
Idincluded