Blazor Framework: The Ultimate Developer’s Cheat Sheet

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

ConceptDescription
Component ModelUI pieces built with C# and Razor markup (.razor files)
Razor SyntaxCombines HTML with C# code (prefixed with @)
Hosting ModelsServer-side or WebAssembly client-side execution
Data BindingOne-way (@variable) or two-way (@bind-Value)
Event Handling@onclick, @onchange, etc. with C# method handlers
Dependency InjectionBuilt-in DI container for service management
JavaScript InteropCommunication between C# and JavaScript

Hosting Models Compared

FeatureBlazor ServerBlazor WebAssembly
ExecutionServer-sideClient-side in browser
Initial LoadFasterSlower (downloads .NET runtime)
PerformanceLower client requirementsHigher client requirements
Offline SupportNoYes (with PWA)
ConnectionRequires SignalR connectionNo persistent connection
Server ResourcesHigher (maintains user sessions)Lower (stateless)
SecurityCode never leaves serverCode 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

MethodDescriptionWhen to Use
OnInitializedSynchronous initializationSimple initialization
OnInitializedAsyncAsynchronous initializationDatabase/API calls
OnParametersSetAfter parameters are setReact to parameter changes
OnParametersSetAsyncAsync version of parameters setAsync operations after params
OnAfterRenderAfter component rendersDOM manipulation
OnAfterRenderAsyncAsync version of after renderAsync operations after render
ShouldRenderControls renderingOptimization
DisposeComponent cleanupResource 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

ComponentHTML EquivalentDescription
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
InputRadioGroupGroup of radiosRadio 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

ConstraintExampleDescription
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

ApproachUse CaseProsCons
ServicesSimple app stateBuilt-in, easy to useLimited reactivity
State Container PatternMedium complexityCustom notificationsMore boilerplate
Flux/Redux PatternComplex statePredictable updatesHigher complexity
Blazor FluxorComplex stateRedux-likeExternal dependency
Browser StoragePersistent stateSurvives refreshesLimited 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

TechniqueImplementationBenefit
Virtualization<Virtualize> componentRenders only visible items
Lazy Loading@page componentsLoads components on demand
Code-behind patternSeparate C# filesBetter organization
MemoizationCache expensive calculationsReduces recalculations
ShouldRender()Override in componentReduces unnecessary renders
Rendering FragmentsRenderFragmentImproves 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

Scroll to Top