In-Depth Guide · 10 Sections · Blazor

Blazor In Depth

From render modes (Server/WASM/Auto) and components to HttpClient data fetching, EF Core, AuthorizeView, forms validation, brief JS interop, deployment and production hardening. Plain English, copy-paste .NET 9 code.

Render Modes Components & Data Auth & Forms Deploy & Production

Prerequisite: C# basics — C# for Dummies or Complete C# for Dummies. For backend APIs see Minimal API or REST API. Database access assumes EF Core In Depth; authentication builds on ASP.NET Core Identity.

01

Why Blazor? Render Modes

Blazor lets you build interactive web UIs in C# instead of JavaScript. .NET 8+ unified render modes let you mix Server, WebAssembly, and static rendering in one app — pick per component or page.

1.1

Server vs WASM vs Auto

// InteractiveServer — UI events over SignalR; fast first load, needs connection
@rendermode InteractiveServer

// InteractiveWebAssembly — runs in browser; offline-capable, larger download
@rendermode InteractiveWebAssembly

// InteractiveAuto — Server first, then WASM after download (best of both)
@rendermode InteractiveAuto

// Static SSR — no interactivity; great for SEO landing pages
@rendermode Static  // or omit on .razor page (default in many templates)

// Choose by scenario:
//   Internal admin app     → InteractiveServer (low latency, secure DB access)
//   Public SPA             → InteractiveWebAssembly
//   Mixed marketing + app  → InteractiveAuto or per-page modes
1.2

Blazor vs SPA frameworks

// Blazor advantages:
//   • One language (C#) front-to-back
//   • Share DTOs, validation, auth with API layer
//   • Server mode — no WASM download, direct DI to DbContext

// Blazor trade-offs:
//   • Server mode — SignalR scale-out needs sticky sessions or Azure SignalR
//   • WASM — initial payload size; use trimming & lazy load
//   • Ecosystem smaller than React — but growing fast

// Hybrid: Blazor UI + Minimal API backend (common enterprise pattern)
02

Project Setup

The Blazor Web App template (.NET 8+) supports all render modes in one project. Structure: Components/ for UI, Program.cs for services, optional Client/ project for WASM.

2.1

Create & configure

dotnet new blazor -n MyShop -int Auto --empty false
cd MyShop
dotnet run

// Program.cs — register render modes
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents()
    .AddInteractiveWebAssemblyComponents();

app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode()
    .AddInteractiveWebAssemblyRenderMode()
    .AddAdditionalAssemblies(typeof(Client._Imports).Assembly);
2.2

App.razor, Routes & _Imports

// Components/App.razor
<Routes />

// Components/Routes.razor
<Router AppAssembly="typeof(Program).Assembly"
        AdditionalAssemblies="new[] { typeof(Client._Imports).Assembly }">
    <Found Context="routeData">
        <RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
    </Found>
</Router>

// Components/_Imports.razor — global usings
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
03

Components, Parameters & Cascading

Blazor components are reusable UI units — .razor files mix HTML and C#. Pass data with parameters, share context with cascading values, and react to events with EventCallback.

3.1

Parameters & EventCallback

// ProductCard.razor
<article class="card">
    <h3>@Product.Name</h3>
    <p>@Product.Price.ToString("C")</p>
    <button @onclick="OnAddToCart">Add to cart</button>
</article>

@code {
    [Parameter, EditorRequired]
    public ProductDto Product { get; set; } = default!;

    [Parameter]
    public EventCallback<ProductDto> OnAdded { get; set; }

    private async Task OnAddToCart() => await OnAdded.InvokeAsync(Product);
}

// Parent: <ProductCard Product="item" OnAdded="HandleAdded" />
3.2

Cascading parameters & lifecycle

// Layout provides theme to all descendants
<CascadingValue Value="Theme" IsFixed="true">
    @Body
</CascadingValue>

// Child reads it
[Parameter]
public string Theme { get; set; } = "light";  // or [CascadingParameter]

// Lifecycle hooks
protected override async Task OnInitializedAsync()
    => _products = await ProductService.GetAllAsync();

protected override void OnParametersSet()
{
    // Runs when parent passes new parameter values
}

// ❌ Don't call StateHasChanged in OnInitialized unless needed
04

Data Fetching with HttpClient

In WASM and Auto modes, call your API with HttpClient. In Server mode you can inject services directly — but HttpClient keeps a clean boundary and works in all modes.

4.1

Typed HttpClient service

// Program.cs
builder.Services.AddHttpClient<IProductApi, ProductApi>(client =>
{
    client.BaseAddress = new Uri(builder.Configuration["ApiBaseUrl"]!);
});

// ProductApi.cs
public sealed class ProductApi(HttpClient http) : IProductApi
{
    public async Task<List<ProductDto>> GetAllAsync(CancellationToken ct = default)
        => await http.GetFromJsonAsync<List<ProductDto>>("api/products", ct)
           ?? [];
}

// Products.razor
@inject IProductApi Api
@if (_loading) { <p>Loading…</p> }
else { @foreach (var p in _products) { <ProductCard Product="p" /> } }
4.2

Loading states & error handling

@code {
    private List<ProductDto> _products = [];
    private bool _loading = true;
    private string? _error;

    protected override async Task OnInitializedAsync()
    {
        try
        {
            _products = await Api.GetAllAsync();
        }
        catch (HttpRequestException ex)
        {
            _error = "Could not load products. Try again later.";
            Logger.LogError(ex, "Product fetch failed");
        }
        finally { _loading = false; }
    }
}

// Server mode: IHttpClientFactory avoids socket exhaustion
05

EF Core in Blazor

Interactive Server can inject DbContext directly — but DbContext is scoped per circuit, not per HTTP request. Use a factory pattern and keep operations short.

5.1

DbContextFactory pattern

// Program.cs — preferred for Blazor Server
builder.Services.AddDbContextFactory<AppDbContext>(options =>
    options.UseSqlServer(connString));

// Component or service
public sealed class ProductService(IDbContextFactory<AppDbContext> factory)
{
    public async Task<List<Product>> GetActiveAsync(CancellationToken ct)
    {
        await using var db = await factory.CreateDbContextAsync(ct);
        return await db.Products.AsNoTracking()
            .Where(p => p.IsActive).ToListAsync(ct);
    }
}
5.2

WASM — never put DbContext in browser

// ❌ WASM runs in browser — connection strings exposed
// ✅ WASM always calls API; Server mode may use factory directly

// Circuit scope gotcha — one user, long-lived circuit:
//   • Don't hold DbContext as component field
//   • Create/dispose per operation via factory
//   • Use AsNoTracking for read-only grids

// Scale-out: Azure SignalR Service + sticky sessions or stateless design
Watch Out — DbContext lifetime in Blazor Server

Blazor Server circuits outlive a single HTTP request. Injecting scoped DbContext into a singleton or long-lived component causes stale data and threading issues. Always use IDbContextFactory<T>.

06

Auth & AuthorizeView

Blazor integrates with ASP.NET Core Identity and cookie or JWT auth. Use AuthorizeView and [Authorize] to show UI based on roles and policies.

6.1

AuthorizeView & cascading auth

<AuthorizeView Roles="Admin">
    <Authorized>
        <button @onclick="DeleteAll">Delete all</button>
    </Authorized>
    <NotAuthorized>
        <p>Admin access required.</p>
    </NotAuthorized>
</AuthorizeView>

<AuthorizeView Policy="CanEditProducts">
    <Authorized Context="auth">
        <p>Hello, @auth.User.Identity?.Name</p>
    </Authorized>
</AuthorizeView>

// Page-level
@attribute [Authorize(Roles = "Manager")]
6.2

Server vs WASM auth setup

// Server — cookie auth works out of the box
builder.Services.AddAuthentication()
    .AddCookie();
builder.Services.AddCascadingAuthenticationState();

// WASM — use OIDC or API tokens
builder.Services.AddOidcAuthentication(options =>
{
    builder.Configuration.Bind("Auth0", options.ProviderOptions);
});

// Pass token to HttpClient
builder.Services.AddHttpClient("API", client => ...)
    .AddHttpMessageHandler<AuthorizationMessageHandler>();
07

Forms & Validation

EditForm with DataAnnotations or FluentValidation gives two-way binding and client-side validation. Server mode validates on the server too — WASM validates in browser before API call.

7.1

EditForm & ValidationMessage

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

    <InputText @bind-Value="_model.Name" />
    <ValidationMessage For="() => _model.Name" />

    <InputNumber @bind-Value="_model.Price" />
    <ValidationMessage For="() => _model.Price" />

    <button type="submit">Save</button>
</EditForm>

@code {
    private ProductEditModel _model = new();

    private async Task HandleSubmit()
        => await Api.CreateAsync(_model);
}
7.2

FluentValidation integration

public sealed class ProductValidator : AbstractValidator<ProductEditModel>
{
    public ProductValidator()
    {
        RuleFor(x => x.Name).NotEmpty().MaximumLength(100);
        RuleFor(x => x.Price).GreaterThan(0);
    }
}

// Register: builder.Services.AddValidatorsFromAssemblyContaining<ProductValidator>();

<EditForm Model="_model" OnValidSubmit="HandleSubmit">
    <FluentValidationValidator />
    ...
</EditForm>
08

JS Interop (Brief)

When Blazor can't do something natively — chart libraries, file downloads, browser APIs — use JavaScript interop. Keep JS thin; call from C# via IJSRuntime.

8.1

IJSRuntime — invoke JS from C#

@inject IJSRuntime JS

<button @onclick="CopyToClipboard">Copy</button>

@code {
    private async Task CopyToClipboard()
        => await JS.InvokeVoidAsync("navigator.clipboard.writeText", _text);

    // Module import (.NET 8+)
    private IJSObjectReference? _module;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
            _module = await JS.InvokeAsync<IJSObjectReference>(
                "import", "./js/charts.js");
    }
}
8.2

JSInvokable — call C# from JS

// wwwroot/js/charts.js
export function initChart(dotNetRef) {
    chart.on('click', () => dotNetRef.invokeMethodAsync('OnChartClick', pointId));
}

// Component
private DotNetObjectReference<ChartComponent>? _dotNetRef;

[JSInvokable]
public void OnChartClick(int pointId) => SelectedId = pointId;

// ⚠️ Dispose DotNetObjectReference in Dispose()
// ⚠️ Avoid JS interop in OnInitialized — use OnAfterRenderAsync
09

Deploy

Deployment depends on render mode: Server apps host like any ASP.NET Core site; WASM publishes static files plus a small API; Auto needs both.

9.1

Publish commands

// Server / Auto (hosted)
dotnet publish -c Release -o ./publish

// WASM standalone — static files in wwwroot
dotnet publish -c Release

// Docker
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runtime
WORKDIR /app
COPY publish/ .
ENV ASPNETCORE_URLS=http://+:8080
EXPOSE 8080
ENTRYPOINT ["dotnet", "MyShop.dll"]
9.2

Azure & CDN for WASM assets

// Azure App Service — Blazor Server + Auto
// Enable WebSockets (required for SignalR)
// Scale-out: Azure SignalR Service

// WASM static assets — Azure Static Web Apps or Blob + CDN
//   • gzip/brotli _framework/*.wasm
//   • Long cache headers on hashed files
//   • Lazy load assemblies: <LazyAssemblyLoader />

// CI: dotnet publish → deploy artifact → health check /health
10

Production — Performance & Reliability

10.1

Server mode production checklist

  • Azure SignalR Service when scaling beyond one instance.
  • Circuit disconnect — handle OnCircuitOpened/Closed; show reconnect UI.
  • Memory — circuits hold state; set idle timeout; avoid large object graphs in components.
  • Prerender static HTML for first paint, then hydrate interactivity.
// Reconnect UI — App.razor
<ReconnectModal />

// Configure circuit options
builder.Services.AddServerSideBlazor(options =>
{
    options.DisconnectedCircuitRetentionPeriod = TimeSpan.FromMinutes(3);
    options.MaxBufferedUnacknowledgedRenderBatches = 10;
});
10.2

Testing & monitoring

// bUnit — component unit tests
// Package: bunit

[Fact]
public void ProductCard_RendersName()
{
    using var ctx = new BunitContext();
    var cut = ctx.Render<ProductCard>(p => p
        .Add(x => x.Product, new ProductDto(1, "Widget", 9.99m)));

    cut.Find("h3").TextContent.Should().Be("Widget");
}

// OpenTelemetry + App Insights — track circuit errors, render duration
Mastery Move — where to go next

1. Testing .NET — bUnit component tests and integration tests for your API.

2. EF Core In Depth — DbContextFactory patterns for Server apps.

3. ASP.NET Core Identity — full auth setup with roles and policies.

4. Azure for .NET — App Service, SignalR Service, and Static Web Apps.