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:

Service lifetimes in plain words
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.