🚀 Blazor Mastery Guide
Comprehensive Q&A with Production-Ready Code Examples
📚 Table of Contents
Answer: Blazor represents a paradigm shift from traditional server-rendered frameworks. Here's the key difference:
- MVC/Razor Pages: Server-side rendering only. Each interaction requires a full HTTP request/response cycle with page reloads.
- Blazor: Enables true SPA experiences using C# instead of JavaScript. Components render interactively with real-time UI updates without full page reloads.
// Traditional Razor Page (Server-side only)
@page "/counter"
<h3>Counter: @count</h3>
<button @onsubmit="Increment">Click</button>
@code {
private int count = 0;
public IActionResult OnPostIncrement() {
count++; // Requires full postback
return Page();
}
}
// Blazor Component (Interactive SPA)
@page "/counter"
<h3>Counter: @count</h3>
<button @onclick="Increment">Click</button>
@code {
private int count = 0;
private void Increment() {
count++; // Real-time update, no page reload!
}
}Answer: Blazor offers compelling advantages for .NET teams:
- ✅ Full-stack C# development (share models, validation, business logic)
- ✅ No JavaScript required (though optional for advanced scenarios)
- ✅ True component-based architecture with reusability
- ✅ Real-time WebSocket support out-of-the-box (Blazor Server)
- ✅ Progressive Web App (PWA) support for offline scenarios
- ✅ .NET ecosystem access (NuGet, EF Core, Azure SDK)
Answer: .NET 8 introduced unified rendering modes:
- Static Server-Side Rendering (SSR): Traditional server rendering, no interactivity
- Interactive Server: Real-time SignalR connection, runs on server
- Interactive WebAssembly: Runs entirely in browser via WebAssembly
- Interactive Auto: Starts with Server, switches to WebAssembly after download
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents() // Enable Server interactivity
.AddInteractiveWebAssemblyComponents(); // Enable WASM interactivity
var app = builder.Build();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode()
.AddInteractiveWebAssemblyRenderMode();Answer: Blazor maintains parent-child communication through parameters and callbacks.
@* Parent Component *@
<h3>Parent Component</h3>
<p>Message from child: @childMessage</p>
<ChildComponent
Title="Hello Child!"
OnNotifyParent="HandleChildNotification" />
@code {
private string childMessage = string.Empty;
private void HandleChildNotification(string message) {
childMessage = message;
}
}@* Child Component *@
<div class="card">
<h4>@Title</h4>
<button @onclick="() => OnNotifyParent.Invoke(\"Button clicked!\")">
Notify Parent
</button>
</div>
@code {
[Parameter] public string Title { get; set; } = string.Empty;
[Parameter] public EventCallback<string> OnNotifyParent { get; set; }
}Answer: Use a state container service as a shared communication hub.
public class StateContainer {
private string? _sharedData;
public event Action? OnStateChange;
public string? SharedData {
get => _sharedData;
set {
_sharedData = value;
NotifyStateChanged();
}
}
private void NotifyStateChanged() => OnStateChange?.Invoke();
}
// In Program.cs
builder.Services.AddSingleton<StateContainer>();// SenderComponent.razor
@inject StateContainer State
<button @onclick="() => State.SharedData = DateTime.Now.ToString()">
Update Shared Data
</button>
// ReceiverComponent.razor
@implements IDisposable
@inject StateContainer State
<p>Received: @State.SharedData</p>
@code {
protected override void OnInitialized() => State.OnStateChange += StateHasChanged;
public void Dispose() => State.OnStateChange -= StateHasChanged;
}Answer: Create an observable state container with event notification pattern.
public class ShoppingCartState {
private readonly List<CartItem> _items = new();
public IReadOnlyList<CartItem> Items => _items;
public event Action? OnCartChanged;
public void AddItem(CartItem item) {
_items.Add(item);
OnCartChanged?.Invoke();
}
public void RemoveItem(int id) {
_items.RemoveAll(i => i.Id == id);
OnCartChanged?.Invoke();
}
}
// Usage in any component
@inject ShoppingCartState Cart
@implements IDisposable
protected override void OnInitialized() => Cart.OnCartChanged += StateHasChanged;
public void Dispose() => Cart.OnCartChanged -= StateHasChanged;Answer: Use ProtectedBrowserStorage for persistent client-side storage.
@inject ProtectedLocalStorage LocalStorage
@implements IDisposable
@code {
private UserPreferences _preferences = new();
protected override async Task OnInitializedAsync() {
try {
var result = await LocalStorage.GetAsync<UserPreferences>("userPrefs");
if (result.Success) _preferences = result.Value;
} catch (InvalidOperationException) {
// Handle privacy mode or unavailable storage
}
}
private async Task SavePreferences() {
await LocalStorage.SetAsync("userPrefs", _preferences);
}
private async Task ClearPreferences() {
await LocalStorage.DeleteAsync("userPrefs");
}
}Answer: Override ShouldRender() and implement custom render logic.
@code {
private ExpensiveData _data = new();
protected override bool ShouldRender() {
// Only re-render when data has actually changed
return _data.HasChanged;
}
// For immutable parameters, use @inherit .NET's equality comparison
[Parameter] public List<string> Items { get; set; } = new();
// Better: Use immutable collections or implement IEquatable
}@* Without @key - entire list re-renders *@
@foreach (var item in items) {
<ChildComponent Item="item" />
}
@* With @key - only changed items re-render *@
@foreach (var item in items) {
<ChildComponent @key="item.Id" Item="item" />
}Answer: Use Virtualize component for handling thousands of items.
@* Virtualize efficiently loads only visible items *@
<Virtualize ItemsProvider="LoadCustomers" Context="customer">
<div class="customer-item">
<strong>@customer.Name</strong> - @customer.Email
</div>
</Virtualize>
@code {
private async ValueTask<ItemsProviderResult<Customer>> LoadCustomers(
ItemsProviderRequest request) {
var customers = await db.Customers
.Skip(request.StartIndex)
.Take(request.Count)
.ToListAsync();
var totalCount = await db.Customers.CountAsync();
return new ItemsProviderResult<Customer>(customers, totalCount);
}
}Answer: Use IJSRuntime with JSON serialization for complex objects.
@inject IJSRuntime JS
<button @onclick="ShowChart">Display Analytics Chart</button>
@code {
private async Task ShowChart() {
var data = new ChartData {
Labels = new[] { "Jan", "Feb", "Mar" },
Values = new[] { 10, 25, 15 },
Colors = new[] { "#ff0000", "#00ff00", "#0000ff" }
};
await JS.InvokeVoidAsync("window.createChart", data);
}
}
// wwwroot/js/site.js
window.createChart = (data) => {
const ctx = document.getElementById('myChart');
new Chart(ctx, {
type: 'bar',
data: {
labels: data.labels,
datasets: [{
data: data.values,
backgroundColor: data.colors
}]
}
});
};Answer: Use DotNetReference to expose .NET methods to JavaScript.
@inject IJSRuntime JS
@implements IAsyncDisposable
<div id="map" style="height: 400px;"></div>
@code {
private DotNetObjectReference<MapComponent>? _dotNetRef;
protected override async Task OnAfterRenderAsync(bool firstRender) {
if (firstRender) {
_dotNetRef = DotNetObjectReference.Create(this);
await JS.InvokeVoidAsync("initMap", _dotNetRef);
}
}
[JSInvokable]
public void OnMapClick(double lat, double lng) {
Console.WriteLine($"Clicked at: {lat}, {lng}");
// Update UI or perform logic
}
async ValueTask IAsyncDisposable.DisposeAsync() {
_dotNetRef?.Dispose();
}
}Answer: Create custom ValidationAttribute and implement IClientModelValidator.
public class FutureDateAttribute : ValidationAttribute, IClientModelValidator {
protected override ValidationResult? IsValid(object? value, ValidationContext context) {
if (value is DateTime date && date <= DateTime.Now) {
return new ValidationResult("Date must be in the future");
}
return ValidationResult.Success;
}
public void AddValidation(ClientModelValidationContext context) {
context.Attributes.Add("data-val", "true");
context.Attributes.Add("data-val-futuredate", ErrorMessage);
context.Attributes.Add("data-val-futuredate-min", DateTime.Now.ToString("yyyy-MM-dd"));
}
}
// Usage in model
public class EventModel {
[FutureDate(ErrorMessage = "Event date must be in the future")]
public DateTime EventDate { get; set; }
}Answer: Implement debounce pattern with Timer and StateHasChanged.
<input @bind="searchTerm" @bind:event="oninput" @oninput="OnSearchInput" />
@code {
private string _searchTerm = string.Empty;
private System.Timers.Timer? _debounceTimer;
private void OnSearchInput(ChangeEventArgs e) {
_debounceTimer?.Dispose();
_debounceTimer = new System.Timers.Timer(500);
_debounceTimer.Elapsed += async (sender, args) => {
await InvokeAsync(async () => {
await PerformSearch();
StateHasChanged();
});
_debounceTimer?.Dispose();
};
_debounceTimer.Start();
}
private async Task PerformSearch() {
// API call or search logic
results = await searchService.SearchAsync(_searchTerm);
}
}Answer: Use built-in route constraints in @page directive.
@page "/products/{id:int}"
@page "/products/{category}/{id:int:min(1)}"
@page "/users/{username:regex(^[a-zA-Z0-9]+$)}"
<h3>Product ID: @ProductId</h3>
<h3>Category: @Category</h3>
@code {
[Parameter] public int ProductId { get; set; }
[Parameter] public string? Category { get; set; }
protected override async Task OnParametersSetAsync() {
// Load data based on route parameters
var product = await ProductService.GetByIdAsync(ProductId);
}
}Answer: Use AuthorizeView component or [Authorize] attribute.
@* Declarative authorization *@
<AuthorizeView Roles="Admin">
<Authorized>
<button @onclick="DeleteData">Delete All Data</button>
</Authorized>
<NotAuthorized>
<p>You need administrator privileges.</p>
</NotAuthorized>
</AuthorizeView>
@* Policy-based authorization *@
<AuthorizeView Policy="RequireElevatedRights">
<Authorized>
<AdminPanel />
</Authorized>
</AuthorizeView>
@* Attribute-based at component level *@
@attribute [Authorize(Roles = "Admin,Manager")]
@code {
[CascadingParameter]
private Task<AuthenticationState>? AuthState { get; set; }
private async Task CheckUserClaims() {
var authState = await AuthState!;
var user = authState.User;
if (user.HasClaim(c => c.Type == "Permission" && c.Value == "Delete")) {
// Show delete button
}
}
}Answer: Use ErrorBoundary component in .NET 8 or implement custom error handling.
@* App.razor - Global error boundary *@
<ErrorBoundary>
<ChildContent>
<Router ... />
</ChildContent>
<ErrorContent>
<div class="alert alert-danger">
<h5>Something went wrong</h5>
<p>@context.Exception.Message</p>
<button @onclick="context.Recover">Recover</button>
</div>
</ErrorContent>
</ErrorBoundary>
// Custom error handling service
public class GlobalExceptionHandler : IErrorBoundaryLogger {
private readonly ILogger<GlobalExceptionHandler> _logger;
public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger) {
_logger = logger;
}
public ValueTask LogErrorAsync(Exception exception) {
_logger.LogError(exception, "Unhandled exception in component");
// Send to Application Insights, Sentry, etc.
return ValueTask.CompletedTask;
}
}Answer: Use bUnit with custom mock implementation.
// Test using bUnit
[Fact]
public void Component_Calls_JavaScript_On_Click() {
// Arrange
var jsRuntimeMock = new Mock<IJSRuntime>();
var cut = RenderComponent<MyComponent>(services =>
services.AddSingleton(jsRuntimeMock.Object));
// Act
cut.Find("button").Click();
// Assert
jsRuntimeMock.Verify(x =>
x.InvokeVoidAsync("myJsFunction", It.IsAny<object[]>(), It.IsAny<CancellationToken>()),
Times.Once);
}
// Custom JS mock for bUnit
public class TestJSRuntime : IJSRuntime {
public List<(string Identifier, object?[] Args)> Invocations { get; } = new();
public ValueTask<TValue> InvokeAsync<TValue>(string identifier, object?[]? args,
CancellationToken cancellationToken = default) {
Invocations.Add((identifier, args ?? Array.Empty<object?>()));
return new ValueTask<TValue>((TValue)(object)null!);
}
}Answer: Create GitHub Actions workflow for automated build and test.
name: Blazor CI/CD
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 8.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore --configuration Release
- name: Run Unit Tests
run: dotnet test --no-build --configuration Release --verbosity normal
- name: Run Playwright Tests
run: |
dotnet tool install --global Microsoft.Playwright.CLI
playwright install
dotnet test --filter "Category=E2E"
- name: Publish Blazor WASM
run: dotnet publish YourBlazorApp.csproj -c Release -o release
- name: Deploy to Azure Static Web Apps
uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_TOKEN }}
repo_token: ${{ secrets.GITHUB_TOKEN }}
action: "upload"
app_location: "release/wwwroot"