Previously, in Part 8, we gave every screen
an address and built PatientDetails.razor at /patients/{id:int}.
Today we cure a quiet illness that's been spreading through ClinicApp: two pages, each
hoarding its own copy of the truth.
The pain: two pages, two truths
Try this. Open /patients, use the form from Part 7 to add a new patient — say,
Meera. She appears in the list. Now click through to her details page. Nothing. No patient
found. Why? Because Patients.razor and PatientDetails.razor each
carry their own List<Patient>, typed out separately. You changed
one list, and the other never heard about it.
Copies drift. That's not a Blazor quirk — it's a law of software. The fix is as old as it is satisfying: keep the data in one place, and let every page ask that one place for it.
The fix: one PatientService
That one place will be a plain C# class called PatientService — a
service, meaning a class whose job is to do work for other parts of the app
rather than draw anything on screen. Create PatientService.cs in the project
root:
public class PatientService
{
private List<Patient> patients = new()
{
new Patient { Id = 1, Name = "Asha Rao", Age = 34 },
new Patient { Id = 2, Name = "Vikram Shah", Age = 52 },
new Patient { Id = 3, Name = "Meera Iyer", Age = 8 },
};
public IReadOnlyList<Patient> GetAll() => patients;
public Patient? Find(int id) => patients.FirstOrDefault(p => p.Id == id);
public void Add(Patient p)
{
p.Id = patients.Count == 0 ? 1 : patients.Max(x => x.Id) + 1;
patients.Add(p);
}
}
Look at what moved in here: the seeded list, the Id-assigning logic from Part 7's
AddPatient, and the lookup from Part 8. Notice also that GetAll
hands out an IReadOnlyList<Patient> — pages may look, but only
Add gets to change the list. One doorway in means one place to enforce the
rules.
Register it in Program.cs
Now tell the app this service exists. In Program.cs, alongside the other
builder.Services lines, add:
builder.Services.AddSingleton<PatientService>();
Singleton means "make exactly one PatientService and hand that
same one to everybody who asks." One instance, one list, one truth — which is precisely the
medicine our symptom called for.
Refactor the pages
Time to put the service to work. In Patients.razor, the change is deleting the
hard-coded list and injecting the service instead. Before:
@code {
private List<Patient> patients = new()
{
new Patient { Id = 1, Name = "Asha Rao", Age = 34 },
// ...and friends, hard-coded
};
}
After:
@page "/patients"
@rendermode InteractiveServer
@inject PatientService Patients
@foreach (var p in Patients.GetAll())
{
<PatientCard Patient="p" OnSelected="GoToDetails" />
}
@code {
private void AddPatient()
{
Patients.Add(newPatient);
newPatient = new Patient();
}
}
The star is @inject PatientService Patients: "give me the app's
PatientService, and let me call it Patients." Note how
AddPatient shrank — the Id logic lives in the service now, so the page just says
Patients.Add(newPatient). Then PatientDetails.razor gets the same
treatment, and its private list disappears entirely:
@page "/patients/{Id:int}"
@inject PatientService Patients
@code {
[Parameter]
public int Id { get; set; }
private Patient? patient => Patients.Find(Id);
}
Run the app, add a patient, click through to the details page — and there they are. Both pages now read from the same list, because there is only one list.
What just happened? Dependency injection
You've just used dependency injection (DI), and here's the everyday version
of it. A chef in a restaurant kitchen doesn't bring their own pans to work — the kitchen hands
them whatever they need, and every chef ends up cooking with the same well-maintained
equipment. In our app, Program.cs is the kitchen's inventory list ("we stock one
shared PatientService"), @inject is the chef calling out for a pan,
and Blazor is the kitchen porter who fetches it. The page never constructs the service, never
wonders where it came from, never accidentally cooks with a different one. It just asks, and
receives.
The three lifetimes
When you register a service, you also choose how long each instance lives. There are three options, and in plain words they are:
| Lifetime | You register with | What you get |
|---|---|---|
| Singleton | AddSingleton |
One instance for the whole app — everyone shares it. |
| Scoped | AddScoped |
One per user session — in Blazor Server, that's one per circuit (a user's live connection). |
| Transient | AddTransient |
A fresh instance every single time anyone asks. |
Note: Our singleton keeps its patients in memory, which comes with two honest caveats. First, the list resets every time the app restarts — Meera vanishes with it. Second, one instance for the whole app means the data is shared by all visitors: if a stranger opened your site right now, they'd see the patients you added. That's perfectly fine for learning, and exactly why real apps store data in a database — which is coming later in the series.
Up next
ClinicApp finally has a single source of truth, and our pages are leaner for it. But a new question is hiding in that refactor: when does a page actually fetch its data? The moment it's created? Every render? What about data that takes time to arrive? Blazor has precise answers — a sequence of moments in every component's life called the lifecycle — and that's where we're headed in Part 10.