dotnet-blazor-patterns
Blazor hosting models, render modes, project setup, routing, enhanced navigation, streaming rendering, and AOT-safe
patterns. Covers all five hosting models (InteractiveServer, InteractiveWebAssembly, InteractiveAuto, Static SSR,
Hybrid) with trade-off analysis for each.
Scope
- Blazor Web App project setup and configuration
- Hosting model selection (Server, WASM, Auto, SSR, Hybrid)
- Render mode configuration (global, per-page, per-component)
- Routing and enhanced navigation
- Streaming rendering and prerendering
- AOT-safe Blazor patterns
Out of scope
- Component architecture (lifecycle, state, JS interop) -- see [skill:dotnet-blazor-components]
- Authentication across hosting models -- see [skill:dotnet-blazor-auth]
- bUnit component testing -- see [skill:dotnet-blazor-testing]
- Standalone SignalR patterns -- see [skill:dotnet-realtime-communication]
- Browser-based E2E testing -- see [skill:dotnet-playwright]
- UI framework selection decision tree -- see [skill:dotnet-ui-chooser]
Cross-references: [skill:dotnet-blazor-components] for component architecture, [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).
Hosting Models & Render Modes
Blazor Web App (.NET 8+) is the default project template, replacing the separate Blazor Server and Blazor WebAssembly
templates. Render modes can be set globally, per-page, or per-component.
Render Mode Overview
| Render Mode |
Attribute |
Interactivity |
Connection |
Best For |
| Static SSR |
(none / default) |
None -- server renders HTML, no interactivity |
HTTP request only |
Content pages, SEO, forms with minimal interactivity |
| InteractiveServer |
@rendermode InteractiveServer |
Full |
SignalR circuit |
Low-latency interactivity, full server access, small user base |
| InteractiveWebAssembly |
@rendermode InteractiveWebAssembly |
Full (after download) |
None (runs in browser) |
Offline-capable, large user base, reduced server load |
| InteractiveAuto |
@rendermode InteractiveAuto |
Full |
SignalR initially, then WASM |
Best of both -- immediate interactivity, eventual client-side |
| Blazor Hybrid |
BlazorWebView in MAUI/WPF/WinForms |
Full (native) |
None (runs in-process) |
Desktop/mobile apps with web UI, native API access |
Per-Mode Trade-offs
| Concern |
Static SSR |
InteractiveServer |
InteractiveWebAssembly |
InteractiveAuto |
Hybrid |
| First load |
Fast |
Fast |
Slow (WASM download) |
Fast (Server first) |
Instant (local) |
| Server resources |
Minimal |
Per-user circuit |
None after download |
Circuit then none |
None |
| Offline support |
No |
No |
Yes |
Partial |
Yes |
| Full .NET API access |
Yes (server) |
Yes (server) |
Limited (browser sandbox) |
Varies by phase |
Yes (native) |
| Scalability |
High |
Limited by circuits |
High |
High (after WASM) |
N/A (local) |
| SEO |
Yes |
Prerender |
Prerender |
Prerender |
N/A |
Setting Render Modes
Global (App.razor):
<!-- Sets default render mode for all pages -->
<Routes @rendermode="InteractiveServer" />
```text
**Per-page:**
```razor
@page "/dashboard"
@rendermode InteractiveServer
<h1>Dashboard</h1>
```text
**Per-component:**
```razor
<Counter @rendermode="InteractiveWebAssembly" />
```text
**Gotcha:** Without an explicit render mode boundary, a child component cannot request a more interactive render mode
than its parent. However, interactive islands are supported: you can place an `@rendermode` attribute on a component
embedded in a Static SSR page to create a render mode boundary, enabling interactive children under otherwise static
content.
---
## Project Setup
### Blazor Web App (Default Template)
```bash
# Creates a Blazor Web App with InteractiveServer render mode
dotnet new blazor -n MyApp
# With specific interactivity options
dotnet new blazor -n MyApp --interactivity Auto # InteractiveAuto
dotnet new blazor -n MyApp --interactivity WebAssembly # InteractiveWebAssembly
dotnet new blazor -n MyApp --interactivity Server # InteractiveServer (default)
dotnet new blazor -n MyApp --interactivity None # Static SSR only
```text
### Blazor Web App Project Structure
```text
MyApp/
MyApp/ # Server project
Program.cs # Host builder, services, middleware
Components/
App.razor # Root component (sets global render mode)
Routes.razor # Router component
Layout/
MainLayout.razor # Main layout
Pages/
Home.razor # Static SSR by default
Counter.razor # Can set per-page render mode
MyApp.Client/ # Client project (only if WASM or Auto)
Pages/
Counter.razor # Components that run in browser
Program.cs # WASM entry point
```csharp
When using InteractiveAuto or InteractiveWebAssembly, components that must run in the browser go in the `.Client`
project. Components in the server project run on the server only.
### Blazor Hybrid Setup (MAUI)
```xml
<!-- .csproj for MAUI Blazor Hybrid -->
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFrameworks>net10.0-android;net10.0-ios;net10.0-maccatalyst</TargetFrameworks>
<OutputType>Exe</OutputType>
<UseMaui>true</UseMaui>
</PropertyGroup>
</Project>
```text
```csharp
// MainPage.xaml.cs hosts BlazorWebView
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
}
}
```text
```xml
<!-- MainPage.xaml -->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:b="clr-namespace:Microsoft.AspNetCore.Components.WebView.Maui;assembly=Microsoft.AspNetCore.Components.WebView.Maui">
<b:BlazorWebView HostPage="wwwroot/index.html">
<b:BlazorWebView.RootComponents>
<b:RootComponent Selector="#app" ComponentType="{x:Type local:Routes}" />
</b:BlazorWebView.RootComponents>
</b:BlazorWebView>
</ContentPage>
```text
---
## Routing
### Basic Routing
```razor
@page "/products"
@page "/products/{Category}"
<h1>Products</h1>
@if (!string.IsNullOrEmpty(Category))
{
<p>Category: @Category</p>
}
@code {
[Parameter]
public string? Category { get; set; }
}
```text
### Route Constraints
```razor
@page "/products/{Id:int}"
@page "/orders/{Date:datetime}"
@page "/search/{Query:minlength(3)}"
@code {
[Parameter] public int Id { get; set; }
[Parameter] public DateTime Date { get; set; }
[Parameter] public string Query { get; set; } = "";
}
```text
### Query String Parameters
```razor
@page "/search"
@code {
[SupplyParameterFromQuery]
public string? Term { get; set; }
[SupplyParameterFromQuery(Name = "page")]
public int CurrentPage { get; set; } = 1;
}
```text
### NavigationManager
```csharp
@inject NavigationManager Navigation
// Programmatic navigation
Navigation.NavigateTo("/products/electronics");
// With query string
Navigation.NavigateTo("/search?term=keyboard&page=2");
// Force full page reload (bypasses enhanced navigation)
Navigation.NavigateTo("/external-page", forceLoad: true);
```text
---
## Enhanced Navigation (.NET 8+)
Enhanced navigation intercepts link clicks and form submissions to update only the changed DOM content, preserving page
state and avoiding full page reloads. This applies to Static SSR and prerendered pages.
### How It Works
1. User clicks a link within the Blazor app
2. Blazor intercepts the navigation
3. A fetch request loads the new page content
4. Blazor patches the DOM with only the differences
5. Scroll position and focus state are preserved
### Opting Out
```razor
<!-- Disable enhanced navigation for a specific link -->
<a href="/legacy-page" data-enhance-nav="false">Legacy Page</a>
<!-- Disable enhanced form handling for a specific form -->
<form method="post" data-enhance="false">
...
</form>
```text
**Gotcha:** Enhanced navigation may interfere with third-party JavaScript libraries that expect full page loads. Use
`data-enhance-nav="false"` on links that navigate to pages with JS that initializes on `DOMContentLoaded`.
---
## Streaming Rendering (.NET 8+)
Streaming rendering sends initial HTML immediately (with placeholder content), then streams updates as async operations
complete. Useful for pages with slow data sources.
```razor
@page "/dashboard"
@attribute [StreamRendering]
<h1>Dashboard</h1>
@if (orders is null)
{
<p>Loading orders...</p>
}
else
{
<table>
@foreach (var order in orders)
{
<tr><td>@order.Id</td><td>@order.Total</td></tr>
}
</table>
}
@code {
private List<OrderDto>? orders;
protected override async Task OnInitializedAsync()
{
// Initial HTML sent immediately with "Loading orders..."
// Updated HTML streamed when this completes
orders = await OrderService.GetRecentOrdersAsync();
}
}
```text
**Behavior per render mode:**
- **Static SSR:** Streaming rendering sends the initial response, then patches the DOM via chunked transfer encoding.
The page is not interactive.
- **InteractiveServer/WebAssembly/Auto:** Streaming rendering is less impactful because components re-render
automatically after async operations. The `[StreamRendering]` attribute primarily benefits the prerender phase.
---
## AOT-Safe Patterns
When targeting Blazor WebAssembly with Native AOT (ahead-of-time compilation) or IL trimming, avoid patterns that rely
on runtime reflection.
### Source-Generator-First Serialization
```csharp
// CORRECT: Source-generated JSON serialization (AOT-compatible)
[JsonSerializable(typeof(ProductDto))]
[JsonSerializable(typeof(List<ProductDto>))]
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
public partial class AppJsonContext : JsonSerializerContext { }
// Register in Program.cs
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default);
});
// Usage in HttpClient calls
var products = await Http.GetFromJsonAsync<List<ProductDto>>(
"/api/products",
AppJsonContext.Default.ListProductDto);
```json
```csharp
// WRONG: Reflection-based serialization (fails under AOT/trimming)
var products = await Http.GetFromJsonAsync<List<ProductDto>>("/api/products");
```csharp
### Trim-Safe JS Interop
```csharp
// CORRECT: Use IJSRuntime with explicit method names (no dynamic dispatch)
await JSRuntime.InvokeVoidAsync("localStorage.setItem", "key", "value");
var value = await JSRuntime.InvokeAsync<string>("localStorage.getItem", "key");
// CORRECT: Use IJSObjectReference for module imports (.NET 8+)
var module = await JSRuntime.InvokeAsync<IJSObjectReference>(
"import", "./js/chart.js");
await module.InvokeVoidAsync("initChart", elementRef, data);
await module.DisposeAsync();
```javascript
```csharp
// WRONG: Dynamic dispatch via reflection (trimmed away)
// var method = typeof(JSRuntime).GetMethod("InvokeAsync");
// method.MakeGenericMethod(returnType).Invoke(...)
```csharp
### Linker Configuration
```xml
<!-- Preserve types used dynamically in components -->
<ItemGroup>
<TrimmerRootAssembly Include="MyApp.Client" />
</ItemGroup>
```text
For types that must be preserved from trimming:
```csharp
// Mark types that are accessed via reflection
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
public class DynamicFormModel
{
// Properties discovered at runtime for form generation
public string Name { get; set; } = "";
public int Age { get; set; }
}
```text
### Anti-Patterns to Avoid
1. **Reflection-based DI** -- Do not use `Activator.CreateInstance` or `Type.GetType` to resolve services. Use the
built-in DI container with explicit registrations.
2. **Dynamic type loading** -- Do not use `Assembly.Load` or `Assembly.GetTypes()` at runtime. Register all types at
startup.
3. **Runtime code generation** -- Do not use `System.Reflection.Emit` or `System.Linq.Expressions.Expression.Compile()`.
Use source generators instead.
4. **Untyped JSON deserialization** -- Do not use `JsonSerializer.Deserialize<T>(json)` without a
`JsonSerializerContext`. Always provide a source-generated context.
---
## Prerendering
Prerendering generates HTML on the server before the interactive runtime loads. This improves perceived performance and
SEO.
### Prerender with Interactive Modes
```razor
<!-- Component prerenders on server, then becomes interactive -->
<Counter @rendermode="InteractiveServer" />
```text
By default, interactive components prerender. To disable:
```razor
@rendermode @(new InteractiveServerRenderMode(prerender: false))
```text
### Persisting State Across Prerender
State computed during prerendering is lost when the component reinitializes interactively. Use
`PersistentComponentState` to preserve it:
```razor
@inject PersistentComponentState ApplicationState
@implements IDisposable
@code {
private List<ProductDto>? products;
private PersistingComponentStateSubscription _subscription;
protected override async Task OnInitializedAsync()
{
_subscription = ApplicationState.RegisterOnPersisting(PersistState);
if (!ApplicationState.TryTakeFromJson<List<ProductDto>>(
"products", out var restored))
{
products = await ProductService.GetProductsAsync();
}
else
{
products = restored;
}
}
private Task PersistState()
{
ApplicationState.PersistAsJson("products", products);
return Task.CompletedTask;
}
public void Dispose() => _subscription.Dispose();
}
```text
---
## .NET 10 Stable Features
These features are available when `net10.0` TFM is detected. They are stable and require no preview opt-in.
### WebAssembly Preloading
.NET 10 adds `blazor.web.js` preloading of WebAssembly assemblies during the Server phase of InteractiveAuto. When the
user first loads a page, the WASM runtime and app assemblies download in the background while the Server circuit handles
interactivity. Subsequent navigations switch to WASM faster because assemblies are already cached.
```razor
<!-- No code changes needed -- preloading is automatic in .NET 10 -->
<!-- Verify in browser DevTools Network tab: assemblies download during Server phase -->
```text
### Enhanced Form Validation
.NET 10 extends `EditForm` validation with improved error message formatting and support for `IValidatableObject` in
Static SSR forms. Validation messages render correctly with enhanced form handling (`Enhance` attribute) without
requiring a full page reload.
```csharp
// IValidatableObject works in Static SSR enhanced forms in .NET 10
public sealed class OrderModel : IValidatableObject
{
[Required]
public string ProductId { get; set; } = "";
[Range(1, 100)]
public int Quantity { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext context)
{
if (ProductId == "DISCONTINUED" && Quantity > 0)
{
yield return new ValidationResult(
"Cannot order discontinued products",
[nameof(ProductId), nameof(Quantity)]);
}
}
}
```text
### Blazor Diagnostics Middleware
.NET 10 adds `MapBlazorDiagnostics` middleware for inspecting Blazor circuit and component state in development:
```csharp
// Program.cs -- available in .NET 10
if (app.Environment.IsDevelopment())
{
app.MapBlazorDiagnostics(); // Exposes /_blazor/diagnostics endpoint
}
```text
The diagnostics endpoint shows active circuits, component tree, render mode assignments, and timing data. Use it to
debug render mode boundaries and component lifecycle issues during development.
---
## Agent Gotchas
1. **Do not default to InteractiveServer for every page.** Static SSR is the default and most efficient render mode.
Only add interactivity where user interaction requires it.
2. **Do not put WASM-targeted components in the server project.** Components that must run in the browser
(InteractiveWebAssembly or InteractiveAuto) belong in the `.Client` project.
3. **Do not forget `PersistentComponentState` when prerendering.** Without it, data fetched during prerender is
discarded and re-fetched when the component becomes interactive, causing a visible flicker.
4. **Do not use reflection-based serialization in WASM.** Always use `JsonSerializerContext` with source-generated
serializers for AOT compatibility and trimming safety.
5. **Do not force-load navigation unless leaving the Blazor app.** `NavigateTo("/page", forceLoad: true)` bypasses
enhanced navigation and causes a full page reload.
6. **Do not nest interactive render modes incorrectly.** A child component cannot request a more interactive mode than
its parent. Plan render mode boundaries from the layout down.
---
## Prerequisites
- .NET 8.0+ (Blazor Web App template, render modes, enhanced navigation, streaming rendering)
- .NET 10.0 for stable enhanced features (WebAssembly preloading, enhanced form validation, diagnostics middleware)
- MAUI workload for Blazor Hybrid (`dotnet workload install maui`)
---
## References
- [Blazor Overview](https://learn.microsoft.com/en-us/aspnet/core/blazor/?view=aspnetcore-10.0)
- [Blazor Render Modes](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes?view=aspnetcore-10.0)
- [Blazor Routing](https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/routing?view=aspnetcore-10.0)
- [Enhanced Navigation](https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/routing?view=aspnetcore-10.0#enhanced-navigation-and-form-handling)
- [Streaming Rendering](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/rendering?view=aspnetcore-10.0#streaming-rendering)
- [Blazor Hybrid](https://learn.microsoft.com/en-us/aspnet/core/blazor/hybrid/?view=aspnetcore-10.0)
- [AOT Deployment](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/)
1---2name: dotnet-blazor-patterns-23description: Architects Blazor apps. Hosting models, render modes, routing, streaming, prerender.4---5# dotnet-blazor-patterns67Blazor hosting models, render modes, project setup, routing, enhanced navigation, streaming rendering, and AOT-safe8patterns. Covers all five hosting models (InteractiveServer, InteractiveWebAssembly, InteractiveAuto, Static SSR,9Hybrid) with trade-off analysis for each.1011## Scope1213- Blazor Web App project setup and configuration14- Hosting model selection (Server, WASM, Auto, SSR, Hybrid)15- Render mode configuration (global, per-page, per-component)16- Routing and enhanced navigation17- Streaming rendering and prerendering18- AOT-safe Blazor patterns1920## Out of scope2122- Component architecture (lifecycle, state, JS interop) -- see [skill:dotnet-blazor-components]23- Authentication across hosting models -- see [skill:dotnet-blazor-auth]24- bUnit component testing -- see [skill:dotnet-blazor-testing]25- Standalone SignalR patterns -- see [skill:dotnet-realtime-communication]26- Browser-based E2E testing -- see [skill:dotnet-playwright]27- UI framework selection decision tree -- see [skill:dotnet-ui-chooser]2829Cross-references: [skill:dotnet-blazor-components] for component architecture, [skill:dotnet-blazor-auth] for30authentication, [skill:dotnet-blazor-testing] for bUnit testing, [skill:dotnet-realtime-communication] for standalone31SignalR, [skill:dotnet-playwright] for E2E testing, [skill:dotnet-ui-chooser] for framework selection,32[skill:dotnet-accessibility] for accessibility patterns (ARIA, keyboard nav, screen readers).3334---3536## Hosting Models & Render Modes3738Blazor Web App (.NET 8+) is the default project template, replacing the separate Blazor Server and Blazor WebAssembly39templates. Render modes can be set globally, per-page, or per-component.4041### Render Mode Overview4243| Render Mode | Attribute | Interactivity | Connection | Best For |44| ---------------------- | ------------------------------------ | --------------------------------------------- | ---------------------------- | -------------------------------------------------------------- |45| Static SSR | (none / default) | None -- server renders HTML, no interactivity | HTTP request only | Content pages, SEO, forms with minimal interactivity |46| InteractiveServer | `@rendermode InteractiveServer` | Full | SignalR circuit | Low-latency interactivity, full server access, small user base |47| InteractiveWebAssembly | `@rendermode InteractiveWebAssembly` | Full (after download) | None (runs in browser) | Offline-capable, large user base, reduced server load |48| InteractiveAuto | `@rendermode InteractiveAuto` | Full | SignalR initially, then WASM | Best of both -- immediate interactivity, eventual client-side |49| Blazor Hybrid | `BlazorWebView` in MAUI/WPF/WinForms | Full (native) | None (runs in-process) | Desktop/mobile apps with web UI, native API access |5051### Per-Mode Trade-offs5253| Concern | Static SSR | InteractiveServer | InteractiveWebAssembly | InteractiveAuto | Hybrid |54| -------------------- | ------------ | ------------------- | ------------------------- | ------------------- | --------------- |55| First load | Fast | Fast | Slow (WASM download) | Fast (Server first) | Instant (local) |56| Server resources | Minimal | Per-user circuit | None after download | Circuit then none | None |57| Offline support | No | No | Yes | Partial | Yes |58| Full .NET API access | Yes (server) | Yes (server) | Limited (browser sandbox) | Varies by phase | Yes (native) |59| Scalability | High | Limited by circuits | High | High (after WASM) | N/A (local) |60| SEO | Yes | Prerender | Prerender | Prerender | N/A |6162### Setting Render Modes6364**Global (App.razor):**6566````razor6768<!-- Sets default render mode for all pages -->69<Routes @rendermode="InteractiveServer" />7071```text7273**Per-page:**7475```razor7677@page "/dashboard"78@rendermode InteractiveServer7980<h1>Dashboard</h1>8182```text8384**Per-component:**8586```razor8788<Counter @rendermode="InteractiveWebAssembly" />8990```text9192**Gotcha:** Without an explicit render mode boundary, a child component cannot request a more interactive render mode93than its parent. However, interactive islands are supported: you can place an `@rendermode` attribute on a component94embedded in a Static SSR page to create a render mode boundary, enabling interactive children under otherwise static95content.9697---9899## Project Setup100101### Blazor Web App (Default Template)102103```bash104105# Creates a Blazor Web App with InteractiveServer render mode106dotnet new blazor -n MyApp107108# With specific interactivity options109dotnet new blazor -n MyApp --interactivity Auto # InteractiveAuto110dotnet new blazor -n MyApp --interactivity WebAssembly # InteractiveWebAssembly111dotnet new blazor -n MyApp --interactivity Server # InteractiveServer (default)112dotnet new blazor -n MyApp --interactivity None # Static SSR only113114```text115116### Blazor Web App Project Structure117118```text119120MyApp/121 MyApp/ # Server project122 Program.cs # Host builder, services, middleware123 Components/124 App.razor # Root component (sets global render mode)125 Routes.razor # Router component126 Layout/127 MainLayout.razor # Main layout128 Pages/129 Home.razor # Static SSR by default130 Counter.razor # Can set per-page render mode131 MyApp.Client/ # Client project (only if WASM or Auto)132 Pages/133 Counter.razor # Components that run in browser134 Program.cs # WASM entry point135136```csharp137138When using InteractiveAuto or InteractiveWebAssembly, components that must run in the browser go in the `.Client`139project. Components in the server project run on the server only.140141### Blazor Hybrid Setup (MAUI)142143```xml144145<!-- .csproj for MAUI Blazor Hybrid -->146<Project Sdk="Microsoft.NET.Sdk.Razor">147 <PropertyGroup>148 <TargetFrameworks>net10.0-android;net10.0-ios;net10.0-maccatalyst</TargetFrameworks>149 <OutputType>Exe</OutputType>150 <UseMaui>true</UseMaui>151 </PropertyGroup>152</Project>153154```text155156```csharp157158// MainPage.xaml.cs hosts BlazorWebView159public partial class MainPage : ContentPage160{161 public MainPage()162 {163 InitializeComponent();164 }165}166167```text168169```xml170171<!-- MainPage.xaml -->172<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"173 xmlns:b="clr-namespace:Microsoft.AspNetCore.Components.WebView.Maui;assembly=Microsoft.AspNetCore.Components.WebView.Maui">174 <b:BlazorWebView HostPage="wwwroot/index.html">175 <b:BlazorWebView.RootComponents>176 <b:RootComponent Selector="#app" ComponentType="{x:Type local:Routes}" />177 </b:BlazorWebView.RootComponents>178 </b:BlazorWebView>179</ContentPage>180181```text182183---184185## Routing186187### Basic Routing188189```razor190191@page "/products"192@page "/products/{Category}"193194<h1>Products</h1>195@if (!string.IsNullOrEmpty(Category))196{197 <p>Category: @Category</p>198}199200@code {201 [Parameter]202 public string? Category { get; set; }203}204205```text206207### Route Constraints208209```razor210211@page "/products/{Id:int}"212@page "/orders/{Date:datetime}"213@page "/search/{Query:minlength(3)}"214215@code {216 [Parameter] public int Id { get; set; }217 [Parameter] public DateTime Date { get; set; }218 [Parameter] public string Query { get; set; } = "";219}220221```text222223### Query String Parameters224225```razor226227@page "/search"228229@code {230 [SupplyParameterFromQuery]231 public string? Term { get; set; }232233 [SupplyParameterFromQuery(Name = "page")]234 public int CurrentPage { get; set; } = 1;235}236237```text238239### NavigationManager240241```csharp242243@inject NavigationManager Navigation244245// Programmatic navigation246Navigation.NavigateTo("/products/electronics");247248// With query string249Navigation.NavigateTo("/search?term=keyboard&page=2");250251// Force full page reload (bypasses enhanced navigation)252Navigation.NavigateTo("/external-page", forceLoad: true);253254```text255256---257258## Enhanced Navigation (.NET 8+)259260Enhanced navigation intercepts link clicks and form submissions to update only the changed DOM content, preserving page261state and avoiding full page reloads. This applies to Static SSR and prerendered pages.262263### How It Works2642651. User clicks a link within the Blazor app2662. Blazor intercepts the navigation2673. A fetch request loads the new page content2684. Blazor patches the DOM with only the differences2695. Scroll position and focus state are preserved270271### Opting Out272273```razor274275<!-- Disable enhanced navigation for a specific link -->276<a href="/legacy-page" data-enhance-nav="false">Legacy Page</a>277278<!-- Disable enhanced form handling for a specific form -->279<form method="post" data-enhance="false">280 ...281</form>282283```text284285**Gotcha:** Enhanced navigation may interfere with third-party JavaScript libraries that expect full page loads. Use286`data-enhance-nav="false"` on links that navigate to pages with JS that initializes on `DOMContentLoaded`.287288---289290## Streaming Rendering (.NET 8+)291292Streaming rendering sends initial HTML immediately (with placeholder content), then streams updates as async operations293complete. Useful for pages with slow data sources.294295```razor296297@page "/dashboard"298@attribute [StreamRendering]299300<h1>Dashboard</h1>301302@if (orders is null)303{304 <p>Loading orders...</p>305}306else307{308 <table>309 @foreach (var order in orders)310 {311 <tr><td>@order.Id</td><td>@order.Total</td></tr>312 }313 </table>314}315316@code {317 private List<OrderDto>? orders;318319 protected override async Task OnInitializedAsync()320 {321 // Initial HTML sent immediately with "Loading orders..."322 // Updated HTML streamed when this completes323 orders = await OrderService.GetRecentOrdersAsync();324 }325}326327```text328329**Behavior per render mode:**330331- **Static SSR:** Streaming rendering sends the initial response, then patches the DOM via chunked transfer encoding.332 The page is not interactive.333- **InteractiveServer/WebAssembly/Auto:** Streaming rendering is less impactful because components re-render334 automatically after async operations. The `[StreamRendering]` attribute primarily benefits the prerender phase.335336---337338## AOT-Safe Patterns339340When targeting Blazor WebAssembly with Native AOT (ahead-of-time compilation) or IL trimming, avoid patterns that rely341on runtime reflection.342343### Source-Generator-First Serialization344345```csharp346347// CORRECT: Source-generated JSON serialization (AOT-compatible)348[JsonSerializable(typeof(ProductDto))]349[JsonSerializable(typeof(List<ProductDto>))]350[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]351public partial class AppJsonContext : JsonSerializerContext { }352353// Register in Program.cs354builder.Services.ConfigureHttpJsonOptions(options =>355{356 options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default);357});358359// Usage in HttpClient calls360var products = await Http.GetFromJsonAsync<List<ProductDto>>(361 "/api/products",362 AppJsonContext.Default.ListProductDto);363364```json365366```csharp367368// WRONG: Reflection-based serialization (fails under AOT/trimming)369var products = await Http.GetFromJsonAsync<List<ProductDto>>("/api/products");370371```csharp372373### Trim-Safe JS Interop374375```csharp376377// CORRECT: Use IJSRuntime with explicit method names (no dynamic dispatch)378await JSRuntime.InvokeVoidAsync("localStorage.setItem", "key", "value");379var value = await JSRuntime.InvokeAsync<string>("localStorage.getItem", "key");380381// CORRECT: Use IJSObjectReference for module imports (.NET 8+)382var module = await JSRuntime.InvokeAsync<IJSObjectReference>(383 "import", "./js/chart.js");384await module.InvokeVoidAsync("initChart", elementRef, data);385await module.DisposeAsync();386387```javascript388389```csharp390391// WRONG: Dynamic dispatch via reflection (trimmed away)392// var method = typeof(JSRuntime).GetMethod("InvokeAsync");393// method.MakeGenericMethod(returnType).Invoke(...)394395```csharp396397### Linker Configuration398399```xml400401<!-- Preserve types used dynamically in components -->402<ItemGroup>403 <TrimmerRootAssembly Include="MyApp.Client" />404</ItemGroup>405406```text407408For types that must be preserved from trimming:409410```csharp411412// Mark types that are accessed via reflection413[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]414public class DynamicFormModel415{416 // Properties discovered at runtime for form generation417 public string Name { get; set; } = "";418 public int Age { get; set; }419}420421```text422423### Anti-Patterns to Avoid4244251. **Reflection-based DI** -- Do not use `Activator.CreateInstance` or `Type.GetType` to resolve services. Use the426 built-in DI container with explicit registrations.4272. **Dynamic type loading** -- Do not use `Assembly.Load` or `Assembly.GetTypes()` at runtime. Register all types at428 startup.4293. **Runtime code generation** -- Do not use `System.Reflection.Emit` or `System.Linq.Expressions.Expression.Compile()`.430 Use source generators instead.4314. **Untyped JSON deserialization** -- Do not use `JsonSerializer.Deserialize<T>(json)` without a432 `JsonSerializerContext`. Always provide a source-generated context.433434---435436## Prerendering437438Prerendering generates HTML on the server before the interactive runtime loads. This improves perceived performance and439SEO.440441### Prerender with Interactive Modes442443```razor444445<!-- Component prerenders on server, then becomes interactive -->446<Counter @rendermode="InteractiveServer" />447448```text449450By default, interactive components prerender. To disable:451452```razor453454@rendermode @(new InteractiveServerRenderMode(prerender: false))455456```text457458### Persisting State Across Prerender459460State computed during prerendering is lost when the component reinitializes interactively. Use461`PersistentComponentState` to preserve it:462463```razor464465@inject PersistentComponentState ApplicationState466@implements IDisposable467468@code {469 private List<ProductDto>? products;470 private PersistingComponentStateSubscription _subscription;471472 protected override async Task OnInitializedAsync()473 {474 _subscription = ApplicationState.RegisterOnPersisting(PersistState);475476 if (!ApplicationState.TryTakeFromJson<List<ProductDto>>(477 "products", out var restored))478 {479 products = await ProductService.GetProductsAsync();480 }481 else482 {483 products = restored;484 }485 }486487 private Task PersistState()488 {489 ApplicationState.PersistAsJson("products", products);490 return Task.CompletedTask;491 }492493 public void Dispose() => _subscription.Dispose();494}495496```text497498---499500## .NET 10 Stable Features501502These features are available when `net10.0` TFM is detected. They are stable and require no preview opt-in.503504### WebAssembly Preloading505506.NET 10 adds `blazor.web.js` preloading of WebAssembly assemblies during the Server phase of InteractiveAuto. When the507user first loads a page, the WASM runtime and app assemblies download in the background while the Server circuit handles508interactivity. Subsequent navigations switch to WASM faster because assemblies are already cached.509510```razor511512<!-- No code changes needed -- preloading is automatic in .NET 10 -->513<!-- Verify in browser DevTools Network tab: assemblies download during Server phase -->514515```text516517### Enhanced Form Validation518519.NET 10 extends `EditForm` validation with improved error message formatting and support for `IValidatableObject` in520Static SSR forms. Validation messages render correctly with enhanced form handling (`Enhance` attribute) without521requiring a full page reload.522523```csharp524525// IValidatableObject works in Static SSR enhanced forms in .NET 10526public sealed class OrderModel : IValidatableObject527{528 [Required]529 public string ProductId { get; set; } = "";530531 [Range(1, 100)]532 public int Quantity { get; set; }533534 public IEnumerable<ValidationResult> Validate(ValidationContext context)535 {536 if (ProductId == "DISCONTINUED" && Quantity > 0)537 {538 yield return new ValidationResult(539 "Cannot order discontinued products",540 [nameof(ProductId), nameof(Quantity)]);541 }542 }543}544545```text546547### Blazor Diagnostics Middleware548549.NET 10 adds `MapBlazorDiagnostics` middleware for inspecting Blazor circuit and component state in development:550551```csharp552553// Program.cs -- available in .NET 10554if (app.Environment.IsDevelopment())555{556 app.MapBlazorDiagnostics(); // Exposes /_blazor/diagnostics endpoint557}558559```text560561The diagnostics endpoint shows active circuits, component tree, render mode assignments, and timing data. Use it to562debug render mode boundaries and component lifecycle issues during development.563564---565566## Agent Gotchas5675681. **Do not default to InteractiveServer for every page.** Static SSR is the default and most efficient render mode.569 Only add interactivity where user interaction requires it.5702. **Do not put WASM-targeted components in the server project.** Components that must run in the browser571 (InteractiveWebAssembly or InteractiveAuto) belong in the `.Client` project.5723. **Do not forget `PersistentComponentState` when prerendering.** Without it, data fetched during prerender is573 discarded and re-fetched when the component becomes interactive, causing a visible flicker.5744. **Do not use reflection-based serialization in WASM.** Always use `JsonSerializerContext` with source-generated575 serializers for AOT compatibility and trimming safety.5765. **Do not force-load navigation unless leaving the Blazor app.** `NavigateTo("/page", forceLoad: true)` bypasses577 enhanced navigation and causes a full page reload.5786. **Do not nest interactive render modes incorrectly.** A child component cannot request a more interactive mode than579 its parent. Plan render mode boundaries from the layout down.580581---582583## Prerequisites584585- .NET 8.0+ (Blazor Web App template, render modes, enhanced navigation, streaming rendering)586- .NET 10.0 for stable enhanced features (WebAssembly preloading, enhanced form validation, diagnostics middleware)587- MAUI workload for Blazor Hybrid (`dotnet workload install maui`)588589---590591## References592593- [Blazor Overview](https://learn.microsoft.com/en-us/aspnet/core/blazor/?view=aspnetcore-10.0)594- [Blazor Render Modes](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes?view=aspnetcore-10.0)595- [Blazor Routing](https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/routing?view=aspnetcore-10.0)596- [Enhanced Navigation](https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/routing?view=aspnetcore-10.0#enhanced-navigation-and-form-handling)597- [Streaming Rendering](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/rendering?view=aspnetcore-10.0#streaming-rendering)598- [Blazor Hybrid](https://learn.microsoft.com/en-us/aspnet/core/blazor/hybrid/?view=aspnetcore-10.0)599- [AOT Deployment](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/)600````