The previous lesson exposed BiblioTech's domain as an HTTP API with ASP.NET Core, meant to be consumed by any client — including a client written in JavaScript, the traditional choice for building web interfaces. Blazor proposes a different path: building that web interface in C#, reusing the same language and much of the component and data binding ideas already seen in WPF, but running in the context of a browser. This lesson explains Blazor's two execution models (Server and WebAssembly), the syntax of a .razor component, and builds a Catalog.razor component that lists BiblioTech's catalog and injects Library directly as a service, with no manual HTTP requests involved.

Contents

  1. What Blazor is: C# in the browser too
  2. The two models: Blazor Server versus Blazor WebAssembly
  3. .razor components: basic structure
  4. Razor syntax: @code, @bind, @onclick
  5. A component's lifecycle: OnInitializedAsync
  6. Dependency injection in a component: @inject
  7. Complete example: Catalog.razor for BiblioTech
  8. When to choose Blazor over "pure" ASP.NET Core or over WPF

  1. What Blazor is: C# in the browser too

Traditionally, a web application's interface is built with HTML, CSS, and JavaScript, while the server (for example, with ASP.NET Core, previous lesson) simply serves data. Blazor breaks that boundary: it lets you write the web interface in C#, organized into reusable components (.razor files), with no need to write JavaScript for the interface logic.

The most important practical consequence: everything you already know from C# — classes, LINQ, async/await, BiblioTech's own domain — can be used directly in the browser, with no need to translate it into another language or expose it first as a JSON API, as would be needed with a traditional JavaScript frontend.

  1. The two models: Blazor Server versus Blazor WebAssembly

Blazor offers two different ways of running that same C# code, with very different implications for where the logic actually lives:

Blazor Server Blazor WebAssembly (WASM)
Where the C# code runs On the server; the browser only receives interface updates Directly in the browser, compiled to WebAssembly
Communication with the server Persistent connection (SignalR) for every user interaction None needed after the initial load (except for explicit calls to an API)
Interaction latency Depends on the network: every click travels to the server and back No network latency for local logic, it runs in the browser itself
Initial download size Small (the logic isn't downloaded, it stays on the server) Larger (the .NET runtime compiled to WebAssembly has to be downloaded)
Works offline after loading No: every interaction depends on the connection to the server Yes, once loaded (as long as it doesn't depend on an external API)
Direct access to server resources (database, files) Direct, with no intermediate API Requires an HTTP API, like the one from section 3 of the previous lesson

For BiblioTech, Blazor Server fits this lesson's example better: the Catalog.razor component can inject Library directly as a server service (just like an ASP.NET Core endpoint), with no HTTP involved; Blazor WebAssembly, on the other hand, would force it to consume the API built in the previous lesson through HttpClient (Module 5), because the component's C# code would run in the user's browser, with no direct access to the server or its database.

  1. .razor components: basic structure

A Blazor component combines HTML-like markup with C# code, in a single .razor file:

@page "/catalog"

<h3>BiblioTech Catalog</h3>

<ul>
    <li>Hopscotch</li>
    <li>Ficciones</li>
</ul>

@code {
    // the component's C# code: properties, methods, lifecycle (section 5)
}
Part Role
@page "/catalog" Directive that assigns a URL route to this component (as a standalone page)
HTML markup The visual structure, very similar to plain HTML
@code { ... } The component's C# code block: properties, fields, and methods live here

Mixing HTML and C# within the same file is reminiscent of WPF's XAML/code-behind separation, but with a notable difference: in Blazor, the markup and the C# code coexist in a single .razor file, instead of being split between a separate .xaml and .xaml.cs.

  1. Razor syntax: @code, @bind, @onclick

Inside the markup, any C# expression is introduced with @:

<p>Total books: @totalBooks</p>

@code {
    private int totalBooks = 2;
}

For data binding on a form field, @bind connects a control's value to a C# property, similar to WPF's {Binding ...} but with Razor's own syntax:

<input @bind="searchText" />
<p>Searching for: @searchText</p>

@code {
    private string searchText = string.Empty;
}

To handle events, @onclick (and equivalents like @onchange, @onsubmit) connects a DOM event to a C# method, with no need for JavaScript:

<button @onclick="IncrementCounter">Add</button>
<p>Counter: @counter</p>

@code {
    private int counter = 0;

    private void IncrementCounter()
    {
        counter++; // Blazor automatically re-renders the component after the event
    }
}

After any event handled by Blazor (@onclick, @bind...), the framework automatically re-renders the part of the component that changed — conceptually similar to how WPF updates the view when PropertyChanged fires, although Blazor's internal mechanism is different (it compares the markup tree before and after the event, and updates only the differences).

  1. A component's lifecycle: OnInitializedAsync

A Blazor component goes through a series of lifecycle methods, invoked automatically by the framework at specific moments. The most common one for loading data when the component is shown is OnInitializedAsync:

@code {
    private List<LibraryItem> catalog = new();

    protected override async Task OnInitializedAsync()
    {
        // runs once, when the component is shown for the first time
        catalog = await GetCatalogAsync();
    }
}

OnInitializedAsync is the usual place to load initial data — from a database, from an API, or, as in section 7, directly from an injected service — equivalent to how CatalogViewModel's constructor in WPF prepared Catalog before the window was shown. The difference is that this is an async method, meant for awaiting operations that take time (like a database query) without blocking the page's initial load.

  1. Dependency injection in a component: @inject

A Blazor component can receive services registered in the dependency container (the same mechanism seen in the ASP.NET Core lesson) with the @inject directive:

@inject Library Library

<p>Total items: @Library.Catalog.Count</p>

@inject Library Library resolves a Library instance from the services container (registered in Program.cs with builder.Services.AddSingleton<Library>(), just as in the previous lesson) and exposes it as a property available throughout the component, with no need to receive it as a parameter or construct it manually. As noted in the module's introduction, dependency injection as a general mechanism is studied in depth in Module 8; for now it's enough to recognize that @inject is, for a Blazor component, the equivalent of receiving an already-resolved parameter in an ASP.NET Core endpoint.

  1. Complete example: Catalog.razor for BiblioTech

Putting all the previous pieces together, a component that lists the catalog and lets you lend each book with a button, using Blazor Server and injecting Library directly:

@page "/catalog"
@inject Library Library

<h3>BiblioTech Catalog</h3>

@if (statusMessage is not null)
{
    <p><strong>@statusMessage</strong></p>
}

<table>
    <thead>
        <tr>
            <th>Title</th>
            <th>Author</th>
            <th>Available</th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (LibraryItem item in Library.Catalog)
        {
            <tr>
                <td>@item.Title</td>
                <td>@item.Author</td>
                <td>@(item.Available ? "Yes" : "No")</td>
                <td>
                    <button @onclick="() => LendItem(item)" disabled="@(!item.Available)">
                        Lend
                    </button>
                </td>
            </tr>
        }
    </tbody>
</table>

@code {
    private string? statusMessage;

    protected override Task OnInitializedAsync()
    {
        // The catalog already lives in memory inside Library (injected above);
        // nothing extra needs to be loaded here, unlike a scenario with an external API.
        return Task.CompletedTask;
    }

    private void LendItem(LibraryItem item)
    {
        if (!item.Available)
        {
            statusMessage = $"'{item.Title}' is already on loan.";
            return;
        }

        item.Lend(); // domain logic that already exists, Module 2
        statusMessage = $"Loan registered: {item.Title}";
    }
}

@onclick="() => LendItem(item)" uses a lambda expression (Module 4) to pass each row's own item to LendItem, something necessary because @foreach generates one row per element and each button must act on its own item, not on a fixed one. disabled="@(!item.Available)" disables the button when the item is already on loan, with no additional code needed: Razor evaluates the C# expression in parentheses and assigns it to the disabled HTML attribute. Just as in WPF, no part of this component reimplements the availability check: it relies entirely on item.Lend(), which already handles it since Module 2.

  1. When to choose Blazor over "pure" ASP.NET Core or over WPF

Scenario Best-suited option
A pure API, with no visual interface of its own, consumed by different clients ASP.NET Core (Minimal APIs, previous lesson)
A visual interface accessible from any browser, with no installation Blazor
A desktop interface with maximum visual control and no dependency on a network connection WPF
A simple desktop interface, quick to build, for internal use on Windows Windows Forms

Blazor doesn't replace ASP.NET Core: in fact, a Blazor Server project runs on top of ASP.NET Core (it uses Kestrel and the same services container underneath); the difference is that Blazor adds, on top, a component model with a visual interface, whereas the previous lesson's Minimal API stays purely at the data layer, with no interface of its own.

Common Mistakes and Tips

  • Confusing Blazor Server with Blazor WebAssembly when deciding how to access data: in Blazor Server, injecting Library directly (as in section 7) is correct and efficient; in Blazor WebAssembly, the same approach wouldn't work, because the component runs in the client's browser, with no direct access to the server — HttpClient against the previous lesson's API would be needed.
  • Forgetting disabled="@(!item.Available)") or an equivalent check: without it, the user could click "Lend" on an item that's already on loan; the LendItem method still catches it (through the Available check), but disabling the button improves the experience by preventing the attempt in the first place.
  • Writing business logic directly in the Razor markup instead of in a method inside the @code block: this makes the component harder to read and test; the practical rule is the same as in WPF, keep the markup declarative and the logic in the code.
  • Tip: Blazor Server keeps a persistent (SignalR) connection with every connected user; in an application with a great many simultaneous users, this consumes more server resources than Blazor WebAssembly, where each browser runs its own copy of the code — a scalability consideration to keep in mind when choosing the model.

Exercises

  1. Add an <input @bind="searchText" /> to Catalog.razor and filter the displayed table so that only items whose Title contains the entered text are shown (use Contains, already seen in Module 1, and update the filter with every keystroke via @bind:event="oninput").

  2. Add a counter <p>Total available: @Library.Catalog.Count(item => item.Available)</p> above the table, and explain in a comment why no additional code is needed for it to update after every loan.

Solutions

<input @bind="searchText" @bind:event="oninput" placeholder="Search by title..." />

<table>
    <tbody>
        @foreach (LibraryItem item in Library.Catalog.Where(
            i => i.Title.Contains(searchText, StringComparison.OrdinalIgnoreCase)))
        {
            <tr>
                <td>@item.Title</td>
                <td>@item.Author</td>
            </tr>
        }
    </tbody>
</table>

@code {
    private string searchText = string.Empty;
    // ... rest of the existing code ...
}
<p>Total available: @Library.Catalog.Count(item => item.Available)</p>

@* No additional code is needed because Blazor re-renders the entire component after every
   handled event (like LendItem's @onclick); when it re-renders, this expression is
   automatically re-evaluated against Library.Catalog's current state, with no need to
   manually update any variable. *@

Conclusion

In this lesson you've seen how Blazor brings C# to the browser, with two execution models — Server and WebAssembly — that completely change where the logic lives and how data is accessed, .razor components that combine markup and code, @bind/@onclick for interaction, and @inject for obtaining services from the dependency container. The Catalog.razor component reuses, once again, the very same Library.Catalog and item.Lend() seen since Module 2, now reachable from any browser.

The last lesson of this module introduces Xamarin and .NET MAUI, which take BiblioTech to mobile and desktop devices with a single cross-platform project, using a XAML very similar to WPF's — closing out the tour of the main ways to build interfaces in C#, before Module 8 polishes the code of everything built so far with best practices and design patterns.

© Copyright 2026. All rights reserved