Blazor
Full-stack C# on the front end -- the Server vs WebAssembly hosting models, render modes, components, data binding, and where Blazor fits
Blazor
Blazor lets you build interactive web UIs in C# instead of JavaScript, which means the models, validation rules, and helper logic you already wrote for the server can be shared with the client rather than re-implemented in a second language. It is a component model in the same family as React and Vue: the unit of composition is a component, components are .razor files that mix HTML-like markup with C#, and a component re-renders when its state changes. If you have written modern front-end code the mental model will feel familiar -- the novelty is that the language underneath is C# and the surface is Razor.
The headline decision when you reach for Blazor is the hosting model -- where the component code actually executes. This is the choice that shapes latency, scaling, offline behaviour, and the size of the initial download, and it is the part of the story that changed shape significantly in .NET 8. Get the hosting model right and Blazor is productive; get it wrong and you fight the framework.
The Hosting Models
Blazor Server runs your components on the server. The browser downloads a tiny bootstrap page and opens a persistent SignalR (WebSocket) connection back to the server; from then on, every UI event -- a click, a keystroke, a form change -- is sent up that connection, handled by C# on the server, which diffs the rendered output and sends back only the DOM changes. The download is tiny, the app loads almost instantly, and you have the full .NET runtime and the whole BCL available server-side with no client-side compromises. The cost is that every interaction is a network round-trip, so the experience is latency-sensitive, and the server holds per-user circuit state in memory for the duration of each session, which becomes a scaling and memory cost as concurrent users grow. For internal line-of-business apps running on a LAN or a low-latency network -- a very common New Zealand enterprise shape -- this trade is often exactly right.
Blazor WebAssembly (WASM) flips it: the .NET runtime and your application's DLLs are downloaded to the browser and run there via WebAssembly. The app executes entirely client-side like a single-page application, it keeps working offline once loaded, and because it is just static files plus a runtime, it scales the way a CDN scales -- there is no per-user server state to hold. The cost is a larger initial download (the runtime plus your assemblies) and the client's CPU and memory doing the work; ahead-of-time (AOT) compilation can improve runtime speed but makes the payload bigger again, so it is a tuning lever rather than a free win.
The .NET 8 unified model and render modes merged these two worlds into one project. Instead of choosing a hosting model for the whole application up front, you choose a render mode per component: static server-side rendering (SSR) for content that does not need interactivity, interactive Server for components that run over SignalR, interactive WebAssembly for components that run in the browser, or Auto -- which starts a component on the server for a fast first load, then transparently switches it to WebAssembly once the runtime has finished downloading in the background. Auto is the modern default story: the user gets Server's instant load and, by their second visit, WASM's offline-capable, no-server-round-trip interactivity. You can mix render modes within one app, rendering a marketing page statically and an admin dashboard interactively.
| Model | Where it runs | First load | Latency | Offline | Scaling |
|---|---|---|---|---|---|
| Blazor Server | Server | Instant | Per-event round-trip | No | Per-circuit memory |
| Blazor WASM | Browser | Slower (download) | Local, no round-trip | Yes | Like static files |
| .NET 8 Auto | Server then browser | Instant | Server first, then local | After switch | Hybrid |
A Component
A component is markup interleaved with C#. The markup is plain HTML until you reach an @ -- which switches into C# -- and a @code block at the bottom holds the component's fields and methods. Here is the canonical counter:
<h3>Counter</h3>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount() => currentCount++;
}The @code block is where the component's state and behaviour live -- it is just a C# class body, and the fields declared there are the component's state. The @currentCount in the markup is an expression that prints the field's current value, and @onclick="IncrementCount" binds the button's click to a C# method rather than a JavaScript handler.
The render model is the part worth internalising: Blazor re-renders a component when its state changes after an event handler runs. You do not manually touch the DOM or call a setState function -- you mutate a field in your event handler, the handler returns, and Blazor re-renders the component, diffs the output against the previous render, and applies the minimal set of changes. Incrementing currentCount and letting the method return is the entire update; the displayed count follows automatically.
Parameters and Data Binding
Components receive data from their parent through properties marked [Parameter], and they bind to form inputs with @bind. A parent passing a Title would write <MyComponent Title="Orders" />; inside the component, the [Parameter] property receives it.
<input @bind="searchTerm" @bind:event="oninput" placeholder="Search..." />
<p>Searching for: @searchTerm</p>
@code {
[Parameter] public string Title { get; set; } = "";
private string searchTerm = "";
}@bind is two-way binding, and it is syntactic sugar: it expands to setting the input's value from the field and wiring a change handler that writes the input's value back into the field. By default that change handler fires on the onchange event -- when the input loses focus -- which is fine for a form you submit but feels laggy for a live search box. Adding @bind:event="oninput" rebinds it to the oninput event so the field updates on every keystroke, which is what makes the Searching for: text track the input in real time.
Dependency Injection and Data
Components are first-class citizens of the same DI container as the rest of ASP.NET Core. You request a service with @inject and the framework resolves it from the container -- the very same registry where you registered services in Program.cs. Loading data belongs in the OnInitializedAsync lifecycle method, which runs once when the component is first set up.
@inject HttpClient Http
<ul>
@foreach (var order in orders)
{
<li>@order.Reference</li>
}
</ul>
@code {
private List<Order> orders = new();
protected override async Task OnInitializedAsync()
{
orders = await Http.GetFromJsonAsync<List<Order>>("api/orders") ?? new();
}
}The most important difference between the hosting models is hiding in that @inject. On Blazor Server, the component runs on the server, so you can inject a DbContext or a domain service and hit the database directly -- there is no HTTP hop because you are already inside the server process. On Blazor WebAssembly, the component runs in the browser, and a browser cannot open a database connection, so you must inject an HttpClient and call an API over HTTP for every piece of data. This is a frequent source of confusion: code that compiles and works perfectly on Server fails on WASM because it tries to inject a service that only exists server-side. Decide the data path with the hosting model in mind, and on Auto components assume the WASM constraint, since the component may end up running in the browser.
Forms and Validation
Blazor's EditForm component binds a form to a model and runs validation against it, and the payoff is that it reuses the same DataAnnotations attributes your server-side models already carry. The validation you wrote once -- [Required], [EmailAddress], [StringLength] -- runs on the client without being rewritten in JavaScript.
<EditForm Model="model" OnValidSubmit="Save">
<DataAnnotationsValidator />
<ValidationSummary />
<InputText @bind-Value="model.Name" />
<ValidationMessage For="() => model.Name" />
<button type="submit">Save</button>
</EditForm>
@code {
private Customer model = new();
private void Save() { /* persist model */ }
public class Customer
{
[Required] public string Name { get; set; } = "";
}
}DataAnnotationsValidator wires the model's attributes into the form, ValidationSummary and ValidationMessage surface the errors, and OnValidSubmit only fires when validation passes. Sharing the model and its attributes between the API and the UI is one of Blazor's most concrete wins -- one source of truth for what "valid" means, enforced on both ends.
When to Use Blazor
Blazor is a strong choice for .NET teams who would rather not run and staff a separate JavaScript front-end stack alongside their backend. It shines for internal tools and line-of-business applications, and especially for apps that share a heavy domain model and validation logic with a .NET backend -- the code reuse is real and the cognitive load of context-switching languages disappears. A single team in one language, one toolchain, and one set of models is a genuine productivity story.
It is weaker where its trade-offs bite. If you need the vast npm ecosystem of UI components and libraries, the absolute smallest possible bundle, deep SEO on a highly public marketing site, or you are hiring from a large pool of JavaScript-first developers, the mainstream JS frameworks remain the pragmatic call. Blazor's component ecosystem is real but smaller, and WASM's download size will never beat a hand-tuned JS bundle.
In the New Zealand enterprise and government context -- organisations that are already .NET shops building internal applications behind a login -- Blazor, and Blazor Server in particular, is a pragmatic and productive choice. The latency cost of Server is negligible on a corporate network, the per-circuit memory cost is manageable for internal user counts, and the ability to put existing C# developers onto front-end work without retraining them in a JS framework is exactly the kind of leverage these teams need.
Sharp Edges
The recurring problems track the hosting models. Blazor Server's latency is invisible on a LAN and painful over a slow or distant connection, since every interaction is a round-trip; and its per-circuit memory means a few thousand concurrent users can pressure server RAM in a way a stateless API never would -- capacity-plan for circuits, not just requests. WASM's initial download remains its headline weakness, and while AOT and trimming help, the runtime is never free. Debugging WASM has improved a great deal but still trails the mature JavaScript tooling, so expect rougher edges in the browser dev tools. You will still drop to JavaScript interop through IJSRuntime for browser APIs and JS libraries that have no .NET equivalent -- Blazor reduces JavaScript but does not eliminate it. And the most modern pitfall is render-mode and prerendering mismatch: a prerendered component runs OnInitializedAsync once on the server and again when it becomes interactive, causing double-fetches or apparently lost state if you assume initialization runs once. Understand which render mode each component is in, and these surprises stop being surprises.