Loading...
Loading...
Architects Blazor apps. Hosting models, render modes, routing, streaming, prerender.
npx skill4agent add novotnyllc/dotnet-artisan dotnet-blazor-patterns| 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 | | Full | SignalR circuit | Low-latency interactivity, full server access, small user base |
| InteractiveWebAssembly | | Full (after download) | None (runs in browser) | Offline-capable, large user base, reduced server load |
| InteractiveAuto | | Full | SignalR initially, then WASM | Best of both -- immediate interactivity, eventual client-side |
| Blazor Hybrid | | Full (native) | None (runs in-process) | Desktop/mobile apps with web UI, native API access |
| 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 |
<!-- Sets default render mode for all pages -->
<Routes @rendermode="InteractiveServer" />@page "/dashboard"
@rendermode InteractiveServer
<h1>Dashboard</h1><Counter @rendermode="InteractiveWebAssembly" />@rendermode# 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 onlyMyApp/
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.Client<!-- .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>// MainPage.xaml.cs hosts BlazorWebView
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
}
}<!-- 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>@page "/products"
@page "/products/{Category}"
<h1>Products</h1>
@if (!string.IsNullOrEmpty(Category))
{
<p>Category: @Category</p>
}
@code {
[Parameter]
public string? Category { get; set; }
}@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; } = "";
}@page "/search"
@code {
[SupplyParameterFromQuery]
public string? Term { get; set; }
[SupplyParameterFromQuery(Name = "page")]
public int CurrentPage { get; set; } = 1;
}@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);<!-- 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>data-enhance-nav="false"DOMContentLoaded@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();
}
}[StreamRendering]// 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);// WRONG: Reflection-based serialization (fails under AOT/trimming)
var products = await Http.GetFromJsonAsync<List<ProductDto>>("/api/products");// 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();// WRONG: Dynamic dispatch via reflection (trimmed away)
// var method = typeof(JSRuntime).GetMethod("InvokeAsync");
// method.MakeGenericMethod(returnType).Invoke(...)<!-- Preserve types used dynamically in components -->
<ItemGroup>
<TrimmerRootAssembly Include="MyApp.Client" />
</ItemGroup>// 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; }
}Activator.CreateInstanceType.GetTypeAssembly.LoadAssembly.GetTypes()System.Reflection.EmitSystem.Linq.Expressions.Expression.Compile()JsonSerializer.Deserialize<T>(json)JsonSerializerContext<!-- Component prerenders on server, then becomes interactive -->
<Counter @rendermode="InteractiveServer" />@rendermode @(new InteractiveServerRenderMode(prerender: false))PersistentComponentState@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();
}net10.0blazor.web.js<!-- No code changes needed -- preloading is automatic in .NET 10 -->
<!-- Verify in browser DevTools Network tab: assemblies download during Server phase -->EditFormIValidatableObjectEnhance// 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)]);
}
}
}MapBlazorDiagnostics// Program.cs -- available in .NET 10
if (app.Environment.IsDevelopment())
{
app.MapBlazorDiagnostics(); // Exposes /_blazor/diagnostics endpoint
}.ClientPersistentComponentStateJsonSerializerContextNavigateTo("/page", forceLoad: true)dotnet workload install maui