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
- What Blazor is: C# in the browser too
- The two models: Blazor Server versus Blazor WebAssembly
.razorcomponents: basic structure- Razor syntax:
@code,@bind,@onclick - A component's lifecycle:
OnInitializedAsync - Dependency injection in a component:
@inject - Complete example:
Catalog.razorfor BiblioTech - When to choose Blazor over "pure" ASP.NET Core or over WPF
- 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.
- 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.
.razor components: basic structure
.razor components: basic structureA 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.
- Razor syntax:
@code, @bind, @onclick
@code, @bind, @onclickInside the markup, any C# expression is introduced with @:
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).
- A component's lifecycle:
OnInitializedAsync
OnInitializedAsyncA 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.
- Dependency injection in a component:
@inject
@injectA 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 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.
- Complete example:
Catalog.razor for BiblioTech
Catalog.razor for BiblioTechPutting 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.
- 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
Librarydirectly (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 —HttpClientagainst 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; theLendItemmethod still catches it (through theAvailablecheck), 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
@codeblock: 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
-
Add an
<input @bind="searchText" />toCatalog.razorand filter the displayed table so that only items whoseTitlecontains the entered text are shown (useContains, already seen in Module 1, and update the filter with every keystroke via@bind:event="oninput"). -
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.
C# Programming Course
Module 1: Introduction to C#
- Introduction to C#
- Setting Up the Development Environment
- Hello World Program
- Basic Syntax and Structure
- Variables and Data Types
- Arrays and Strings
Module 2: Control Structures
Module 3: Object-Oriented Programming
- Classes and Objects
- Methods
- Constructors and Destructors
- Inheritance
- Polymorphism
- Encapsulation
- Abstraction
- Structs and Records: Value Types and Reference Types
Module 4: Advanced C# Concepts
- Interfaces
- Delegates and Events
- Pattern Matching and Modern C# Features
- Generics
- Collections
- LINQ (Language Integrated Query)
- Asynchronous Programming
Module 5: Working with Data
- File I/O
- Serialization
- Database Connectivity
- Entity Framework
- Working with JSON and Consuming REST APIs
Module 6: Advanced Topics
- Reflection
- Attributes
- Dynamic Programming
- Memory Management and Garbage Collection
- Multithreading and Parallel Programming
Module 7: Building Applications
Module 8: Best Practices and Design Patterns
- Coding Standards and Best Practices
- Design Patterns
- Dependency Injection and Inversion of Control
- Unit Testing
- Code Review and Refactoring
