Introduction: What is Blazor and Why It Matters
Blazor is a free, open-source web framework developed by Microsoft that allows developers to build interactive web applications using C# and .NET instead of JavaScript. Blazor matters because it:
- Enables full-stack web development with C# only
- Provides two hosting models: Blazor WebAssembly (client-side) and Blazor Server
- Leverages existing .NET ecosystem and libraries
- Allows code reuse between server and client
- Offers seamless integration with existing ASP.NET infrastructure
Core Concepts and Architecture
Concept | Description |
---|
Component Model | UI pieces built with C# and Razor markup (.razor files) |
Razor Syntax | Combines HTML with C# code (prefixed with @) |
Hosting Models | Server-side or WebAssembly client-side execution |
Data Binding | One-way (@variable) or two-way (@bind-Value) |
Event Handling | @onclick, @onchange, etc. with C# method handlers |
Dependency Injection | Built-in DI container for service management |
JavaScript Interop | Communication between C# and JavaScript |
Hosting Models Compared
Feature | Blazor Server | Blazor WebAssembly |
---|
Execution | Server-side | Client-side in browser |
Initial Load | Faster | Slower (downloads .NET runtime) |
Performance | Lower client requirements | Higher client requirements |
Offline Support | No | Yes (with PWA) |
Connection | Requires SignalR connection | No persistent connection |
Server Resources | Higher (maintains user sessions) | Lower (stateless) |
Security | Code never leaves server | Code runs in browser |
Setting Up a Blazor Project
Creating a New Blazor Application
# Create a Blazor Server app
dotnet new blazorserver -o MyBlazorServerApp
# Create a Blazor WebAssembly app
dotnet new blazorwasm -o MyBlazorWebAssemblyApp
# Create a hosted Blazor WebAssembly app (with ASP.NET Core backend)
dotnet new blazorwasm --hosted -o MyHostedBlazorApp
Project Structure
MyBlazorApp/
├── Pages/ # Routable components (.razor)
├── Shared/ # Reusable components
├── Data/ # Data access and models
├── wwwroot/ # Static assets (CSS, JS, images)
├── _Imports.razor # Common using statements
├── App.razor # Application root component
├── Program.cs # Application entry point
└── Startup.cs # Configuration (Server only)
Component Basics
Basic Component Structure (.razor file)
@page "/counter"
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}
Component Lifecycle Methods
Method | Description | When to Use |
---|
OnInitialized | Synchronous initialization | Simple initialization |
OnInitializedAsync | Asynchronous initialization | Database/API calls |
OnParametersSet | After parameters are set | React to parameter changes |
OnParametersSetAsync | Async version of parameters set | Async operations after params |
OnAfterRender | After component renders | DOM manipulation |
OnAfterRenderAsync | Async version of after render | Async operations after render |
ShouldRender | Controls rendering | Optimization |
Dispose | Component cleanup | Resource cleanup |
Data Binding and Forms
Data Binding Examples
<!-- One-way binding -->
<p>Name: @person.Name</p>
<!-- Two-way binding -->
<input @bind="person.Name" />
<input @bind-value="person.Name" @bind-value:event="oninput" />
<!-- Event binding -->
<button @onclick="HandleClick">Click Me</button>
<input @onchange="HandleChange" />
<!-- Complex binding with expressions -->
<div style="color: @(isImportant ? "red" : "black")">
@message
</div>
Form Validation
<EditForm Model="@person" OnValidSubmit="@HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="form-group">
<label for="name">Name:</label>
<InputText id="name" @bind-Value="person.Name" class="form-control" />
<ValidationMessage For="@(() => person.Name)" />
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</EditForm>
@code {
private PersonModel person = new PersonModel();
private void HandleValidSubmit()
{
// Process the valid form
}
}
// In a separate class file:
public class PersonModel
{
[Required]
[StringLength(50, ErrorMessage = "Name is too long.")]
public string Name { get; set; }
}
Built-in Form Components
Component | HTML Equivalent | Description |
---|
InputText | <input type="text"> | Text input with validation |
InputTextArea | <textarea> | Multi-line text input |
InputSelect | <select> | Dropdown selection |
InputNumber | <input type="number"> | Numeric input |
InputCheckbox | <input type="checkbox"> | Boolean checkbox |
InputDate | <input type="date"> | Date picker |
InputFile | <input type="file"> | File input |
InputRadio | <input type="radio"> | Radio button |
InputRadioGroup | Group of radios | Radio button group |
Routing and Navigation
Route Configuration
@page "/counter"
@page "/counter/{currentCount:int}"
<h1>Counter</h1>
<p>Current count: @CurrentCount</p>
@code {
[Parameter]
public int CurrentCount { get; set; } = 0;
}
Navigation
// Inject navigation manager
@inject NavigationManager NavigationManager
// Navigate programmatically
private void NavigateToHome()
{
NavigationManager.NavigateTo("/");
}
// With query parameters
NavigationManager.NavigateTo("/counter?count=10");
// Force page reload
NavigationManager.NavigateTo("/", forceLoad: true);
Route Constraints
Constraint | Example | Description |
---|
int | {id:int} | Integer values |
bool | {active:bool} | Boolean values |
decimal | {price:decimal} | Decimal values |
guid | {id:guid} | GUID values |
datetime | {date:datetime} | DateTime values |
min | {id:min(1)} | Minimum value |
max | {age:max(120)} | Maximum value |
minlength | {name:minlength(2)} | Minimum length |
maxlength | {name:maxlength(50)} | Maximum length |
regex | {zip:regex(^\\d{{5}}$)} | Regular expression |
Dependency Injection
Service Registration (in Program.cs)
// Server-side Blazor
builder.Services.AddSingleton<IDataService, DataService>();
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddTransient<IReportGenerator, ReportGenerator>();
// WebAssembly Blazor
builder.Services.AddSingleton<IDataService, DataService>();
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
Service Injection in Components
@page "/data"
@inject IDataService DataService
@inject IJSRuntime JSRuntime
@inject NavigationManager NavigationManager
<h1>Data Component</h1>
@if (items == null)
{
<p>Loading...</p>
}
else
{
<ul>
@foreach (var item in items)
{
<li>@item.Name</li>
}
</ul>
}
@code {
private List<Item> items;
protected override async Task OnInitializedAsync()
{
items = await DataService.GetItemsAsync();
}
}
JavaScript Interop
Calling JavaScript from C#
@inject IJSRuntime JS
@code {
// Call JS function
private async Task ShowAlert(string message)
{
await JS.InvokeVoidAsync("alert", message);
}
// Call JS function with return value
private async Task<string> GetLocalStorage(string key)
{
return await JS.InvokeAsync<string>("localStorage.getItem", key);
}
// Call JS function defined in a separate file
private async Task CallCustomFunction()
{
await JS.InvokeVoidAsync("myCustomFunctions.doSomething");
}
}
Calling C# from JavaScript
// JavaScript (in wwwroot/js/app.js)
window.callCSharpMethod = (dotNetHelper) => {
dotNetHelper.invokeMethodAsync('UpdateData', { id: 1, name: 'New Item' });
};
// C# component
@inject IJSRuntime JS
<button @onclick="SetupJSCallback">Setup JS Callback</button>
@code {
private DotNetObjectReference<MyComponent> objRef;
protected override void OnInitialized()
{
objRef = DotNetObjectReference.Create(this);
}
private async Task SetupJSCallback()
{
await JS.InvokeVoidAsync("callCSharpMethod", objRef);
}
[JSInvokable]
public void UpdateData(Item item)
{
// Handle data from JavaScript
}
public void Dispose()
{
objRef?.Dispose();
}
}
State Management
Component State
- Local variables within components
@code { private string myState = "initial"; }
App-wide State Options
Approach | Use Case | Pros | Cons |
---|
Services | Simple app state | Built-in, easy to use | Limited reactivity |
State Container Pattern | Medium complexity | Custom notifications | More boilerplate |
Flux/Redux Pattern | Complex state | Predictable updates | Higher complexity |
Blazor Fluxor | Complex state | Redux-like | External dependency |
Browser Storage | Persistent state | Survives refreshes | Limited to serializable data |
Service-based State Example
// StateService.cs
public class StateService
{
private readonly List<Item> items = new();
public event Action OnChange;
public IReadOnlyList<Item> Items => items.AsReadOnly();
public void AddItem(Item item)
{
items.Add(item);
NotifyStateChanged();
}
private void NotifyStateChanged() => OnChange?.Invoke();
}
// In component:
@inject StateService State
@implements IDisposable
<ul>
@foreach (var item in State.Items)
{
<li>@item.Name</li>
}
</ul>
@code {
protected override void OnInitialized()
{
State.OnChange += StateHasChanged;
}
public void Dispose()
{
State.OnChange -= StateHasChanged;
}
}
Component Communication
Parent to Child: Parameters
<!-- Parent component -->
<ChildComponent Title="Hello World" Count="42" />
<!-- Child component (ChildComponent.razor) -->
@code {
[Parameter]
public string Title { get; set; }
[Parameter]
public int Count { get; set; }
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object> AdditionalAttributes { get; set; }
}
Child to Parent: Event Callbacks
<!-- Parent component -->
<ChildComponent OnSave="@HandleSave" />
@code {
private void HandleSave(Item item)
{
// Handle the item from child
}
}
<!-- Child component -->
@code {
[Parameter]
public EventCallback<Item> OnSave { get; set; }
private async Task SaveItem()
{
var item = new Item { /* ... */ };
await OnSave.InvokeAsync(item);
}
}
Cascading Parameters
<!-- Parent component -->
<CascadingValue Value="@theme" Name="Theme">
<CascadingValue Value="@currentUser" Name="User">
<ChildComponent />
</CascadingValue>
</CascadingValue>
@code {
private string theme = "dark";
private UserInfo currentUser = new UserInfo { Name = "John" };
}
<!-- Child or nested component -->
@code {
[CascadingParameter(Name = "Theme")]
private string CurrentTheme { get; set; }
[CascadingParameter(Name = "User")]
private UserInfo LoggedInUser { get; set; }
}
Performance Optimization
Key Optimization Techniques
Technique | Implementation | Benefit |
---|
Virtualization | <Virtualize> component | Renders only visible items |
Lazy Loading | @page components | Loads components on demand |
Code-behind pattern | Separate C# files | Better organization |
Memoization | Cache expensive calculations | Reduces recalculations |
ShouldRender() | Override in component | Reduces unnecessary renders |
Rendering Fragments | RenderFragment | Improves component composition |
Virtualization Example
<Virtualize Items="@largeDataset" Context="item" OverscanCount="10">
<ItemTemplate>
<div class="item">@item.Name</div>
</ItemTemplate>
<Placeholder>
<div class="placeholder">Loading...</div>
</Placeholder>
</Virtualize>
@code {
private List<DataItem> largeDataset = new();
protected override void OnInitialized()
{
// Initialize with thousands of items
largeDataset = Enumerable.Range(1, 10000)
.Select(i => new DataItem { Id = i, Name = $"Item {i}" })
.ToList();
}
}
ShouldRender Example
@code {
private string previousValue;
private string currentValue;
protected override bool ShouldRender()
{
return currentValue != previousValue;
}
public void UpdateValue(string newValue)
{
previousValue = currentValue;
currentValue = newValue;
}
}
Common Challenges and Solutions
Challenge: Handling Authentication
Solution: Use ASP.NET Core Identity with Blazor
// Program.cs (Server)
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie();
// AuthenticationStateProvider implementation
public class CustomAuthStateProvider : AuthenticationStateProvider
{
private readonly HttpClient _httpClient;
public CustomAuthStateProvider(HttpClient httpClient)
{
_httpClient = httpClient;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
// Get user from API or localStorage
var user = await _httpClient.GetFromJsonAsync<UserInfo>("api/user");
if (user?.IsAuthenticated == true)
{
var claims = new[] { new Claim(ClaimTypes.Name, user.UserName) };
var identity = new ClaimsIdentity(claims, "apiauth");
var principal = new ClaimsPrincipal(identity);
return new AuthenticationState(principal);
}
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
}
}
Challenge: Server-Side Rendering for SEO
Solution: Use prerendering with Blazor Server or SSR in .NET 7+
// Program.cs
builder.Services.AddServerSideBlazor()
.AddCircuitOptions(options => { options.DetailedErrors = true; });
// For WebAssembly with prerendering
builder.Services.AddScoped<IPrerenderService, PrerenderService>();
Challenge: Managing Large Forms
Solution: Split into smaller components and use Flux pattern
<EditForm Model="@complexModel" OnValidSubmit="@HandleSubmit">
<DataAnnotationsValidator />
<PersonalInfoForm @bind-PersonalInfo="complexModel.PersonalInfo" />
<AddressForm @bind-Address="complexModel.Address" />
<PaymentForm @bind-Payment="complexModel.Payment" />
<button type="submit">Submit</button>
</EditForm>
Challenge: Network Connectivity Issues
Solution: Implement offline support with local storage and PWA features
// In program.cs for WebAssembly
builder.Services.AddScoped<OfflineStorageService>();
// Implement service
public class OfflineStorageService
{
private readonly IJSRuntime _js;
public OfflineStorageService(IJSRuntime js)
{
_js = js;
}
public async Task SaveDataLocally(string key, object data)
{
await _js.InvokeVoidAsync("localStorage.setItem",
key, JsonSerializer.Serialize(data));
}
public async Task<T> GetLocalData<T>(string key)
{
var json = await _js.InvokeAsync<string>("localStorage.getItem", key);
if (string.IsNullOrEmpty(json))
return default;
return JsonSerializer.Deserialize<T>(json);
}
}
Best Practices and Tips
Architecture Best Practices
- Separate business logic from UI components
- Use repository pattern for data access
- Create small, focused components
- Implement proper error handling and logging
- Utilize the CQRS pattern for complex applications
Code Organization
- Group related components in folders
- Use shared components for reusable UI elements
- Implement code-behind files for complex components
- Create base components for common functionality
- Use partial classes to split large components
Performance Tips
- Avoid unnecessary renders with
@key
directive - Implement lazy loading for routes
- Use server-side pagination for large datasets
- Minimize JavaScript interop calls
- Cache API results when appropriate
- Use
@memo
for expensive calculations in .NET 8+
Debugging Tips
- Enable detailed errors in circuit options
- Use browser developer tools
- Install Blazor WebAssembly debugging extension
- Add logging with ILogger
- Use browser network tab to inspect API calls
Resources for Further Learning
Official Documentation
Recommended Books
- “Blazor in Action” by Chris Sainty
- “Programming Blazor” by Ed Charbeneau
- “ASP.NET Core Blazor: Create Dynamic Web UIs” by Jimmy Engström
Online Courses and Tutorials
- Pluralsight: “Blazor: Getting Started” by Brian Lagunas
- Udemy: “Blazor – The Complete Guide” by Frank Valbrias
- Microsoft Learn: Blazor path
Community Resources
Blazor Component Libraries