Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
Hosting model selection and render modes -- see [skill:dotnet-blazor-patterns]
Auth components (AuthorizeView, CascadingAuthenticationState) -- see [skill:dotnet-blazor-auth]
bUnit testing -- see [skill:dotnet-blazor-testing]
Standalone SignalR hub patterns -- see [skill:dotnet-realtime-communication]
E2E testing -- see [skill:dotnet-playwright]
UI framework selection -- see [skill:dotnet-ui-chooser]
Accessibility patterns (ARIA, keyboard navigation) -- see [skill:dotnet-accessibility]
Cross-references: [skill:dotnet-blazor-patterns] for hosting models and render modes, [skill:dotnet-blazor-auth] for
authentication, [skill:dotnet-blazor-testing] for bUnit testing, [skill:dotnet-realtime-communication] for standalone
SignalR, [skill:dotnet-playwright] for E2E testing, [skill:dotnet-ui-chooser] for framework selection,
[skill:dotnet-accessibility] for accessibility patterns (ARIA, keyboard nav, screen readers).
Component Lifecycle
Lifecycle Methods
@code {
// 1. Called when parameters are set/updatedpublicoverrideasync Task SetParametersAsync(ParameterView parameters)
{
// Access raw parameters before they are appliedawaitbase.SetParametersAsync(parameters);
}
// 2. Called after parameters are assigned (sync)protectedoverridevoidOnInitialized()
{
}
{
products = ProductService.GetProductsAsync();
}
{
}
{
(firstRender)
{
}
}
{
(firstRender)
{
JSRuntime.InvokeVoidAsync(, chartElement);
}
}
{
}
{
(module )
{
module.DisposeAsync();
}
}
}
```text
| Lifecycle Event | Static SSR | InteractiveServer | InteractiveWebAssembly | InteractiveAuto | Hybrid |
| ---------------------- | --------------------- | -------------------------------------------- | -------------------------------- | ------------------------------------------------------------------ | --------------------------- |
| `OnInitialized(Async)` | Runs server | Runs server | Runs browser | Server first load, browser after WASM cached | Runs -process |
| `OnAfterRender(Async)` | Never called | Runs server after SignalR confirms render | Runs browser after DOM update | Server-side then browser-side (matches active runtime) | Runs after WebView render |
| `Dispose(Async)` | Called after response | Called circuit ends | Called component removal | = Name=>
<Router AppAssembly=>
<!-- All descendants can receive AppTheme -->
</Router>
</CascadingValue>
@code {
ThemeSettings theme = () { IsDarkMode = , AccentColor = };
}
```text
```razor
<!-- Child: consume the cascading -->
@code {
[]
ThemeSettings? Theme { ; ; }
}
```text
**=` to avoid
re-render overhead:
```razor
<CascadingValue Value= IsFixed=>
<ChildComponent />
</CascadingValue>
```text
```csharp
builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.AddSingleton<AppState>();
@inject IProductService ProductService
@inject AppState State
```text
**DI lifetime behavior per render mode:**
| Lifetime | InteractiveServer | InteractiveWebAssembly | InteractiveAuto | Hybrid |
| --------- | ---------------------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------ | ---------------------------------------- |
| Singleton | Shared across all circuits the server | One per browser tab | Server-shared during Server phase; per-tab after WASM | One per app instance |
| Scoped |
{
(firstRender)
{
SessionStorage.SetAsync(, cartItems);
result = SessionStorage.GetAsync<List<CartItem>>();
(result.Success) { cartItems = result.Value!; }
LocalStorage.SetAsync(, userPrefs);
}
}
```text
For InteractiveWebAssembly, use JS interop to access browser storage directly:
```csharp
JSRuntime.InvokeVoidAsync(, ,
JsonSerializer.Serialize(, AppJsonContext.Default.UserPrefs));
json = JSRuntime.InvokeAsync<?>(, );
(json )
{
= JsonSerializer.Deserialize(json, AppJsonContext.Default.UserPrefs);
}
```json
**Gotcha:** `ProtectedBrowserStorage` available during prerendering. Always access it
`OnAfterRenderAsync(firstRender: )`, never `OnInitializedAsync`.
---
```csharp
@inject IJSRuntime JSRuntime
JSRuntime.InvokeVoidAsync(, );
width = JSRuntime.InvokeAsync<>();
result = JSRuntime.InvokeAsync<>(
,
TimeSpan.FromSeconds(),
inputData);
```text
```csharp
IJSObjectReference? module;
{
(firstRender)
{
module = JSRuntime.InvokeAsync<IJSObjectReference>(
, );
module.InvokeVoidAsync(, elementRef);
}
}
{
(module )
{
module.DisposeAsync();
}
}
```text
```javascript
{
}
{
element.;
}
```text
```csharp
DotNetObjectReference<MyComponent>? dotNetRef;
{
dotNetRef = DotNetObjectReference.Create();
}
[]
{
message = data;
StateHasChanged();
}
{
dotNetRef?.Dispose();
}
```text
```javascript
{
document.addEventListener(, e => {
dotNetRef.invokeMethodAsync(, e.detail);
});
}
```text
| Concern | InteractiveServer | InteractiveWebAssembly | InteractiveAuto | Hybrid |
| ------------------------- | ----------------------------- | ------------------------------- | ----------------------------------------------------------------------- | ------------------------------- |
| JS call timing | After SignalR confirms render | After WASM runtime loads | SignalR initially, then direct after WASM | After WebView loads |
| `OnAfterRender` available | Yes | Yes | Yes | Yes |
| IJSRuntime sync calls | ; `IJSInProcessRuntime` after WASM | `IJSInProcessRuntime` available |
| Module imports | = OnValidSubmit= FormName=>
<DataAnnotationsValidator />
<ValidationSummary />
<div>
<label =>Name:</label>
<InputText id= @bind-Value= />
<ValidationMessage For= />
</div>
<div>
<label =>Price:</label>
<InputNumber id= @bind-Value= />
<ValidationMessage For= />
</div>
<div>
<label =>Category:</label>
<InputSelect id= @bind-Value=>
<option =>Select...</option>
<option =>Electronics</option>
<option =>Clothing</option>
</InputSelect>
<ValidationMessage For= />
</div>
<button type=>Save</button>
</EditForm>
@code {
ProductModel product = ();
{
ProductService.CreateAsync(product);
Navigation.NavigateTo();
}
}
```text
```csharp
{
[]
[]
Name { ; ; } = ;
[]
Price { ; ; }
[]
Category { ; ; } = ;
}
```text
Static SSR forms require `FormName` use `[SupplyParameterFromForm]`:
```razor
@page
<EditForm Model= OnValidSubmit= FormName= Enhance>
<DataAnnotationsValidator />
<!-- form fields -->
<button type=>Create</button>
</EditForm>
@code {
[]
ProductModel product { ; ; } = ();
{
ProductService.CreateAsync(product);
Navigation.NavigateTo();
}
}
```text
The `Enhance` attribute enables enhanced form handling -- the form submits via fetch patches the DOM without a full
page reload.
**Gotcha:** `FormName` must be unique across all forms the page. Duplicate `FormName` values cause ambiguous form
submission errors.
---
QuickGrid a high-=>
<PropertyColumn Property= Sortable= />
<PropertyColumn Property= Format= Sortable= />
<PropertyColumn Property= Sortable= />
<TemplateColumn Title=>
<button @onclick=>Edit</button>
</TemplateColumn>
</QuickGrid>
@code {
IQueryable<Product> products = Enumerable.Empty<Product>().AsQueryable();
{
list = ProductService.GetAllAsync();
products = list.AsQueryable();
}
=> Navigation.NavigateTo();
}
```text
```razor
<QuickGrid Items= Pagination=>
<PropertyColumn Property= Sortable= />
<PropertyColumn Property= Format= />
</QuickGrid>
<Paginator State= />
@code {
PaginationState pagination = () { ItemsPerPage = };
IQueryable<Product> products = !;
}
```text
For large datasets, virtualization renders only visible rows:
```razor
<QuickGrid Items= Virtualize= ItemSize=>
<PropertyColumn Property= />
<PropertyColumn Property= Format= />
</QuickGrid>
```text
<!-- net11-preview -->
.NET adds `OnRowClick` to QuickGrid row-level click handling without template columns:
```razor
<QuickGrid Items= OnRowClick=>
<PropertyColumn Property= />
<PropertyColumn Property= Format= />
</QuickGrid>
@code {
{
Navigation.NavigateTo();
}
}
```text
**Fallback (net10):** Use a `TemplateColumn` a click handler wrap each row a clickable element.
Source:
[](https:
---
<!-- net11-preview -->
`EnvironmentBoundary` =>
<p>Debug panel -- only visible Development</p>
<DebugToolbar />
</EnvironmentBoundary>
<EnvironmentBoundary Exclude=>
<p>Testing controls -- hidden Production</p>
</EnvironmentBoundary>
```text
**Fallback (net10):** Inject `IWebHostEnvironment` use conditional rendering `@code`.
Source:
[](https:
.NET adds `[DisplayName]` support input components, automatically generating `<label>` elements:
```razor
<EditForm Model= FormName=>
<!-- Automatically renders <label> [DisplayName] -->
<InputText @bind-Value= />
<InputText @bind-Value= />
</EditForm>
@code {
ContactModel model = ();
}
{
[]
[]
FullName { ; ; } = ;
[]
[]
EmailAddress { ; ; } = ;
}
```text
**Fallback (net10):** Add `<label =>` elements manually.
Source:
[](https:
.NET allows `IHostedService` implementations to run Blazor WebAssembly, enabling background tasks the browser:
```csharp
builder.Services.AddHostedService<DataSyncService>();
:
{
{
(!stoppingToken.IsCancellationRequested)
{
SyncDataFromServer();
Task.Delay(TimeSpan.FromMinutes(), stoppingToken);
}
}
}
```text
**Fallback (net10):** Use a `Timer` a component inject a singleton service that starts background work first
use.
Source:
[](https:
<!-- net11-preview -->
.NET adds `ConfigureConnection` to the Blazor Server circuit hub, ;
```text
**Fallback (net10):** Use `IHubFilter` middleware to inspect/modify connections at the hub level.
Source:
[](https:
---
**Do call JS interop `OnInitializedAsync`.** The DOM available yet. Use
`OnAfterRenderAsync(firstRender: )` JS calls that need DOM elements.
**Do forget `StateHasChanged()` after external state changes.** When state changes a non-
// One-time initialization (runs once per component instance)
// 3. Called after parameters are assigned (async)
protectedoverrideasync Task OnInitializedAsync()
// Async initialization (data fetching, service calls)
Called when circuit ends (Server phase) oronremoval (WASM phase) | Called on component removal |
**Gotcha:** In Static SSR, `OnAfterRender` never executes because there is no persistent connection. Do not place
critical logic in `OnAfterRender` for Static SSR pages.
---
## State Management
### Cascading Values
Cascading values flow data down the component tree without explicit parameter passing.
```razor
<!-- Parent: provide a cascading value -->
<CascadingValue Value
"@theme"
"AppTheme"
"typeof(App).Assembly"
private
new
false
"#0078d4"
value
CascadingParameter(Name = "AppTheme")
public
get
set
Fixed cascading values (.NET 8+):** For values that never change after initial render, use `IsFixed
"true"
"@config"
"true"
### Dependency Injection
// Register services in Program.cs
// Inject in components
on
switch
One per circuit (acts like per-user) | One per browser tab (same as Singleton) | Per-circuit (Server phase), per-tab (WASM phase) -- state does not transfer between phases | One per app instance (same as Singleton) |
| Transient | New instance each injection | New instance each injection | New instance each injection | New instance each injection |
**Gotcha:** In Blazor Server, `Scoped` services live for the entire circuit duration (not per-request like in MVC). A
circuit persists until the user navigates away or the connection drops. Long-lived scoped services may accumulate state
-- use `OwningComponentBase<T>` for component-scoped DI.
### Browser Storage
```csharp
// ProtectedBrowserStorage -- encrypted, per-user storage// Available in InteractiveServer only (not WASM -- server encrypts/decrypts)
@inject ProtectedSessionStorage SessionStorage
@inject ProtectedLocalStorage LocalStorage
protectedoverrideasync Task OnAfterRenderAsync(bool firstRender)
if
// Session storage (cleared when tab closes)
await
"cart"
var
await
"cart"
if
// Local storage (persists across sessions)
await
"preferences"
// WASM: Direct browser storage via JS interop
await
"localStorage.setItem"
"key"
value
var
await
string
"localStorage.getItem"
"key"
if
is
not
null
value
is
not
in
true
in
## JavaScript Interop
### Calling JavaScript from .NET
// Invoke a global JS function
await
"console.log"
"Hello from Blazor"
// Invoke and get a return value
var
await
int
"getWindowWidth"
// With timeout (important for Server to avoid hanging circuits)
Not supported (async only) | `IJSInProcessRuntime` available | Async-only during Server phase
switch
Via SignalR (latency) | Direct (fast) | SignalR (Server phase), direct (WASM phase) | Direct (fast) |
**Gotcha:** In InteractiveServer, all JS interop calls travel over SignalR, adding network latency. Minimize round trips
by batching operations into a single JS function call.
---
## EditForm Validation
### Basic EditForm with Data Annotations
```razor
<EditForm Model
"product"
"HandleSubmit"
"product-form"
for
"name"
"name"
"product.Name"
"() => product.Name"
for
"price"
"price"
"product.Price"
"() => product.Price"
for
"category"
"category"
"product.Category"
value
""
value
"Electronics"
value
"Clothing"
"() => product.Category"
"submit"
private
new
privateasync Task HandleSubmit()
await
"/products"
### Model with Validation Attributes
public
sealed
class
ProductModel
Required(ErrorMessage = "Product name is required")
StringLength(200, MinimumLength = 1)
public
string
get
set
""
Range(0.01, 1_000_000, ErrorMessage = "Price must be between {1} and {2}")
public
decimal
get
set
Required(ErrorMessage = "Category is required")
public
string
get
set
""
### EditForm with Enhanced Form Handling (.NET 8+)
and
"/products/create"
"product"
"HandleSubmit"
"create-product"
"submit"
SupplyParameterFromForm
private
get
set
new
privateasync Task HandleSubmit()
await
"/products"
and
on
## QuickGrid
is
performance grid component built intoBlazor (.NET 8+). It supports sorting, filtering, pagination,
and virtualization.
### Basic QuickGrid
```razor
@using Microsoft.AspNetCore.Components.QuickGrid
<QuickGrid Items
Blazor context
(timer, event handler, JS callback), call `StateHasChanged()` or `InvokeAsync(StateHasChanged)` to trigger re-render.
3. **Do not use `ProtectedBrowserStorage` during prerendering.** It throws because no interactive circuit exists yet.
Access it only in `OnAfterRenderAsync`.
4. **Do not forget `FormName` on Static SSR forms.** Without it, form submissions in Static SSR mode are not routed to
the correct handler.
5. **Do not dispose `DotNetObjectReference` before JS is done with it.** Premature disposal causes `JSException` when
JavaScript tries to invoke the callback. Dispose in `Dispose()` or `DisposeAsync()`.
6. **Do not assume Scoped services are per-request in Blazor Server.** Scoped services live for the entire circuit. Use
`OwningComponentBase<T>` when you need component-scoped service lifetimes.
---
## Prerequisites
- .NET 8.0+ (QuickGrid, enhanced form handling, cascading values with `IsFixed`)
- `Microsoft.AspNetCore.Components.QuickGrid` package for QuickGrid
- .NET 11 preview for EnvironmentBoundary, Label/DisplayName, QuickGrid OnRowClick, IHostedService in WASM
---
## Knowledge Sources
Blazor component patterns inthis skill are grounded in guidance from:
- **Damian Edwards** -- Razor and Blazor component design patterns, render mode architecture, and performance best
practices. Principal architect on the ASP.NET team.
> These sources inform the patterns and rationale presented above. This skill does not claim to represent or speak for
> any individual.
---
## Code Navigation (Serena MCP)
**Primary approach:** Use Serena symbol operations for efficient code navigation:
1. **Find definitions**: `serena_find_symbol` instead of text search
2. **Understand structure**: `serena_get_symbols_overview` forfile organization
3. **Track references**: `serena_find_referencing_symbols` for impact analysis
4. **Precise edits**: `serena_replace_symbol_body` for clean modifications
**When to use Serena vs traditional tools:**
- ✅ **Use Serena**: Navigation, refactoring, dependency analysis, precise edits
- ✅ **Use Read/Grep**: Reading full files, pattern matching, simple text operations
- ✅ **Fallback**: If Serena unavailable, traditional tools work fine
**Example workflow:**
```text
# Instead of:
Read: src/Services/OrderService.cs
Grep: "publicvoid ProcessOrder"
# Use:
serena_find_symbol: "OrderService/ProcessOrder"
serena_get_symbols_overview: "src/Services/OrderService.cs"
```
## References
- [Blazor Component Lifecycle](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/lifecycle?view=aspnetcore-10.0)
- [Blazor State Management](https://learn.microsoft.com/en-us/aspnet/core/blazor/state-management?view=aspnetcore-10.0)
- [Blazor JS Interop](https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/?view=aspnetcore-10.0)
- [Blazor Forms and Validation](https://learn.microsoft.com/en-us/aspnet/core/blazor/forms/?view=aspnetcore-10.0)
- [QuickGrid Component](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/quickgrid?view=aspnetcore-10.0)
- [Cascading Values and Parameters](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/cascading-values-and-parameters?view=aspnetcore-10.0)