Skip to content

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 /*/test only 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 via BrouterOptions.IgnoreTrailingSlash.