Route templates
Every template shape Brouter supports, with live examples. The grammar is a superset-compatible
match of the built-in Blazor router - templates you'd write in @page work here,
with the same matching semantics.
Literals, parameters & type constraints
Literal segments match their exact text (case-insensitive by default). {name}
binds a segment to a parameter; adding a type constraint both restricts what matches
and converts the bound value to that CLR type. Built-in type constraints:
int, long, bool, guid, datetime,
decimal, double, float.
@* /counter/1234 matches; /counter/abc does not - and init arrives as an int. *@ <Broute Path="/counter/{init:int}" ... /> @* Several constraints can chain; each must accept the value. *@ <Broute Path="/counter/multi/{id:int:long}/{age:long:decimal:double}/{name}" ... />
Optional parameters - trailing and middle
A trailing {param?} may be omitted by the URL - the parameter binds
null. An optional may also sit in the middle of a template
(/products/{culture?}/list): it parses fine, but - exactly like the built-in
router - a middle optional is required at match time. Only the trailing run
of optional segments can actually be left out.
@* Trailing optional: both /profile and /profile/saleh match. *@ <Broute Path="/profile/{username?}" ... /> @* Middle optional: /products/en/list matches (culture = "en"), but /products/list does NOT - the literal "list" that follows makes {culture?} effectively required. Framework parity. *@ <Broute Path="/products/{culture?}/list" ... /> @* A middle optional can carry constraints too: *@ <Broute Path="/{lang:nonfile?}/welcome" ... />
Default values
{param=value} makes the segment omittable like an optional, but instead of
null the parameter binds the declared default. Combine with a type constraint
and the default converts too ({id:int=5} binds int 5). A parameter
can't be optional and defaulted ({id=5?} is rejected at parse time).
@* /blog binds action = "Index"; /blog/archive binds action = "archive". *@ <Broute Path="/blog/{action=Index}" ... />
Catch-alls & wildcards
A catch-all parameter - {*path} or {**path}, both equivalent for
matching - must be the last segment and binds the entire remaining path (zero or more
segments, slashes included). Catch-alls accept constraints: the constraint validates the
whole remainder, and the bound value always stays a string. nonfile is the
classic use - route everything except static-asset-looking URLs. Literal wildcards
* (one segment) and ** (rest) match without binding anything.
@* Binds the whole tail: /posts/2026/05/intro → slug = "2026/05/intro". *@ <Broute Path="/posts/{**slug}" ... /> @* Single-star form + a constraint. Matches /assets and /assets/img/logo, but NOT /assets/app.js - "app.js" looks like a file name. *@ <Broute Path="/assets/{*path:nonfile}" ... /> @* Literal wildcards - match one / all segments, bind nothing: *@ <Broute Path="/*/test" ... />
Complex (multi-part) segments
One URL segment can mix literals and several parameters: {name}.{ext},
v{major}-{minor}. Matching runs right-to-left on the segment text (the same
algorithm as the built-in router), every parameter must capture at least one character, and
two parameters can't touch without a literal between them. The last part may be optional
when preceded by a period - {name}.{ext?} matches report.pdf
and report, but never report..
@* /files/report.pdf → name = "report", ext = "pdf" /files/report → name = "report", ext = null /files/a.b.c → name = "a.b", ext = "c" (rightmost '.' wins) *@ <Broute Path="/files/{name}.{ext?}" ... /> @* Parts can be typed: /api/v2-10 → major = 2, minor = 10 (both int). *@ <Broute Path="/api/v{major:int}-{minor:int}" ... />
Validation constraints & chaining rules
Beyond the type constraints there are validation constraints - they accept or reject
but leave the value a string: alpha, file, nonfile,
and the parameterized min(1), max(10), range(1,10),
minlength(2), maxlength(8), length(4) /
length(2,8), regex(...). In a chain the last type
constraint decides the bound value: {score:int:min(1):max(5)} still
binds an int - the validators just gate it.
@* Only 1..5 pass, and score arrives as an int, not a string. *@ <Broute Path="/rate/{score:int:min(1):max(5)}" ... />
Which route wins? (specificity)
When several templates match one URL the most specific wins: literal beats complex segment beats constrained parameter beats parameter beats single-segment wildcard beats catch-all; more constraints rank higher, optional/defaulted parameters slightly lower. Exact-duplicate templates are rejected at registration instead of silently picking one.
/rate/3- the constrained/rate/{score:int:min(1):max(5)}outranks the/c/{kind}/{value}fallback used by the constraint tester./anything/test- the wildcard/*/testonly wins because nothing more specific matches.
Small parity details
~/products- a leading~/is trimmed, same as the built-in router.{{and}}escape literal braces: the template/docs/{{literal}}matches the URL path segment{literal}.- A catch-all can't be optional (
{*path?}throws) and an optional can't have a default ({id=5?}throws) - both rejected at parse time, matching the framework. - Matching is case-insensitive (configurable via
BrouterOptions.CaseSensitive); trailing-slash strictness viaBrouterOptions.IgnoreTrailingSlash.