Blazor Web Development: Create Interactive UIs with C# 12
30–45 min read
Intermediate
Blazor lets you build interactive web UIs using C# instead of JavaScript. Whether you target WebAssembly for offline-capable apps or Blazor Server for real-time experiences, you write the same component model, enjoy full .NET tooling, and ship faster.
In this tutorial, you'll build a Todo Dashboard from the ground up. You'll learn how components work, how to bind data, handle forms, manage state, call APIs, and deploy production-ready apps. By the end, you'll have practical experience with Blazor .NET 8's new render modes, streaming SSR, QuickGrid, and authentication patterns.
What You’ll Build
The Todo Dashboard evolves from a simple list to a full-featured app with:
Interactive components using Razor syntax and C# 12 features
Forms with validation leveraging DataAnnotations and FluentValidation
State management across components with services and cascading values
Authentication & authorization protecting routes and actions
Performance optimization with virtualization and streaming rendering
What's New in Blazor for .NET 8
Blazor for .NET 8 brings unified render modes, streaming server-side rendering, and productivity enhancements that make building interactive web UIs faster and more flexible. Here's what makes Blazor .NET 8 compelling:
🚀
Render Modes
Choose per page or component: SSR for static content, InteractiveServer for real-time updates, InteractiveWebAssembly for offline, or Auto for best of both worlds.
🌊
Streaming SSR
Progressively send markup as data becomes available for faster perceived page loads on dashboards and reports with slow data sources.
🧩
Enhanced Components
C# 12 primary constructors for component parameters and collection expressions for list rendering make code more concise and expressive.
🧮
QuickGrid
Built-in data grid with sorting, filtering, pagination, and virtualization out of the box—no third-party libraries needed for common scenarios.
🔐
Identity UI
Streamlined authentication scaffolding with better protected content patterns and improved integration with ASP.NET Core Identity.
Long-Term Support
.NET 8 is an LTS release supported through November 2026. Start with SSR plus interactive islands for optimal UX and performance. Use full WebAssembly when offline capability is required or when you need maximum client-side scalability.
Setup & First Project
You need the .NET 8 SDK to build Blazor applications. The SDK includes templates for both Blazor Server and Blazor WebAssembly projects.
# Blazor Web App (default: SSR with optional interactivity)
dotnet new blazor -n TodoDashboard
cd TodoDashboard
dotnet run
# Blazor WebAssembly (client-side only)
dotnet new blazorwasm -n TodoDashboard.Wasm
# With Auto interactivity (hybrid SSR/interactive)
dotnet new blazor -n TodoDashboard --interactivity Auto
Your app will start on https://localhost:5001. The default template includes sample components demonstrating weather data and counter interactions.
var builder = WebApplication.CreateBuilder(args);
// Add services
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents()
.AddInteractiveWebAssemblyComponents();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAntiforgery();
// Map Razor components with render modes
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode()
.AddInteractiveWebAssemblyRenderMode();
app.Run();
Tip: IDE Options
Visual Studio 2022 (17.8+), Visual Studio Code with C# Dev Kit, and JetBrains Rider (2023.3+) all support Blazor with hot reload, IntelliSense, and component debugging.
Components Fundamentals
Blazor components are reusable UI chunks written in Razor syntax (.razor files). Components combine HTML markup with C# logic and can nest, pass parameters, and respond to events.
@attribute [Authorize] - Apply attributes to component
@rendermode InteractiveServer - Set component render mode
@key - Help Blazor identify elements in lists
Performance Warning
Avoid heavy work in OnAfterRender without guards—it runs on every render. Use the firstRender parameter to execute logic only once. Use StateHasChanged() sparingly; Blazor automatically re-renders after event handlers complete.
Data Binding & Forms
Blazor provides declarative data binding with @bind for two-way synchronization between UI and C# properties. Forms include validation with DataAnnotations or FluentValidation.
For complex validation rules, use FluentValidation:
FluentValidation Example
// Install: dotnet add package FluentValidation
using FluentValidation;
public class TodoCreateModelValidator : AbstractValidator<TodoCreateModel>
{
public TodoCreateModelValidator()
{
RuleFor(x => x.Title)
.NotEmpty().WithMessage("Title is required")
.Length(3, 100).WithMessage("Title must be 3-100 characters")
.Must(NotContainBadWords).WithMessage("Title contains inappropriate content");
RuleFor(x => x.Description)
.MaximumLength(500).WithMessage("Description too long");
RuleFor(x => x.Priority)
.Must(p => new[] { "Low", "Medium", "High" }.Contains(p))
.WithMessage("Invalid priority");
}
private bool NotContainBadWords(string title)
{
var badWords = new[] { "spam", "test" };
return !badWords.Any(word => title.Contains(word, StringComparison.OrdinalIgnoreCase));
}
}
Tip: Prefer Record Models
Use C# 12 record types with validation attributes for form models. Records provide value semantics and immutability by default, making state management clearer. For controlled inputs, use @bind:get and @bind:set directives for fine-grained control.
Routing & Navigation
Blazor routing maps URLs to components using the @page directive. The router supports parameters, constraints, query strings, and programmatic navigation.
Route Parameters and Constraints
Route Examples
@* Simple route *@
@page "/todos"
@* Route with parameter *@
@page "/todo/{id:int}"
@* Optional parameter *@
@page "/search/{query?}"
@* Multiple constraints *@
@page "/posts/{year:int:min(2020)}/{month:int:range(1,12)}"
@* Catch-all parameter *@
@page "/docs/{*path}"
@code {
[Parameter]
public int Id { get; set; }
[Parameter]
public string? Query { get; set; }
[Parameter]
public int Year { get; set; }
[Parameter]
public int Month { get; set; }
[Parameter]
public string? Path { get; set; }
}
With SSR render mode, the first load is server-rendered HTML. Interactive render modes (Server or WebAssembly) then hydrate the page, enabling client-side navigation. This provides fast initial loads with SPA-like navigation after hydration completes.
State Management
State management in Blazor ranges from local component state to shared application state. Choose the right approach based on scope and lifetime requirements.
Local state: Component-only data that doesn't need sharing
Cascading values: Theme, user context, or configuration flowing down the tree
Scoped services: Per-user state in Server apps; per-circuit lifetime
Singleton services: Application-wide shared state (use carefully in Server mode)
Browser storage: localStorage/sessionStorage via JS interop for persistence
Server Mode Caution
Avoid accidental singleton cross-user state in Blazor Server apps. Use scoped services where identity or user-specific data matters. Singleton services are shared across all users and circuits—only use them for truly global, immutable data.
JavaScript Interop
Most Blazor apps don't need JavaScript, but when you do need browser APIs or third-party libraries, JavaScript interop provides the bridge. Use it sparingly and wrap it in services for testability.
Before reaching for JS interop, check if Blazor has a built-in component. Use <InputFile> for file uploads, <NavigationManager> for routing, and <AuthorizeView> for auth. Wrap interop in services for easier unit testing.
Authentication & Authorization
Blazor integrates with ASP.NET Core Identity for Server apps and supports token-based authentication for WebAssembly apps calling backend APIs.
Server-Side Authentication Setup
Program.cs - Identity Setup
using Microsoft.AspNetCore.Identity;
var builder = WebApplication.CreateBuilder(args);
// Add authentication and authorization
builder.Services.AddAuthentication();
builder.Services.AddAuthorization();
// Add Identity (if using database)
builder.Services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.Run();
For WebAssembly apps, use the Microsoft.AspNetCore.Components.WebAssembly.Authentication package templates to wire OAuth quickly. Configure your identity provider (Auth0, Azure AD, IdentityServer) and let the package handle token management, refresh, and protected API calls.
Advanced Patterns: Render Modes & Performance
Blazor .NET 8 introduces flexible render modes that let you choose how each component executes. Combined with streaming rendering and performance optimizations, you can build fast, scalable applications.
@page "/editor"
@rendermode InteractiveWebAssembly
<h2>Offline Editor</h2>
<textarea @bind="content" @bind:event="oninput"></textarea>
<p>Characters: @content.Length</p>
@code {
private string content = "";
}
// Best for: Offline capability, high user scalability, client-heavy apps
Auto Mode
@page "/hybrid"
@rendermode InteractiveAuto
<h2>Hybrid Component</h2>
<button @onclick="Increment">Count: @count</button>
@code {
private int count = 0;
private void Increment() => count++;
}
// Best for: Best of both worlds—SSR first load, then interactive based on availability
Render Mode Selection Guidance
Static SSR: Marketing pages, blogs, product catalogs—anything that benefits from SEO and doesn't need interactivity
InteractiveServer: Admin dashboards, internal tools, real-time apps with low user count
InteractiveWebAssembly: Offline-capable apps, PWAs, apps with thousands of concurrent users
Auto: E-commerce, SaaS apps—fast initial load with interactive features
Streaming sends initial markup quickly, then streams updates as data arrives. This improves perceived performance but may affect SEO crawlers that don't wait for streaming content. Test with Google Search Console and consider SSR without streaming for critical SEO pages.
Testing Blazor components ensures UI behavior and logic work correctly. bUnit provides a testing framework specifically designed for Blazor components.
Setup bUnit
Install bUnit
# Create test project
dotnet new xunit -n TodoDashboard.Tests
cd TodoDashboard.Tests
# Add bUnit
dotnet add package bUnit
dotnet add package bUnit.web
# Add reference to your Blazor project
dotnet add reference ../TodoDashboard/TodoDashboard.csproj
Basic Component Test
CounterTests.cs
using Bunit;
using Xunit;
public class CounterTests : TestContext
{
[Fact]
public void Counter_Increments_When_Button_Clicked()
{
// Arrange
var cut = RenderComponent<Counter>();
// Act
cut.Find("button").Click();
// Assert
cut.Find("p").TextContent.Should().Contain("1");
}
[Fact]
public void Counter_Starts_At_Zero()
{
// Arrange
var cut = RenderComponent<Counter>();
// Assert
cut.Find("p").TextContent.Should().Contain("0");
}
}
Testing Forms and Validation
TodoFormTests.cs
using Bunit;
using Xunit;
using Microsoft.Extensions.DependencyInjection;
public class TodoFormTests : TestContext
{
[Fact]
public void Form_Shows_Validation_Error_For_Empty_Title()
{
// Arrange
var cut = RenderComponent<TodoForm>();
// Act - try to submit without filling form
cut.Find("form").Submit();
// Assert
cut.Find(".validation-message")
.TextContent.Should().Contain("Title is required");
}
[Fact]
public async Task Form_Calls_Service_On_Valid_Submit()
{
// Arrange
var mockService = new Mock<ITodoService>();
Services.AddSingleton(mockService.Object);
var cut = RenderComponent<TodoForm>();
// Act
cut.Find("input[name='title']").Change("New Todo");
cut.Find("form").Submit();
// Assert
await cut.InvokeAsync(() => { });
mockService.Verify(s => s.CreateAsync(It.IsAny<TodoItem>()), Times.Once);
}
}
Mocking Services and HTTP
Service Mocking
using Bunit;
using Xunit;
using Moq;
public class TodoListTests : TestContext
{
[Fact]
public void TodoList_Displays_Todos_From_Service()
{
// Arrange
var mockService = new Mock<ITodoService>();
mockService.Setup(s => s.GetAllAsync())
.ReturnsAsync(new List<TodoItem>
{
new() { Id = 1, Title = "Test Todo 1" },
new() { Id = 2, Title = "Test Todo 2" }
});
Services.AddSingleton(mockService.Object);
// Act
var cut = RenderComponent<TodoList>();
// Assert
cut.FindAll(".todo-item").Count.Should().Be(2);
cut.Find(".todo-item").TextContent.Should().Contain("Test Todo 1");
}
}
Testing Strategy
Focus on testing component behavior, not implementation details. Test user interactions (clicks, form submissions), state changes, and service calls. Run tests in CI with dotnet test for confidence before deployment.
Deployment & Production Practices
Blazor apps deploy to various hosting environments. Server apps run on ASP.NET Core hosts, while WebAssembly apps are static files served from any web server or CDN.
Blazor Server Deployment
Publish Blazor Server
# Publish for production
dotnet publish -c Release -o ./publish
# Publish to folder with self-contained runtime
dotnet publish -c Release -r linux-x64 --self-contained -o ./publish
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["TodoDashboard.csproj", "./"]
RUN dotnet restore
COPY . .
RUN dotnet build -c Release -o /app/build
FROM build AS publish
RUN dotnet publish -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "TodoDashboard.dll"]
Azure Deployment
Deploy to Azure
# Blazor Server to App Service
az webapp up --name my-blazor-app --resource-group mygroup --runtime "DOTNET|8.0"
# Blazor WASM to Static Web Apps
az staticwebapp create \
--name my-blazor-wasm \
--resource-group mygroup \
--source https://github.com/user/repo \
--location "East US" \
--branch main \
--app-location "src" \
--output-location "wwwroot"
// Install: Microsoft.ApplicationInsights.AspNetCore
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddApplicationInsightsTelemetry(options =>
{
options.ConnectionString = builder.Configuration["ApplicationInsights:ConnectionString"];
});
var app = builder.Build();
app.Run();
Tip: GitHub Actions for CI/CD
Use GitHub Actions to automate builds and deployments. Enable response compression and cache headers for WASM assets. For Server apps, configure server GC settings and connection limits for optimal performance under load.
Migration from Blazor .NET 7
Upgrading from Blazor .NET 7 to .NET 8 is straightforward. Most component code works unchanged; the main task is adopting the new render mode model.
Migration Checklist
✅ Update .NET SDK to 8.0.x
✅ Update TargetFramework to net8.0
✅ Update all NuGet packages to 8.0.x versions
✅ Run dotnet restore and fix any warnings
✅ Choose render modes for your pages and components
✅ Update authentication wiring if using Identity
✅ Test SSR and streaming behavior
✅ Run performance checks and verify no regressions
Routing: Enhanced router may handle edge cases differently
Migration Strategy
Most Blazor .NET 7 component code ports directly to .NET 8. The main work is choosing appropriate render modes for each page and component. Start with SSR for static content and add interactive modes where needed. Test thoroughly, especially authentication and JavaScript interop.
Next Steps & Resources
You've learned Blazor fundamentals. Here's where to continue your journey:
Clone the complete Todo Dashboard with basic and advanced implementations:
Clone Repository
git clone https://github.com/dotnet-guide-com/tutorials.git
cd tutorials/blazor
# Run basic Todo Dashboard
cd TodoDashboard.Basic
dotnet run
# Run advanced version (auth + EF Core)
cd ../TodoDashboard.Advanced
dotnet run
Use Blazor Server for internal apps with low latency requirements and simpler deployment—no need to download the .NET runtime to the browser. Use WebAssembly for client-heavy apps that need offline capability or high user scalability without server connections. Use Auto render mode to get the best of both worlds—SSR on first load with interactive mode based on connection availability and performance characteristics.
What are render modes and which should I use?
Render modes control how components execute. Static SSR is fast and SEO-friendly for content pages. InteractiveServer maintains a SignalR connection for real-time updates. InteractiveWebAssembly runs entirely in the browser with offline support. Auto mode combines SSR for initial load with interactive mode afterward. Start with SSR plus interactive islands for optimal performance—use full interactivity only where needed.
Do I need JavaScript at all with Blazor?
Most Blazor apps don't need JavaScript. Blazor provides built-in components for forms, navigation, file uploads, and authentication. Use JavaScript interop only for browser APIs not exposed to C# (clipboard, localStorage, geolocation) or when integrating third-party JavaScript libraries. Wrap interop calls in services for better testability and maintainability.
How do I secure a WebAssembly app calling my API?
Use the Microsoft.AspNetCore.Components.WebAssembly.Authentication package with OAuth/OIDC. Configure AuthenticationStateProvider, add HttpClient with BaseAddressAuthorizationMessageHandler for automatic token injection, and protect routes with AuthorizeView and [Authorize] attributes. Never trust WebAssembly client-side logic alone—always validate and authorize on the server API.
How do I handle very large lists or tables efficiently?
Use QuickGrid with virtualization for built-in sorting, filtering, and efficient rendering of large datasets. For custom solutions, use the <Virtualize> component to render only visible items in scrollable lists. Load data in pages from the server rather than materializing entire collections in memory. Use the @key directive to help Blazor identify and efficiently update list items.
Can I mix SSR, Server, and WebAssembly in one solution?
Yes. Configure multiple render modes in Program.cs and annotate components with @rendermode directives. You can have SSR pages with InteractiveServer islands for real-time features and even lazy-load WebAssembly components for heavy client-side processing. This hybrid approach optimizes for initial load speed while enabling rich interactivity where needed.
What's the difference between Blazor and Razor Pages?
Razor Pages are server-rendered page-based applications with traditional request/response model—each interaction triggers a full page reload. Blazor uses component-based architecture with interactivity through SignalR (Server) or WebAssembly. Blazor enables SPA-like experiences without JavaScript frameworks. Use Razor Pages for simple CRUD apps with traditional forms. Use Blazor for rich, interactive UIs with client-side state management.
When should I use streaming rendering?
Use streaming SSR for pages with slow data sources (external APIs, complex database queries) where you want to show content progressively rather than waiting for all data. Streaming sends initial markup quickly with loading placeholders, then streams updates as data arrives. Ideal for dashboards and reports. Be aware that streaming content may affect SEO crawlers that don't wait for streamed updates—test with Google Search Console and consider standard SSR for critical SEO pages.