blazor

SKILL.md

Blazor Guide

Applies to: Blazor (.NET 8+), C# 12+, WebAssembly, Server, Blazor United, Interactive Web Apps

Core Principles

  1. Component-Based: Build encapsulated .razor components that own their state and rendering
  2. Hosting Flexibility: Choose WebAssembly, Server, or Blazor United (hybrid) per-component
  3. C# Everywhere: Share models, validation, and logic between client and server
  4. Type Safety: Leverage nullable reference types and strong parameter typing
  5. Lifecycle Awareness: Understand component lifecycle to avoid leaks and race conditions

Hosting Models

Model Runs In Best For
WebAssembly Browser (WASM) Offline-capable apps, PWAs, reduced server load
Server Server (SignalR) Low-latency, SEO, thin clients
United (.NET 8) Hybrid SSR + interactive Per-component render mode selection

Guardrails

Component Rules

  • Keep components under 200 lines (split with child components)
  • Use [Parameter, EditorRequired] for required parameters
  • Never mutate [Parameter] properties directly -- use local state instead
  • Use EventCallback<T> for parent-child communication (not Action<T>)
  • Implement IDisposable when subscribing to events, timers, or state changes
  • Use @key directive on list-rendered items for DOM diffing performance
  • Wrap component trees in <ErrorBoundary> for graceful error handling
  • Use RenderFragment and RenderFragment<T> for templated components

Lifecycle Rules

  • Do NOT call JS interop in OnInitialized/OnInitializedAsync -- use OnAfterRender
  • Use OnParametersSet for reacting to parameter changes (not property setters)
  • Call StateHasChanged() only when necessary (avoid triggering excessive re-renders)
  • Use ShouldRender() override to prevent unnecessary renders on hot paths
  • Always use CancellationToken with async lifecycle methods to handle disposal

Data Binding

  • Use @bind-Value for two-way binding with input components
  • Use @bind:event to control when binding updates (e.g., oninput vs onchange)
  • Use EditForm with DataAnnotationsValidator for form validation
  • Never use @bind and @onchange on the same element (they conflict)

Security

  • Use [Authorize] attribute on pages requiring authentication
  • Use <AuthorizeView> for conditional UI based on auth state
  • Validate all inputs server-side (client validation is for UX, not security)
  • Never trust Blazor WASM for authorization decisions -- enforce on API
  • Use antiforgery tokens for form submissions in SSR mode

Performance

  • Use <Virtualize> for large lists instead of @foreach
  • Use @rendermode appropriately (static SSR by default, interactive only when needed)
  • Use [StreamRendering] for pages with async data loading
  • Minimize JS interop calls (batch operations when possible)
  • Use IMemoryCache or Blazored.LocalStorage for client-side caching
  • Lazy-load assemblies for large WASM apps with LazyAssemblyLoader

Project Structure

MyApp/
├── MyApp.Client/                    # Blazor WebAssembly / interactive
│   ├── Pages/                       # Routable components (@page)
│   │   ├── Home.razor
│   │   └── Users/
│   │       ├── UserList.razor
│   │       ├── UserDetail.razor
│   │       └── UserForm.razor
│   ├── Components/                  # Reusable components
│   │   ├── Layout/
│   │   │   ├── MainLayout.razor
│   │   │   └── NavMenu.razor
│   │   ├── Common/
│   │   │   ├── LoadingSpinner.razor
│   │   │   ├── ErrorBoundary.razor
│   │   │   └── Modal.razor
│   │   └── Users/
│   │       └── UserCard.razor
│   ├── Services/                    # Client-side services
│   │   ├── IUserService.cs
│   │   └── UserService.cs
│   ├── State/                       # State management
│   │   └── AppState.cs
│   ├── wwwroot/                     # Static assets
│   ├── _Imports.razor               # Global using directives
│   ├── App.razor                    # Root component
│   └── Program.cs                   # DI and startup
├── MyApp.Server/                    # API / server host
│   ├── Controllers/
│   └── Program.cs
├── MyApp.Shared/                    # Shared models and DTOs
│   ├── Models/
│   └── DTOs/
├── tests/
│   ├── MyApp.Client.Tests/          # bUnit component tests
│   └── MyApp.Server.Tests/          # API integration tests
└── MyApp.sln

Layer Responsibilities

Layer Purpose Depends On
Client UI components, pages, client services Shared
Server API endpoints, server logic, auth Shared
Shared Models, DTOs, validation attributes Nothing
Tests bUnit component tests, API tests Client, Server

Component Patterns

Page Component with Data Loading

@page "/users"
@attribute [Authorize]
@inject IUserService UserService

<PageTitle>Users</PageTitle>

@if (loading)
{
    <LoadingSpinner />
}
else if (error is not null)
{
    <div class="alert alert-danger">
        <p>@error</p>
        <button class="btn btn-outline-danger" @onclick="LoadUsers">Retry</button>
    </div>
}
else if (users is null || users.Count == 0)
{
    <div class="alert alert-info">No users found.</div>
}
else
{
    @foreach (var user in users)
    {
        <UserCard @key="user.Id" User="user" OnEdit="() => Edit(user.Id)" />
    }
}

@code {
    private List<UserResponse>? users;
    private bool loading = true;
    private string? error;

    protected override async Task OnInitializedAsync()
    {
        await LoadUsers();
    }

    private async Task LoadUsers()
    {
        loading = true;
        error = null;
        try
        {
            users = await UserService.GetUsersAsync();
        }
        catch (Exception ex)
        {
            error = ex.Message;
        }
        finally
        {
            loading = false;
        }
    }

    private void Edit(long id) => Navigation.NavigateTo($"/users/{id}/edit");
}

Reusable Component with Parameters

<div class="card h-100">
    <div class="card-body">
        <h5 class="card-title">@User.FirstName @User.LastName</h5>
        <small class="text-muted">@User.Email</small>
        <span class="badge @(User.Active ? "bg-success" : "bg-secondary")">
            @(User.Active ? "Active" : "Inactive")
        </span>
    </div>
    <div class="card-footer">
        <button class="btn btn-sm btn-outline-primary" @onclick="OnEdit">Edit</button>
        <button class="btn btn-sm btn-outline-danger" @onclick="OnDelete">Delete</button>
    </div>
</div>

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

    [Parameter]
    public EventCallback OnEdit { get; set; }

    [Parameter]
    public EventCallback OnDelete { get; set; }
}

Templated Component (RenderFragment)

@code {
    [Parameter] public string Title { get; set; } = "Modal";
    [Parameter] public RenderFragment? BodyContent { get; set; }
    [Parameter] public RenderFragment? FooterContent { get; set; }
    [Parameter] public EventCallback OnClose { get; set; }
}

Use RenderFragment for content slots, RenderFragment<T> for typed template parameters.

Data Binding & Forms

EditForm with Validation

<EditForm Model="model" OnValidSubmit="HandleSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary class="alert alert-danger" />

    <div class="mb-3">
        <label for="email" class="form-label">Email</label>
        <InputText id="email" class="form-control" @bind-Value="model.Email" />
        <ValidationMessage For="@(() => model.Email)" class="text-danger" />
    </div>

    <button type="submit" class="btn btn-primary" disabled="@submitting">
        @(submitting ? "Saving..." : "Save")
    </button>
</EditForm>

Routing

Route Parameters and Constraints

@page "/users/{Id:long}"
@page "/users/{Id:long}/edit"

@code {
    [Parameter]
    public long Id { get; set; }
}

Navigation

@inject NavigationManager Navigation

Navigation.NavigateTo("/users");                    // Client-side navigation
Navigation.NavigateTo("/users/1", forceLoad: true); // Full page reload

Use <NavLink> with Match="NavLinkMatch.Prefix" or NavLinkMatch.All for active CSS state.

State Management

Observable State Service

public class AppState
{
    private UserResponse? _currentUser;

    public UserResponse? CurrentUser
    {
        get => _currentUser;
        set { _currentUser = value; NotifyStateChanged(); }
    }

    public event Action? OnChange;
    private void NotifyStateChanged() => OnChange?.Invoke();
}

Consuming State in Components

@inject AppState State
@implements IDisposable

<span>@State.CurrentUser?.FirstName</span>

@code {
    protected override void OnInitialized()
    {
        State.OnChange += StateHasChanged;
    }

    public void Dispose()
    {
        State.OnChange -= StateHasChanged;
    }
}

CascadingValue for Shared Data

Use <CascadingValue Value="@theme"> in layouts. Consume with [CascadingParameter] in children. Good for theme, auth state, and locale. Avoid for frequently changing data (triggers full subtree re-render).

JavaScript Interop

Calling JS from C#

@inject IJSRuntime JS

@code {
    // Only call in OnAfterRenderAsync, not OnInitializedAsync
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            var width = await JS.InvokeAsync<int>("blazorInterop.getWindowWidth");
        }
    }
}

Encapsulate JS interop behind service classes registered in DI. Use InvokeVoidAsync for calls without return values, InvokeAsync<T> for typed returns.

.NET 8 Blazor United

Interactive Render Modes

@* Static SSR (default -- no interactivity) *@
<StaticComponent />

@* Interactive Server (SignalR) *@
<InteractiveComponent @rendermode="InteractiveServer" />

@* Interactive WebAssembly *@
<InteractiveComponent @rendermode="InteractiveWebAssembly" />

@* Interactive Auto (starts Server, switches to WASM when downloaded) *@
<InteractiveComponent @rendermode="InteractiveAuto" />

Streaming Rendering

Add @attribute [StreamRendering] to pages with async data loading. The page renders immediately with placeholder content, then streams updates as OnInitializedAsync completes. Ideal for SSR pages loading slow data.

Testing Overview

Standards

  • Use bUnit for component unit tests
  • Use xUnit as the test runner
  • Use Moq or NSubstitute for service mocking
  • Use FluentAssertions for readable assertions
  • Test rendered markup, parameter behavior, and event callbacks
  • Coverage target: >80% for business logic components

Basic Component Test

using Bunit;
using FluentAssertions;

public class UserCardTests : TestContext
{
    [Fact]
    public void UserCard_DisplaysUserInfo()
    {
        var user = new UserResponse(1, "test@example.com", "John", "Doe",
            "User", true, DateTime.UtcNow);

        var cut = RenderComponent<UserCard>(p => p.Add(x => x.User, user));

        cut.Find(".card-title").TextContent.Should().Contain("John Doe");
        cut.Find("small.text-muted").TextContent.Should().Contain("test@example.com");
    }

    [Fact]
    public void UserCard_EditButton_InvokesCallback()
    {
        var user = new UserResponse(1, "t@e.com", "J", "D", "User", true, DateTime.UtcNow);
        var clicked = false;

        var cut = RenderComponent<UserCard>(p => p
            .Add(x => x.User, user)
            .Add(x => x.OnEdit, EventCallback.Factory.Create(this, () => clicked = true)));

        cut.Find("button.btn-outline-primary").Click();
        clicked.Should().BeTrue();
    }
}

Tooling

Essential Commands

dotnet new blazorwasm -o MyApp.Client     # New Blazor WASM app
dotnet new blazorserver -o MyApp.Server   # New Blazor Server app
dotnet new blazor -o MyApp                # New Blazor Web App (.NET 8 United)
dotnet watch run --project MyApp.Client   # Dev server with hot reload
dotnet build                              # Build
dotnet publish -c Release                 # Publish
dotnet test                               # Run tests
dotnet add package bunit                  # Add bUnit testing

Key Dependencies

Package Purpose
Microsoft.AspNetCore.Components.WebAssembly WASM hosting
Microsoft.AspNetCore.Components.WebAssembly.Authentication WASM auth
Blazored.LocalStorage Browser local storage
Blazored.Toast Toast notifications
bunit Component testing
Fluxor.Blazor.Web Redux-style state management

Best Practices Summary

DO

  • Use @key on list items for efficient DOM diffing
  • Use EventCallback over Action for component events
  • Implement IDisposable for event handler and timer cleanup
  • Use <Virtualize> for rendering large collections
  • Use typed HttpClient for API calls
  • Use CascadingValue for widely shared state (theme, auth)
  • Use @rendermode per-component in .NET 8 United

DO NOT

  • Call JS interop in OnInitialized (use OnAfterRender)
  • Mutate [Parameter] properties directly
  • Use synchronous HTTP calls
  • Ignore component lifecycle (leaks events and timers)
  • Overuse JS interop (prefer C# solutions)
  • Skip loading/error state handling in data-fetching components

Advanced Topics

For detailed patterns and examples, see:

External References

Weekly Installs
11
Repository
ar4mirez/samuel
GitHub Stars
3
First Seen
Feb 20, 2026
Installed on
opencode11
github-copilot11
codex11
kimi-cli11
gemini-cli11
amp11