Learnixo
Back to blog
Backend Systemsintermediate

Blazor WebAssembly: .NET in the Browser

Build interactive SPAs with Blazor WebAssembly. Covers components, data binding, forms, HTTP calls, authentication, JavaScript interop, PWA support, and when to choose Blazor WASM over React.

LearnixoJune 4, 20266 min read
.NETC#BlazorWebAssemblyWASMSPAFrontend
Share:𝕏

What is Blazor WebAssembly?

Blazor WebAssembly runs your C# code directly in the browser via WebAssembly. No JavaScript framework needed. The .NET runtime ships to the browser on first load; subsequent navigations are instant.

Browser
├── WebAssembly (.NET runtime)
├── Your compiled C# app (DLLs)
└── Razor components (.razor files → HTML + C#)

When to choose Blazor WASM:

  • Your team knows C# and wants to avoid JavaScript/TypeScript
  • You're building an internal tool, admin dashboard, or enterprise SPA
  • You want to share models/validation logic between client and server
  • Offline/PWA capability is needed

When to stick with React/Angular:

  • You need SEO (WASM has poor initial SEO without pre-rendering)
  • First-load performance is critical (WASM runtime download ~10MB)
  • You have a large frontend team already proficient in JS

Project Setup

Bash
dotnet new blazorwasm -n OrderFlow.Client
cd OrderFlow.Client
dotnet run

Project structure:

OrderFlow.Client/
├── Pages/          ← routable components
├── Components/     ← reusable components
├── Layout/         ← MainLayout.razor, NavMenu.razor
├── Services/       ← HTTP service classes
├── wwwroot/        ← static assets, index.html
└── Program.cs

Components

Every UI element is a .razor file — HTML markup + C# logic in one file.

RAZOR
@* Pages/Orders.razor *@
@page "/orders"
@inject IOrderService OrderService

<h1>Orders</h1>

@if (_loading)
{
    <p>Loading...</p>
}
else if (_orders is null || !_orders.Any())
{
    <p>No orders found.</p>
}
else
{
    <table class="table">
        <thead>
            <tr><th>ID</th><th>Customer</th><th>Total</th><th>Status</th></tr>
        </thead>
        <tbody>
            @foreach (var order in _orders)
            {
                <tr>
                    <td>@order.Id</td>
                    <td>@order.CustomerName</td>
                    <td>@order.Total.ToString("C")</td>
                    <td><span class="badge @GetStatusClass(order.Status)">@order.Status</span></td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private List<OrderDto>? _orders;
    private bool _loading = true;

    protected override async Task OnInitializedAsync()
    {
        _orders  = await OrderService.GetOrdersAsync();
        _loading = false;
    }

    private string GetStatusClass(string status) => status switch
    {
        "Pending"   => "bg-warning",
        "Submitted" => "bg-info",
        "Delivered" => "bg-success",
        "Cancelled" => "bg-danger",
        _           => "bg-secondary"
    };
}

Data Binding

RAZOR
@* Two-way binding with @bind *@
<input @bind="_searchTerm" @bind:event="oninput" placeholder="Search orders..." />
<p>Searching for: @_searchTerm</p>

@* Bind to a property with event *@
<input type="number" @bind="_quantity" />

@* Conditional rendering *@
@if (_showDetails)
{
    <OrderDetails OrderId="@_selectedId" />
}

<button @onclick="ToggleDetails">Toggle</button>

@code {
    private string _searchTerm = "";
    private int _quantity = 1;
    private bool _showDetails = false;
    private Guid _selectedId;

    private void ToggleDetails() => _showDetails = !_showDetails;
}

Components with Parameters

RAZOR
@* Components/OrderCard.razor *@
<div class="card mb-3">
    <div class="card-body">
        <h5 class="card-title">Order @Order.Id</h5>
        <p>@Order.CustomerName — @Order.Total.ToString("C")</p>
        <button class="btn btn-primary" @onclick="() => OnSelect.InvokeAsync(Order.Id)">
            View
        </button>
    </div>
</div>

@code {
    [Parameter] public OrderDto Order { get; set; } = null!;
    [Parameter] public EventCallback<Guid> OnSelect { get; set; }
}
RAZOR
@* Parent usage *@
@foreach (var order in _orders)
{
    <OrderCard Order="order" OnSelect="HandleOrderSelected" />
}

@code {
    private async Task HandleOrderSelected(Guid id)
    {
        _selectedId = id;
        await LoadOrderDetailsAsync(id);
    }
}

Forms and Validation

RAZOR
@* Pages/CreateOrder.razor *@
@page "/orders/create"
@inject IOrderService OrderService
@inject NavigationManager Nav

<h2>Create Order</h2>

<EditForm Model="_model" OnValidSubmit="HandleSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <div class="mb-3">
        <label>Customer ID</label>
        <InputText class="form-control" @bind-Value="_model.CustomerId" />
        <ValidationMessage For="() => _model.CustomerId" />
    </div>

    <div class="mb-3">
        <label>Product</label>
        <InputSelect class="form-control" @bind-Value="_model.ProductId">
            <option value="">Select product...</option>
            @foreach (var p in _products)
            {
                <option value="@p.Id">@p.Name — @p.Price.ToString("C")</option>
            }
        </InputSelect>
    </div>

    <div class="mb-3">
        <label>Quantity</label>
        <InputNumber class="form-control" @bind-Value="_model.Quantity" />
        <ValidationMessage For="() => _model.Quantity" />
    </div>

    <button type="submit" class="btn btn-primary" disabled="@_submitting">
        @(_submitting ? "Creating..." : "Create Order")
    </button>
</EditForm>

@code {
    private CreateOrderModel _model = new();
    private List<ProductDto> _products = new();
    private bool _submitting = false;

    protected override async Task OnInitializedAsync()
    {
        _products = await ProductService.GetProductsAsync();
    }

    private async Task HandleSubmit()
    {
        _submitting = true;
        try
        {
            var id = await OrderService.CreateOrderAsync(_model);
            Nav.NavigateTo($"/orders/{id}");
        }
        finally
        {
            _submitting = false;
        }
    }
}
C#
// Model with validation attributes
public class CreateOrderModel
{
    [Required]
    public string CustomerId { get; set; } = "";

    [Required]
    public string ProductId { get; set; } = "";

    [Range(1, 1000)]
    public int Quantity { get; set; } = 1;
}

HTTP Calls

C#
// Services/OrderService.cs
public class OrderService : IOrderService
{
    private readonly HttpClient _http;

    public OrderService(HttpClient http) => _http = http;

    public async Task<List<OrderDto>> GetOrdersAsync()
        => await _http.GetFromJsonAsync<List<OrderDto>>("api/orders") ?? new();

    public async Task<OrderDto?> GetOrderAsync(Guid id)
        => await _http.GetFromJsonAsync<OrderDto>($"api/orders/{id}");

    public async Task<Guid> CreateOrderAsync(CreateOrderModel model)
    {
        var response = await _http.PostAsJsonAsync("api/orders", model);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<Guid>();
    }
}
C#
// Program.cs
builder.Services.AddScoped(sp => new HttpClient
{
    BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
});

builder.Services.AddScoped<IOrderService, OrderService>();

Authentication

Bash
dotnet add package Microsoft.AspNetCore.Components.WebAssembly.Authentication
C#
// Program.cs — OIDC authentication
builder.Services.AddOidcAuthentication(options =>
{
    builder.Configuration.Bind("OidcConfig", options.ProviderOptions);
});

// Or Azure AD / Entra ID
builder.Services.AddMsalAuthentication(options =>
{
    builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
    options.ProviderOptions.DefaultAccessTokenScopes.Add("api://my-api/access");
});
RAZOR
@* Protect a page *@
@page "/admin"
@attribute [Authorize]

<AuthorizeView>
    <Authorized>
        <p>Welcome, @context.User.Identity?.Name!</p>
    </Authorized>
    <NotAuthorized>
        <p>Please <a href="authentication/login">log in</a>.</p>
    </NotAuthorized>
</AuthorizeView>

JavaScript Interop

When you need browser APIs not yet in .NET:

C#
// Inject IJSRuntime
@inject IJSRuntime JS

// Call JavaScript from C#
await JS.InvokeVoidAsync("console.log", "Hello from .NET!");
var result = await JS.InvokeAsync<string>("prompt", "Enter your name:");

// Clipboard
await JS.InvokeVoidAsync("navigator.clipboard.writeText", textToCopy);
JAVASCRIPT
// wwwroot/js/app.js — expose functions for .NET to call
window.scrollToTop = () => window.scrollTo(0, 0);
window.downloadFile = (filename, content) => {
    const a = document.createElement('a');
    a.href = `data:text/plain;charset=utf-8,${encodeURIComponent(content)}`;
    a.download = filename;
    a.click();
};

PWA Support

Bash
dotnet new blazorwasm --pwa -n OrderFlow.Client

Adds a service worker for offline support. The app caches its own DLLs and assets — works offline after first load.


Hosted Mode (with ASP.NET Core backend)

Bash
dotnet new blazorwasm --hosted -n OrderFlow
# Creates OrderFlow.Client, OrderFlow.Server, OrderFlow.Shared

The Server project hosts the WASM app AND provides the API. The Shared project contains DTOs and validation logic used by both.


Interview Questions

Q: What is the difference between Blazor WebAssembly and Blazor Server? WASM downloads the .NET runtime and runs entirely in the browser — no server connection needed after load. Blazor Server runs C# on the server and streams UI updates to the browser over SignalR — small payload, instant startup, but requires constant connectivity and server memory per user.

Q: What is the biggest disadvantage of Blazor WASM? Initial load time — the browser downloads the .NET runtime (~5–10MB) and app DLLs on first visit. Subsequent loads use the cache. For public-facing apps where first impression matters, this is a significant tradeoff vs a JavaScript SPA.

Q: How does Blazor handle data binding? @bind creates two-way binding — changes to the variable update the DOM, and user input updates the variable. By default it binds on the onchange event. Use @bind:event="oninput" for real-time updates as the user types.

Q: What is JavaScript interop in Blazor? IJSRuntime lets C# call JavaScript functions (InvokeAsync, InvokeVoidAsync) and JavaScript can call .NET static methods or instance methods via DotNet.invokeMethod. Needed for browser APIs not yet exposed in .NET (clipboard, geolocation, media).

Enjoyed this article?

Explore the Backend Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.