--- name: blazor description: | Blazor framework guardrails, patterns, and best practices for AI-assisted development. Use when working with Blazor projects, or when the user mentions Blazor. Provides WebAssembly/Server patterns, component lifecycle, SignalR, and state management guidelines. license: MIT metadata: author: samuel version: "1.0" category: framework language: csharp extensions: ".razor,.cs" --- # 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` for parent-child communication (not `Action`) - 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 `` for graceful error handling - Use `RenderFragment` and `RenderFragment` 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 `` 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 `` 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 ```razor @page "/users" @attribute [Authorize] @inject IUserService UserService Users @if (loading) { } else if (error is not null) {

@error

} else if (users is null || users.Count == 0) {
No users found.
} else { @foreach (var user in users) { } } @code { private List? 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 ```razor
@User.FirstName @User.LastName
@User.Email @(User.Active ? "Active" : "Inactive")
@code { [Parameter, EditorRequired] public UserResponse User { get; set; } = default!; [Parameter] public EventCallback OnEdit { get; set; } [Parameter] public EventCallback OnDelete { get; set; } } ``` ### Templated Component (RenderFragment) ```razor @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` for typed template parameters. ## Data Binding & Forms ### EditForm with Validation ```razor
``` ## Routing ### Route Parameters and Constraints ```razor @page "/users/{Id:long}" @page "/users/{Id:long}/edit" @code { [Parameter] public long Id { get; set; } } ``` ### Navigation ```csharp @inject NavigationManager Navigation Navigation.NavigateTo("/users"); // Client-side navigation Navigation.NavigateTo("/users/1", forceLoad: true); // Full page reload ``` Use `` with `Match="NavLinkMatch.Prefix"` or `NavLinkMatch.All` for active CSS state. ## State Management ### Observable State Service ```csharp 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 ```razor @inject AppState State @implements IDisposable @State.CurrentUser?.FirstName @code { protected override void OnInitialized() { State.OnChange += StateHasChanged; } public void Dispose() { State.OnChange -= StateHasChanged; } } ``` ### CascadingValue for Shared Data Use `` 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# ```csharp @inject IJSRuntime JS @code { // Only call in OnAfterRenderAsync, not OnInitializedAsync protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { var width = await JS.InvokeAsync("blazorInterop.getWindowWidth"); } } } ``` Encapsulate JS interop behind service classes registered in DI. Use `InvokeVoidAsync` for calls without return values, `InvokeAsync` for typed returns. ## .NET 8 Blazor United ### Interactive Render Modes ```razor @* Static SSR (default -- no interactivity) *@ @* Interactive Server (SignalR) *@ @* Interactive WebAssembly *@ @* Interactive Auto (starts Server, switches to WASM when downloaded) *@ ``` ### 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 ```csharp 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(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(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 ```bash 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 `` 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: - [references/patterns.md](references/patterns.md) -- Forms, SignalR, authentication, testing, performance, error handling patterns ## External References - [Blazor Documentation](https://learn.microsoft.com/aspnet/core/blazor) - [Blazor University](https://blazor-university.com/) - [bUnit Testing](https://bunit.dev/) - [Blazored Libraries](https://github.com/Blazored) - [Awesome Blazor](https://github.com/AdrienTorris/awesome-blazor)