From dotnet
Best practices and conventions for Blazor application development including Razor component structure, component lifecycle methods (OnInitializedAsync, OnParametersSetAsync), data binding, state management patterns, Server vs WebAssembly optimization, caching strategies, and error handling with ErrorBoundary. Use this skill whenever writing, reviewing, or refactoring Blazor components or pages — including when the user mentions Razor components, Blazor Server, Blazor WebAssembly (WASM), render optimization, cascading parameters, component parameters, or Blazor forms and validation, even if they do not explicitly say "Blazor best practices."
npx claudepluginhub atc-net/atc-agentic-toolkit --plugin dotnetThis skill uses the workspace's default tool permissions.
Apply these practices when writing, reviewing, or refactoring Blazor applications.
Building Blazor apps. Hosting models, render modes, routing, streaming rendering, prerender.
Builds C# .NET 8+ apps: ASP.NET Core APIs with minimal/controller routing, Entity Framework Core databases, Blazor components, CQRS via MediatR, async patterns, MAUI, SignalR.
Builds C# .NET 8+ apps with ASP.NET Core APIs using minimal/controller routing, EF Core data access, MediatR CQRS, Blazor components, async patterns, MAUI, and SignalR.
Share bugs, ideas, or general feedback.
Apply these practices when writing, reviewing, or refactoring Blazor applications.
.razor files) for all component-based UI@inherits pattern: define a base class (e.g., DeviceDetailsBase : NexusPageComponentBase) and reference it with @inherits DeviceDetailsBase in the .razor file@code { } blocksPageComponentBase that provides common injections (NavigationManager, AuthenticationState, ISnackbar) and helper methods (IsAuthenticatedAsync(), RedirectToLogin())async/await for all non-blocking UI operations — never block on Task.Result or .Wait() inside components, as this deadlocks the synchronization context_orderService)I (IOrderService)OrderList.razor for OrderList)Blazor components follow a well-defined lifecycle. Understanding when each method runs prevents redundant work and subtle bugs.
| Method | When it runs | Typical use |
|---|---|---|
SetParametersAsync | On every parameter update, before other lifecycle methods | Low-level parameter interception (rarely overridden directly) |
OnInitialized / OnInitializedAsync | Once when the component first initializes | Initial data loading, one-time setup |
OnParametersSet / OnParametersSetAsync | After parameter values are set (on init and every update) | Reacting to parameter changes, recomputing derived state |
OnAfterRender / OnAfterRenderAsync | After each render, with firstRender flag | JS interop calls, DOM measurements, third-party library init |
Dispose / DisposeAsync | When the component is removed from the render tree | Unsubscribe from events, cancel CancellationTokenSource, release resources |
IDisposable or IAsyncDisposable to clean up event subscriptions and cancellation tokens — leaked subscriptions cause memory leaks and ghost updatesfirstRender in OnAfterRenderAsync to run one-time JS interop or DOM setupStateHasChanged() inside OnInitializedAsync or OnParametersSetAsync — Blazor calls it automatically after these methods complete@bind for two-way binding to input elements: <input @bind="searchText" />@bind:event to control when the binding updates (e.g., @bind:event="oninput" for real-time updates vs the default onchange)@bind:after to run logic after a bound value changes without writing a manual setterValue / ValueChanged pair to support @bind-Value from parent components:[Parameter] public string Value { get; set; } = string.Empty;
[Parameter] public EventCallback<string> ValueChanged { get; set; }
[Parameter] propertiesEventCallback<T> to notify the parent of changes[CascadingParameter] with <CascadingValue> to avoid prop-drilling through intermediate componentsEventCallback over Action/Func for event handling — EventCallback automatically triggers a re-render on the correct component and handles async delegatesIn code-behind base classes, use [Inject] on properties. In .razor files, use @inject:
// Code-behind pattern (preferred for complex components)
[Inject] protected GatewayService GatewayService { get; set; }
[Inject] protected IDialogService DialogService { get; set; }
[Inject] protected ISnackbar SnackBarService { get; set; }
[Inject] protected NavigationManager Navigation { get; set; }
Scoped for per-circuit (Server) or per-user (WASM) services, Singleton for shared state and hub connections, Transient for stateless utilitiesScoped services into Singleton services — this captures a stale instance (the captive dependency problem)HttpClient directly from componentsLeverage current C# language features for clarity and conciseness:
record OrderDto(int Id, string Name, decimal Total);)_Imports.razor for commonly used namespacesISnackbar:try
{
await LoadDataAsync(cancellationToken);
}
catch (UnauthorizedAccessException)
{
RedirectToLogin();
}
catch (Exception ex)
{
SnackBarService.Add($"Failed to load data: {ex.Message}", Severity.Error);
}
NavigationException separately — it is expected in Blazor Server when navigation aborts renderingErrorBoundary for catching unhandled rendering exceptions when appropriateapp.UseExceptionHandler("/Error")EditForm with DataAnnotationsValidator for form-heavy scenarios with validationMudTextField, MudSelect) with debouncing:<MudTextField DebounceInterval="500"
OnDebounceIntervalElapsed="ApplyFilters"
@bind-Value="SearchQuery"
Placeholder="Search..." />
Blazor re-renders components more often than you might expect. These patterns keep the UI responsive.
ShouldRender() and return false when the component output has not changed — this skips the entire diff and DOM patch cycle@key on list items so Blazor can match existing elements by identity instead of recreating them on every render@rendermode appropriately (Server vs WebAssembly vs Static SSR) based on interactivity needs| Concern | Blazor Server | Blazor WebAssembly |
|---|---|---|
| Initial load | Fast — only a small SignalR connection is established | Slower — the .NET runtime and app assemblies download to the browser |
| Latency | Every UI interaction is a SignalR round-trip | No round-trip for UI logic; HTTP calls only for data |
| Scalability | Each user holds a server circuit and memory | Client-side — the server only serves APIs |
| Offline support | None — requires a persistent connection | Possible with service workers and local caching |
| Data access | Direct access to server-side resources (databases, file system) | Must go through HTTP APIs |
<Router OnNavigateAsync="OnNavigateAsync">) to defer assembly downloads and reduce initial payloadIAsyncEnumerable streaming with Server to progressively render large datasets without loading everything into memoryGetFromJsonAsync, PostAsJsonAsync) for HTTP operationsEventCallback<T> over raw delegates for component events — they batch render updates correctlyChoose a caching approach based on the hosting model:
IMemoryCache for fast, in-process caching of frequently accessed dataSingleton so they survive across circuit lifetimeslocalStorage for persistent, cross-session data (user preferences, tokens)sessionStorage for per-tab, session-scoped dataBlazored.LocalStorage or Blazored.SessionStorage packages for a typed, async-friendly APIChoose the right pattern based on complexity:
Use a scoped StateContainer service for app-wide state like theme, user preferences, or shared UI settings. Expose state via properties and notify components via events:
public class StateContainer
{
public bool IsDarkMode { get; private set; }
public event Action? OnThemeChange;
public void UseDarkMode(bool darkMode)
{
IsDarkMode = darkMode;
OnThemeChange?.Invoke();
}
}
Register as Scoped, inject into components, subscribe to events, call StateHasChanged() in the handler, and unsubscribe in Dispose.
For feature-specific state (device lists, search filters, real-time updates), create a dedicated scoped service that combines data caching with event notifications:
public class DataStateService : IDataStateService, IDisposable
{
public List<DeviceType> DeviceTypes { get; private set; } = [];
public DeviceSearchState SearchState { get; } = new();
public event Action? DeviceStateUpdated;
// ... fetch, cache, notify
}
Use [PersistentState] (.NET 10+) to automatically serialize/deserialize component state across navigations:
[PersistentState]
public List<Device>? Devices { get; set; }
For larger applications where many unrelated components need coordinated state, consider Fluxor or Blazor-State for Redux-style unidirectional data flow.
localStorage / sessionStorage (via Blazored.LocalStorage or similar) to persist state across page reloads in WASM appsWhen using a component library like MudBlazor, follow its conventions consistently:
MudLayout, MudAppBar, MudDrawer, MudMainContent) for the application shellIDialogService for modal dialogs and ISnackbar for toast notifications instead of custom implementationsMudTable, MudSelect, MudTextField) with their built-in features (sorting, paging, debouncing)For real-time features, use SignalR:
Singleton for connection lifecycle managementDataStateService or a similar scoped service, and propagate changes via events that components subscribe toHttpClient directlyIHttpClientFactory for HTTP client management with named or typed clientsISnackbar rather than letting exceptions propagate to the UICancellationToken with HTTP calls so navigating away cancels in-flight requests@attribute [StreamRendering] on pages for improved perceived performance — content renders progressively as data loadsNavigationManager, HttpClient, and custom services via bUnit's dependency injectionMicrosoft.Identity.Web) for enterprise authentication, or ASP.NET Identity / JWT for other scenariosAddCascadingAuthenticationState() to make auth state available throughout the component tree[CascadingParameter] protected Task<AuthenticationState>? AuthenticationStateTask[Authorize] attribute and use <AuthorizeView> / <AuthorizeRouteView> for conditional renderingBearerTokenHandler to automatically attach JWT tokens to downstream API calls