Previously, in Part 7, we stopped hard-coding
patients and built a real form with validation on Patients.razor. Today we answer
a question you've probably been quietly asking since Part 1: how does a URL become a page?
Every screen deserves an address
Think about how you find anything on the web. You don't ask a site to "please show the
patients screen" — you go to an address. In Blazor, giving a screen an address takes exactly
one line: the @page directive. Our Patients.razor
has answered at /patients since Part 5 for precisely this reason:
@page "/patients"
This process of matching a URL to a component is called routing, and the
component doing the matching is Blazor's router. When a request for
/patients arrives, the router runs through every @page address it
knows about, finds the match, and renders that component. Simple — until you need one page to
show different content depending on the URL.
Route parameters: one page, many patients
Here's the puzzle. We want /patients/1 to show the first patient and
/patients/2 to show the second. Do we write a page per patient? Obviously not —
we write one page with a blank to fill in. That blank is a
route parameter. Create a new file called PatientDetails.razor:
@page "/patients/{Id:int}"
@code {
[Parameter]
public int Id { get; set; }
}
Two things are happening here. In the address, {Id:int} marks a segment that can
vary — whatever appears after /patients/ gets captured into it. And in the code,
a public property named Id wears the [Parameter] attribute, which is
Blazor's signal that the captured value should be delivered there. Visit
/patients/2 and Id is 2 before the page even renders.
What about the :int part? That's a route constraint — a bouncer
at the door. It tells the router this segment must be a whole number, so
/patients/2 matches but /patients/abc doesn't even get in. Letters
simply won't match, and the router moves on as if this page didn't exist. Here's the whole
journey at a glance:
Finding the patient behind the Id
An Id on its own isn't much of a details page. Let's look the patient up and show
something human:
@page "/patients/{Id:int}"
@if (patient is null)
{
<p>No patient found with id @Id.</p>
}
else
{
<h2>@patient.Name</h2>
<p>Age: @patient.Age</p>
}
@code {
[Parameter]
public int Id { get; set; }
private List<Patient> patients = new()
{
new Patient { Id = 1, Name = "Asha Rao", Age = 34 },
new Patient { Id = 2, Name = "Vikram Shah", Age = 52 },
};
private Patient? patient => patients.FirstOrDefault(p => p.Id == Id);
}
FirstOrDefault returns the matching patient, or null if the
Id doesn't exist — which is why the markup checks for null before
celebrating.
Note: Yes, this page has its own little patient list, separate
from the one on Patients.razor. That's a temporary crutch, and it will start
to hurt the moment you add a patient on one page and the other doesn't notice. Sharing one
list properly between pages is exactly what
Part 9 is about.
Linking between pages
How do users actually get to these addresses? The humble anchor tag works everywhere:
<a href="/patients/2"> takes you straight to Vikram. For navigation menus,
Blazor offers NavLink, which behaves like an anchor but adds an
active CSS class when its address matches the current URL — that's how menus
highlight where you are:
<NavLink href="/patients">Patients</NavLink>
Navigating from code
Sometimes navigation isn't a link — it's a decision your code makes. Remember
OnSelected, the EventCallback<Patient> we gave
PatientCard.razor in Part 6?
Let's make clicking a card jump to that patient's details. Blazor provides
NavigationManager for exactly this; ask for it with @inject at
the top of Patients.razor:
@inject NavigationManager Nav
<PatientCard Patient="p" OnSelected="GoToDetails" />
@code {
private void GoToDetails(Patient p)
{
Nav.NavigateTo($"/patients/{p.Id}");
}
}
Click a card, the callback fires, and NavigateTo builds the URL from the
patient's Id and sails you there. Cards, callbacks, and routes — three parts of
the series clicking together.
When no route matches
What if someone types /bananas? The router checks every address it knows,
finds nothing, and falls back to its not-found content — the "Sorry, there's
nothing at this address" screen you may have already stumbled into. Current Blazor (.NET 10)
handles this nicely out of the box: NavigationManager now has built-in
NotFound() handling, so showing a proper 404 page works cleanly whether your page
is statically rendered or fully interactive. And note the difference from our
null check earlier — /patients/999 does match a route; it's
our own page that discovers the patient doesn't exist.
By the way, routing is only the last leg of a URL's journey — DNS lookups, requests, and responses all happen first. For the bigger picture, see what happens when you type a URL.
Up next
ClinicApp now has two proper pages — but as that note above warned, they each hoard their own
copy of the patient list, and copies drift apart. The fix is one shared
PatientService that every page borrows from, courtesy of something called
dependency injection. It sounds scarier than it is, and it's all in
Part 9.