Beginner

Blazor Foundations – ASP.NET Core 10

Razor components, render modes and interactivity, routing and EF Core with a full demo app on Azure.

Technology: .NET 10 LTS, Visual Studio 2026, Blazor Web App Demo Application: Bethany’s Pie Shop HRM (Human Resource Management) Level: Beginner to Intermediate | Estimated Duration: ~2h30


Table of Contents

  1. Why Blazor — The .NET Web Development Revolution
  2. Blazor .NET 10 Project Templates
  3. Blazor Web App Project Structure
  4. Razor Components — The Heart of Blazor
  5. Render Modes and Interactivity
  6. Navigation and Routing
  7. Entity Framework Core InMemory
  8. Demo Application — Bethany’s Pie Shop HRM
  9. Azure App Service Deployment
  10. Best Practices and Productivity
  11. Practical Exercises
  12. Glossary

1. Why Blazor — The .NET Web Development Revolution

1.1 The Problem It Solves

Before Blazor, a .NET developer who wanted to build an interactive web application had to:

  1. Learn JavaScript (or TypeScript)
  2. Master a JS framework (React, Angular, Vue)
  3. Manage two completely different codebases (C# server-side, JS client-side)
  4. Serialize/deserialize data between both worlds
  5. Maintain duplicated models (C# models + TypeScript interfaces)

Blazor solves this problem by allowing C# and .NET to be used on both sides of the equation.

flowchart LR
    subgraph Before["Before Blazor"]
        CS1[C# Server\nASP.NET Core] -->|"JSON API"| JS1[JavaScript\nReact/Angular/Vue]
        JS1 -->|"HTTP Requests"| CS1
    end
    
    subgraph With["With Blazor"]
        CS2[C# Server + Client\nBlazor] -->|"same language,\nsame code"| CS2
    end
    
    style Before fill:#FFE4E4
    style With fill:#E4FFE4

1.2 What Is Blazor?

Blazor is a Microsoft framework, an integral part of ASP.NET Core, that allows you to:

  • Build interactive user interfaces for the web in C# and HTML
  • Use the same code on both server and client
  • Leverage the full .NET ecosystem (NuGet, libraries, tooling)
  • Enjoy a debugging experience identical to other .NET applications
  • Share code across web, mobile, and desktop applications
AspectBlazorReactAngularVue
LanguageC#JavaScript/TypeScriptTypeScriptJavaScript/TypeScript
Runtime.NET / WASMJavaScript EngineJavaScript EngineJavaScript Engine
Code sharing✅ With .NET apps❌ Limited❌ Limited❌ Limited
Type safety✅ Native C#⚠️ TypeScript✅ TypeScript⚠️ TypeScript
NuGet packages✅ Yes❌ npm only❌ npm only❌ npm only
Visual Studio debug✅ Full⚠️ Partial⚠️ Partial⚠️ Partial
PopularityGrowingVery highHighHigh

1.3 .NET 10 — LTS Version

.NET 10 is an LTS (Long-Term Support) version with 3 years of guaranteed support. It’s the best choice for starting new projects.

timeline
    title .NET Version Schedule
    section LTS (3-year support)
        .NET 6 : Nov 2021
        .NET 8 : Nov 2023
        .NET 10 : Nov 2025
    section STS (18-month support)
        .NET 7 : Nov 2022
        .NET 9 : Nov 2024

Blazor updates in .NET 10:

  • SSR-first model by default
  • AddAuthenticationStateSerialization / AddAuthenticationStateDeserialization
  • Rendering performance improvements
  • Better integration with .NET Aspire
  • Enhanced QuickGrid components

2. Blazor .NET 10 Project Templates

2.1 Available Templates

When creating a new Blazor project in Visual Studio 2026:

TemplateDescriptionUse Case
Blazor Web AppThe main .NET 10 templateStandard web applications (recommended)
Blazor WebAssembly Standalone AppPure WASM, no serverOffline apps, PWA
.NET MAUI Blazor Hybrid AppMobile + DesktopNative cross-platform apps
Blazor Server AppRemoved since .NET 8Use Blazor Web App with Server mode
Blazor WebAssembly AppRemoved since .NET 8Use WASM Standalone

2.2 Blazor Web App Template Options

When creating a Blazor Web App, the important options are:

Project name: BethanysPieShopHRM

.NET Version: .NET 10 (LTS)

Authentication type:
    □ None (to start, add later)
    □ Individual Accounts (built-in Identity UI)

Interactive render mode:
    □ None (pure SSR — recommended default to start)
    ● Server (SignalR — reactive from first render)
    □ WebAssembly (WASM — requires initial download)
    □ Auto (Server then WASM — best experience)

Interactivity location:
    ● Global (whole app interactive)
    □ Per page/component (selective interactivity)

☑ Include sample pages

2.3 Understanding Render Modes

flowchart TD
    A[Blazor Web App] --> B{Render Mode}
    
    B --> SSR[None / Static SSR]
    B --> IS[Interactive Server]
    B --> IW[Interactive WebAssembly]
    B --> IA[Interactive Auto]
    
    SSR --> SSR1["✅ SEO-friendly\n✅ Ultra-fast loading\n✅ HttpContext available\n❌ No C# interactivity"]
    
    IS --> IS1["✅ Interactive immediately\n✅ Code on server\n✅ Direct DB access\n⚠️ SignalR connection required"]
    
    IW --> IW1["✅ Offline possible\n✅ Reduce server load\n❌ Initial download\n❌ No direct DB access"]
    
    IA --> IA1["✅ Best UX\n✅ Fast + offline\n⚠️ Hybrid security management\n✅ Recommended for production"]
    
    style SSR fill:#90EE90
    style IS fill:#87CEEB
    style IW fill:#FFD700
    style IA fill:#FF9999

3. Blazor Web App Project Structure

3.1 Architecture with SSR (None mode)

BethanysPieShopHRM/
├── Components/                     ← All Razor components
│   ├── Layout/
│   │   ├── MainLayout.razor        ← Main app layout
│   │   ├── MainLayout.razor.css    ← Isolated CSS for layout
│   │   └── NavMenu.razor           ← Navigation menu
│   ├── Pages/
│   │   ├── Home.razor              ← Home page
│   │   ├── EmployeeOverview.razor  ← Employee list
│   │   └── EmployeeDetail.razor    ← Employee details
│   ├── App.razor                   ← Root component (generates HTML)
│   └── Routes.razor                ← Routing configuration
├── wwwroot/                        ← Static files
│   ├── css/
│   │   └── app.css                 ← Global application CSS
│   ├── lib/
│   │   └── bootstrap/              ← Bootstrap 5.3 (included)
│   └── images/                     ← Application images
├── appsettings.json                ← Configuration
├── appsettings.Development.json    ← Development configuration
└── Program.cs                      ← Application entry point

3.2 Architecture with Server or Auto mode (two projects)

When the interactive mode is WebAssembly or Auto, a second client project is created:

Solution BethanysPieShopHRM
├── BethanysPieShopHRM/             ← Host project (server)
│   ├── Components/
│   │   ├── Layout/
│   │   └── Pages/
│   ├── Program.cs
│   └── appsettings.json
│
├── BethanysPieShopHRM.Client/      ← WASM client project
│   ├── Components/
│   │   └── Pages/
│   │       └── InteractiveAutoPages.razor
│   └── Program.cs                  ← Client-side Program.cs (WASM)
│
└── BethanysPieShopHRM.Shared/      ← Shared project (models, services)
    ├── Models/
    │   ├── Employee.cs
    │   ├── JobCategory.cs
    │   └── Country.cs
    └── Contracts/
        └── IEmployeeRepository.cs

3.3 Key Files in Detail

App.razor — The root of everything:

@* Components/App.razor *@
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <base href="/" />
    @* Bootstrap reference from wwwroot/lib *@
    <link rel="stylesheet" href="lib/bootstrap/dist/css/bootstrap.min.css" />
    @* Application CSS *@
    <link rel="stylesheet" href="app.css" />
    @* Isolated component CSS (auto-generated) *@
    <link rel="stylesheet" href="BethanysPieShopHRM.styles.css" />
    <HeadOutlet />
</head>
<body>
    @* Routes.razor is used here — the main router *@
    <Routes />
    @* Blazor script — loads the framework *@
    <script src="_framework/blazor.web.js"></script>
</body>
</html>

Routes.razor — Routing configuration:

@* Components/Routes.razor *@
<Router AppAssembly="typeof(Program).Assembly">
    <Found Context="routeData">
        @* RouteView for pure SSR, AuthorizeRouteView for auth *@
        <RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
        <FocusOnNavigate RouteData="routeData" Selector="h1" />
    </Found>
    <NotFound>
        <PageTitle>Page not found</PageTitle>
        <LayoutView Layout="typeof(Layout.MainLayout)">
            <p role="alert">Sorry, there is nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

Program.cs — Service configuration:

// Program.cs (SSR only)
var builder = WebApplication.CreateBuilder(args);

// Register Razor Components services
builder.Services.AddRazorComponents();
// For Interactive Server mode:
// builder.Services.AddRazorComponents().AddInteractiveServerComponents();
// For WASM mode:
// builder.Services.AddRazorComponents().AddInteractiveWebAssemblyComponents();

// Business services
builder.Services.AddScoped<IEmployeeDataService, EmployeeDataService>();
builder.Services.AddScoped<IEmployeeRepository, EmployeeRepository>();
builder.Services.AddDbContextFactory<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));

var app = builder.Build();

// Middleware
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAntiforgery();

// Configure Razor components
app.MapRazorComponents<App>();
// For Interactive Server mode:
// app.MapRazorComponents<App>().AddInteractiveServerRenderMode();

app.Run();

4. Razor Components — The Heart of Blazor

4.1 Anatomy of a Razor Component

A Razor component is a .razor file that combines HTML and C#. There are two approaches for organizing the code:

Approach 1 — Everything in one file (default template):

@* Components/Pages/Counter.razor *@
@page "/counter"

<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current value: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Increment</button>

@code {
    private int currentCount = 0;
    
    private void IncrementCount()
    {
        currentCount++;
    }
}

Approach 2 — Code-behind (recommended for real projects):

@* Components/Pages/EmployeeOverview.razor *@
@page "/employees"

<PageTitle>Employees</PageTitle>
<h1>@Title</h1>

@if (Employees == null)
{
    <p>Loading...</p>
}
else
{
    <div class="employee-grid">
        @foreach (var employee in Employees)
        {
            <EmployeeCard Employee="employee" 
                         OnEmployeeSelected="ShowEmployeeDetail" />
        }
    </div>
}
// Components/Pages/EmployeeOverview.razor.cs
// The class is partial — it will be merged with the class generated from the .razor file
public partial class EmployeeOverview
{
    // ─── DEPENDENCY INJECTION ───────────────────────────────────
    [Inject]
    private IEmployeeDataService EmployeeDataService { get; set; } = default!;
    
    [Inject]
    private NavigationManager NavigationManager { get; set; } = default!;
    
    // ─── STATE PROPERTIES ───────────────────────────────────────
    private IEnumerable<Employee>? Employees { get; set; }
    private string Title { get; set; } = "Employee List";
    
    // ─── LIFECYCLE HOOKS ────────────────────────────────────────
    // OnInitialized : synchronous (without async)
    // OnInitializedAsync : asynchronous (with await)
    protected override async Task OnInitializedAsync()
    {
        // Data loaded from DB via service
        Employees = await EmployeeDataService.GetAllEmployeesAsync();
    }
    
    // ─── EVENT HANDLERS ─────────────────────────────────────────
    private void ShowEmployeeDetail(int employeeId)
    {
        NavigationManager.NavigateTo($"/employees/{employeeId}");
    }
}

4.2 Component Lifecycle

sequenceDiagram
    participant Blazor as Blazor Runtime
    participant Component as Component
    participant UI as User Interface

    Blazor->>Component: SetParametersAsync(parameters)
    Component->>Component: OnInitialized() [synchronous]
    Component->>Component: OnInitializedAsync() [async]
    Component->>UI: First render (pre-render)
    
    loop State change
        Component->>Component: StateHasChanged()
        Component->>Component: ShouldRender() ?
        alt ShouldRender() = true
            Component->>UI: Partial re-render
            Component->>Component: OnAfterRender(firstRender)
            Component->>Component: OnAfterRenderAsync(firstRender)
        end
    end
    
    Blazor->>Component: Dispose() [if IDisposable]

Lifecycle methods:

public partial class MyComponent : IDisposable
{
    // 1. Before first render — parameter initialization
    protected override void OnInitialized()
    {
        // Synchronous code — avoid long operations
        // Good for: initializing simple values
    }
    
    // 2. Before first render — async operations
    protected override async Task OnInitializedAsync()
    {
        // Asynchronous code — data loading
        // Blazor does a first render, then updates when this method completes
        Employees = await EmployeeDataService.GetAllEmployeesAsync();
    }
    
    // 3. When parameters change
    protected override async Task OnParametersSetAsync()
    {
        // Triggered when component parameters change
        // E.g.: ID in URL changes, reload data
        if (EmployeeId.HasValue)
        {
            CurrentEmployee = await EmployeeDataService.GetEmployeeByIdAsync(EmployeeId.Value);
        }
    }
    
    // 4. After each render (client-side only for firstRender=true)
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            // Safe for JavaScript Interop (DOM available)
            await JSRuntime.InvokeVoidAsync("initializeChart", "#salesChart");
        }
    }
    
    // 5. Resource cleanup
    public void Dispose()
    {
        // Cancel timers, unsubscribe from events, etc.
    }
}

4.3 Component Parameters

// Components/EmployeeCard.razor.cs
public partial class EmployeeCard
{
    // ─── SIMPLE PARAMETER ───────────────────────────────────────
    // Value passed from the parent component
    [Parameter]
    public Employee Employee { get; set; } = default!;
    
    // ─── CALLBACK PARAMETER ─────────────────────────────────────
    // EventCallback to notify the parent of an event
    [Parameter]
    public EventCallback<int> OnQuickViewRequested { get; set; }
    
    [Parameter]
    public EventCallback<Employee> OnEmployeeSelected { get; set; }
    
    // ─── OPTIONAL PARAMETER WITH DEFAULT VALUE ──────────────────
    [Parameter]
    public bool ShowFullDetails { get; set; } = false;
    
    // ─── CAPTURE UNRECOGNIZED ATTRIBUTES ────────────────────────
    // Useful for wrapper components
    [Parameter(CaptureUnmatchedValues = true)]
    public Dictionary<string, object>? AdditionalAttributes { get; set; }
    
    // ─── CHILD CONTENT ──────────────────────────────────────────
    [Parameter]
    public RenderFragment? ChildContent { get; set; }
    
    // ─── EVENT HANDLER ──────────────────────────────────────────
    private async Task HandleQuickViewClick()
    {
        await OnQuickViewRequested.InvokeAsync(Employee.EmployeeId);
    }
    
    private async Task HandleEmployeeSelected()
    {
        await OnEmployeeSelected.InvokeAsync(Employee);
    }
}
@* Components/EmployeeCard.razor — Template *@
<div class="employee-card card" @attributes="AdditionalAttributes">
    <div class="card-body">
        @if (Employee.ProfilePicture is not null)
        {
            <img src="@Employee.ProfilePicture" alt="@Employee.FirstName @Employee.LastName" 
                 class="profile-picture" />
        }
        <h5 class="card-title">@Employee.FirstName @Employee.LastName</h5>
        <p class="card-text">@Employee.Email</p>
        <p class="card-text text-muted">@Employee.JobCategory?.JobCategoryName</p>
        
        @* Customizable content *@
        @if (ChildContent is not null)
        {
            @ChildContent
        }
        
        @* Action buttons *@
        <div class="card-actions">
            <button class="btn btn-sm btn-outline-primary" @onclick="HandleQuickViewClick">
                Quick View
            </button>
            <button class="btn btn-sm btn-primary" @onclick="HandleEmployeeSelected">
                Full Details
            </button>
        </div>
    </div>
</div>

Usage from the parent:

@* In EmployeeOverview.razor *@
@foreach (var employee in Employees!)
{
    <EmployeeCard Employee="@employee"
                 OnQuickViewRequested="@ShowQuickView"
                 OnEmployeeSelected="@NavigateToDetail"
                 class="mb-3">
        @* Custom ChildContent *@
        <span class="badge bg-success">@employee.JobCategory?.JobCategoryName</span>
    </EmployeeCard>
}

4.4 Data Binding

@* One-way binding: display a value *@
<p>@employee.FirstName</p>

@* Two-way binding with @bind *@
<input @bind="@searchTerm" type="text" placeholder="Search..." />
@* Equivalent to: *@
<input value="@searchTerm" @onchange="@(e => searchTerm = e.Value?.ToString() ?? "")" />

@* Two-way binding with EventCallback (both directions) *@
<ToggleSwitch IsOn="@isHolidayMode" IsOnChanged="@OnHolidayModeChanged" />
@code {
    private bool isHolidayMode;
    private void OnHolidayModeChanged(bool value) => isHolidayMode = value;
}

@* Binding with custom event *@
<input @bind="@searchTerm" @bind:event="oninput" />
@* @bind:event="oninput" updates on every keystroke (not just onblur) *@

@* Formatted binding *@
<input @bind="@startDate" @bind:format="yyyy-MM-dd" type="date" />

5. Render Modes and Interactivity

5.1 Static Server-Side Rendering (SSR)

The default mode. Each navigation generates a complete HTTP request/response cycle.

@* No rendermode attribute = SSR by default *@
@page "/employees"

<h1>Employees</h1>
@* Content will be rendered server-side and sent as static HTML *@

@* ⚠️ No @onclick in pure SSR — use HTML forms *@
@* ⚠️ No dynamic UI updates *@

When to use SSR:

  • Public pages (SEO important)
  • Static content pages
  • Simple submission forms

5.2 Interactive Server (SignalR)

@page "/employee-detail/{employeeId:int}"
@rendermode InteractiveServer  ← Adding interactivity

<PageTitle>Employee — @Employee?.FirstName @Employee?.LastName</PageTitle>

<div class="employee-detail">
    @if (Employee is not null)
    {
        <h1>@Employee.FirstName @Employee.LastName</h1>
        
        @* Interactive button — works thanks to SignalR *@
        <div class="holiday-status">
            <span>Holiday mode: @(Employee.IsOnHoliday ? "✅ Active" : "❌ Inactive")</span>
            <button class="btn btn-outline-secondary" @onclick="ToggleHolidayMode">
                @(Employee.IsOnHoliday ? "Deactivate" : "Activate")
            </button>
        </div>
    }
</div>
// EmployeeDetail.razor.cs
public partial class EmployeeDetail
{
    [Parameter]
    public int EmployeeId { get; set; }
    
    [Inject]
    private IEmployeeDataService EmployeeDataService { get; set; } = default!;
    
    private Employee? Employee { get; set; }
    
    protected override async Task OnInitializedAsync()
    {
        Employee = await EmployeeDataService.GetEmployeeByIdAsync(EmployeeId);
    }
    
    private async Task ToggleHolidayMode()
    {
        if (Employee is null) return;
        
        // Modify state
        Employee.IsOnHoliday = !Employee.IsOnHoliday;
        
        // Persist the change
        await EmployeeDataService.UpdateEmployeeAsync(Employee);
        
        // StateHasChanged() is called automatically after a @onclick event
        // But can be called manually if needed
        StateHasChanged();
    }
}

Configure Server mode in Program.cs:

// Program.cs — Enable Server mode
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents(); // ← Add this line

// In the pipeline:
app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode(); // ← Add this line

5.3 Streaming Rendering — Improving UX

Streaming Rendering is an SSR enhancement that allows displaying an initial UI while data loads:

sequenceDiagram
    participant Browser as Browser
    participant Server as Server

    Browser->>Server: GET /employees
    Server->>Browser: Initial HTML (loading spinner)
    Note over Browser: User sees something immediately!
    
    Server->>Server: DB query in progress...
    Server->>Browser: Final HTML with data
    Note over Browser: UI auto-updated
@* EmployeeOverview.razor — With Streaming Rendering *@
@page "/employees"
@attribute [StreamRendering(true)]  ← Enable streaming

@if (Employees == null)
{
    @* Initial UI displayed immediately *@
    <div class="loading-placeholder">
        <div class="spinner-border text-primary" role="status">
            <span class="visually-hidden">Loading...</span>
        </div>
        <p>Loading employees...</p>
    </div>
}
else if (!Employees.Any())
{
    <p class="text-muted">No employees found.</p>
}
else
{
    @* Final UI with data *@
    <div class="row">
        @foreach (var employee in Employees)
        {
            <div class="col-md-4 mb-3">
                <EmployeeCard Employee="@employee" />
            </div>
        }
    </div>
}
// EmployeeOverview.razor.cs
public partial class EmployeeOverview
{
    [Inject]
    private IEmployeeDataService EmployeeDataService { get; set; } = default!;
    
    // Initialize to null to trigger loading display
    private IEnumerable<Employee>? Employees { get; set; }
    
    protected override async Task OnInitializedAsync()
    {
        // Simulate DB latency (in production: real query)
        await Task.Delay(2000);
        
        // Data arrives → Streaming Rendering updates the UI
        Employees = await EmployeeDataService.GetAllEmployeesAsync();
    }
}

6. Navigation and Routing

6.1 Routing Configuration

flowchart LR
    Browser[Browser\n/employees/5] --> Router[Router Component\nRoutes.razor]
    Router --> |"Scan @page directives"| Components[Components with @page]
    Components --> |"Parameter ID=5"| Detail[EmployeeDetail.razor]
    Router --> |"Route not found"| NotFound[404 Page]

6.2 @page Directives and Route Parameters

@* Simple route *@
@page "/employees"

@* Route with required parameter *@
@page "/employees/{EmployeeId:int}"

@* Route with optional parameter *@
@page "/employees/{EmployeeId:int?}"

@* Multiple routes on the same component *@
@page "/employees"
@page "/staff"
@page "/personnel"

@* Type constraints *@
@page "/employees/{Id:int}"      @* Integer *@
@page "/products/{Slug:alpha}"   @* Letters only *@
@page "/reports/{Date:datetime}" @* Date *@
@page "/items/{Price:decimal}"   @* Decimal *@
// EmployeeDetail.razor.cs
public partial class EmployeeDetail
{
    // Parameter must match the name in @page
    [Parameter]
    public int EmployeeId { get; set; }
    
    // Query string (from URL: /employees?search=john)
    [SupplyParameterFromQuery]
    public string? SearchTerm { get; set; }
    
    [SupplyParameterFromQuery(Name = "page")]
    public int CurrentPage { get; set; } = 1;
}

6.3 Programmatic Navigation

// Inject NavigationManager
[Inject]
private NavigationManager NavigationManager { get; set; } = default!;

// ─── SIMPLE NAVIGATION ───────────────────────────────────────────
private void GoToEmployeeList()
{
    NavigationManager.NavigateTo("/employees");
}

// ─── NAVIGATION WITH PARAMETER ──────────────────────────────────
private void GoToEmployee(int employeeId)
{
    NavigationManager.NavigateTo($"/employees/{employeeId}");
}

// ─── NAVIGATION WITH QUERY STRING ───────────────────────────────
private void SearchEmployees(string term)
{
    NavigationManager.NavigateTo($"/employees?search={Uri.EscapeDataString(term)}");
}

// ─── FORCED NAVIGATION (full page reload) ───────────────────────
// Required for SSR pages from interactive components
private void GoToLoginPage()
{
    NavigationManager.NavigateTo("/Account/Login", forceLoad: true);
}

// ─── GO BACK ─────────────────────────────────────────────────────
private void GoBack()
{
    NavigationManager.NavigateTo("javascript:history.back()");
    // Or use JSRuntime.InvokeVoidAsync("history.back")
}

// ─── CURRENT URL ─────────────────────────────────────────────────
private string GetCurrentUrl()
{
    return NavigationManager.Uri;
    // E.g.: "https://localhost:5001/employees/42"
}

private string GetRelativeUrl()
{
    return NavigationManager.ToBaseRelativePath(NavigationManager.Uri);
    // E.g.: "employees/42"
}
@* NavMenu.razor — Navigation links with active highlight *@
<nav class="sidebar">
    @* NavLink automatically adds "active" when URL matches *@
    <NavLink href="/" class="nav-link" Match="NavLinkMatch.All">
        <span class="oi oi-home"></span> Home
    </NavLink>
    
    @* NavLinkMatch.Prefix: active for /employees and all sub-paths *@
    <NavLink href="/employees" class="nav-link" Match="NavLinkMatch.Prefix">
        <span class="oi oi-people"></span> Employees
    </NavLink>
    
    @* NavLink is a Blazor component, not just an <a> tag *@
    @* It monitors navigation and updates the CSS class *@
</nav>

7. Entity Framework Core InMemory

7.1 Why InMemory for Demos?

FeatureInMemorySQL ServerSQLite
SetupNuGet onlySQL Server requiredNuGet + file
Cross-platform✅ Yes❌ (LocalDB Windows)✅ Yes
Persistence❌ Lost on restart✅ Permanent✅ Permanent
Migrations❌ Not needed✅ Required✅ Required
Unit tests✅ Perfect⚠️ More complex✅ Possible
Production❌ Not recommended✅ Yes✅ Small apps

7.2 EF Core InMemory Configuration

// 1. Install NuGet package:
// Microsoft.EntityFrameworkCore.InMemory

// 2. Create the DbContext
// Data/AppDbContext.cs

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
    {
    }
    
    // DbSet = table in the database
    public DbSet<Employee> Employees { get; set; }
    public DbSet<Country> Countries { get; set; }
    public DbSet<JobCategory> JobCategories { get; set; }
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        
        // Relationship configuration (Fluent API)
        modelBuilder.Entity<Employee>()
            .HasOne(e => e.Country)
            .WithMany()
            .HasForeignKey(e => e.CountryId);
        
        modelBuilder.Entity<Employee>()
            .HasOne(e => e.JobCategory)
            .WithMany()
            .HasForeignKey(e => e.JobCategoryId);
    }
}

// 3. Register in Program.cs
builder.Services.AddDbContextFactory<AppDbContext>(options =>
    options.UseInMemoryDatabase("BethanysPieShopDB"));
    
// For EF Core InMemory, use DbContextFactory (not DbContext directly)
// because DbContext is not thread-safe

7.3 Seeding (Initial Data)

// Program.cs — Initialize data after app is built
var app = builder.Build();

// Seed initial data
using (var scope = app.Services.CreateScope())
{
    var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
    
    // Add data only if DB is empty
    if (!context.Employees.Any())
    {
        // Countries
        var belgium = new Country { CountryId = 1, CountryName = "Belgium" };
        var france = new Country { CountryId = 2, CountryName = "France" };
        context.Countries.AddRange(belgium, france);
        
        // Job categories
        var dev = new JobCategory { JobCategoryId = 1, JobCategoryName = "Developer" };
        var manager = new JobCategory { JobCategoryId = 2, JobCategoryName = "Manager" };
        context.JobCategories.AddRange(dev, manager);
        
        // Employees
        context.Employees.AddRange(
            new Employee
            {
                EmployeeId = 1,
                FirstName = "Marie",
                LastName = "Dupont",
                Email = "marie.dupont@bethany.com",
                HireDate = new DateTime(2020, 3, 15),
                IsOnHoliday = false,
                CountryId = 1,
                JobCategoryId = 1
            },
            new Employee
            {
                EmployeeId = 2,
                FirstName = "John",
                LastName = "Martin",
                Email = "john.martin@bethany.com",
                HireDate = new DateTime(2019, 7, 1),
                IsOnHoliday = true,
                CountryId = 2,
                JobCategoryId = 2
            },
            new Employee
            {
                EmployeeId = 3,
                FirstName = "Sophie",
                LastName = "Laurent",
                Email = "sophie.laurent@bethany.com",
                HireDate = new DateTime(2021, 11, 20),
                IsOnHoliday = false,
                CountryId = 1,
                JobCategoryId = 1
            }
        );
        
        context.SaveChanges();
    }
}

7.4 Repository Pattern with EF Core

// Contracts/Repository/IEmployeeRepository.cs
public interface IEmployeeRepository
{
    Task<IEnumerable<Employee>> GetAllEmployeesAsync();
    Task<Employee?> GetEmployeeByIdAsync(int employeeId);
    Task<Employee> AddEmployeeAsync(Employee employee);
    Task<Employee> UpdateEmployeeAsync(Employee employee);
    Task DeleteEmployeeAsync(int employeeId);
}

// Repositories/EmployeeRepository.cs
public class EmployeeRepository : IEmployeeRepository
{
    // DbContextFactory is thread-safe (unlike direct DbContext)
    private readonly IDbContextFactory<AppDbContext> _contextFactory;
    
    public EmployeeRepository(IDbContextFactory<AppDbContext> contextFactory)
    {
        _contextFactory = contextFactory;
    }
    
    public async Task<IEnumerable<Employee>> GetAllEmployeesAsync()
    {
        // Create a new context for each operation
        await using var context = await _contextFactory.CreateDbContextAsync();
        
        return await context.Employees
            .Include(e => e.Country)
            .Include(e => e.JobCategory)
            .OrderBy(e => e.LastName)
            .ToListAsync();
    }
    
    public async Task<Employee?> GetEmployeeByIdAsync(int employeeId)
    {
        await using var context = await _contextFactory.CreateDbContextAsync();
        
        return await context.Employees
            .Include(e => e.Country)
            .Include(e => e.JobCategory)
            .FirstOrDefaultAsync(e => e.EmployeeId == employeeId);
    }
    
    public async Task<Employee> AddEmployeeAsync(Employee employee)
    {
        await using var context = await _contextFactory.CreateDbContextAsync();
        
        context.Employees.Add(employee);
        await context.SaveChangesAsync();
        return employee;
    }
    
    public async Task<Employee> UpdateEmployeeAsync(Employee employee)
    {
        await using var context = await _contextFactory.CreateDbContextAsync();
        
        var existingEmployee = await context.Employees.FindAsync(employee.EmployeeId);
        if (existingEmployee is null)
            throw new KeyNotFoundException($"Employee {employee.EmployeeId} not found.");
        
        // Map modified properties
        context.Entry(existingEmployee).CurrentValues.SetValues(employee);
        await context.SaveChangesAsync();
        
        return existingEmployee;
    }
    
    public async Task DeleteEmployeeAsync(int employeeId)
    {
        await using var context = await _contextFactory.CreateDbContextAsync();
        
        var employee = await context.Employees.FindAsync(employeeId);
        if (employee is not null)
        {
            context.Employees.Remove(employee);
            await context.SaveChangesAsync();
        }
    }
}

// Services/EmployeeDataService.cs — Service layer (between UI and Repository)
public class EmployeeDataService : IEmployeeDataService
{
    private readonly IEmployeeRepository _employeeRepository;
    
    public EmployeeDataService(IEmployeeRepository employeeRepository)
    {
        _employeeRepository = employeeRepository;
    }
    
    public async Task<IEnumerable<Employee>> GetAllEmployeesAsync()
    {
        return await _employeeRepository.GetAllEmployeesAsync();
    }
    
    public async Task<Employee?> GetEmployeeByIdAsync(int employeeId)
    {
        return await _employeeRepository.GetEmployeeByIdAsync(employeeId);
    }
    
    // Additional business logic possible here
    public async Task<IEnumerable<Employee>> GetEmployeesOnHolidayAsync()
    {
        var allEmployees = await _employeeRepository.GetAllEmployeesAsync();
        return allEmployees.Where(e => e.IsOnHoliday);
    }
}

8. Demo Application — Bethany’s Pie Shop HRM

8.1 Data Models

// BethanysPieShopHRM.Shared/Models/Employee.cs
public class Employee
{
    public int EmployeeId { get; set; }
    
    [Required(ErrorMessage = "First name is required")]
    [StringLength(50, ErrorMessage = "First name cannot exceed 50 characters")]
    public string FirstName { get; set; } = string.Empty;
    
    [Required(ErrorMessage = "Last name is required")]
    [StringLength(50)]
    public string LastName { get; set; } = string.Empty;
    
    [Required]
    [EmailAddress(ErrorMessage = "Invalid email format")]
    public string Email { get; set; } = string.Empty;
    
    public string? PhoneNumber { get; set; }
    
    [Required]
    [DataType(DataType.Date)]
    public DateTime HireDate { get; set; }
    
    public bool IsOnHoliday { get; set; }
    
    public string? ProfilePicture { get; set; }
    
    // Navigation properties
    public int CountryId { get; set; }
    public Country? Country { get; set; }
    
    public int JobCategoryId { get; set; }
    public JobCategory? JobCategory { get; set; }
    
    // Computed property
    public string FullName => $"{FirstName} {LastName}";
    
    public int YearsOfService => 
        (DateTime.Today - HireDate).Days / 365;
}

// BethanysPieShopHRM.Shared/Models/Country.cs
public class Country
{
    public int CountryId { get; set; }
    public string CountryName { get; set; } = string.Empty;
    public string? CountryCode { get; set; } // E.g.: "BE", "FR"
}

// BethanysPieShopHRM.Shared/Models/JobCategory.cs
public class JobCategory
{
    public int JobCategoryId { get; set; }
    public string JobCategoryName { get; set; } = string.Empty;
    public string? Description { get; set; }
}

8.2 Custom Layout

@* Components/Layout/MainLayout.razor *@
@inherits LayoutComponentBase

<div class="page">
    @* Left sidebar *@
    <div class="sidebar">
        <div class="top-row ps-3 navbar navbar-dark">
            <div class="container-fluid">
                <a class="navbar-brand" href="">
                    <img src="/images/bethany-logo.png" alt="Bethany's Pie Shop" height="40" />
                </a>
            </div>
        </div>
        <input type="checkbox" title="Navigation menu" class="navbar-toggler" />
        <div class="nav-scrollable">
            <NavMenu />
        </div>
    </div>
    
    @* Main content area *@
    <main>
        <div class="top-row px-4">
            <a href="https://learn.microsoft.com/aspnet/core/" target="_blank">
                About
            </a>
        </div>
        <article class="content px-4">
            @* Body is injected here by pages *@
            @Body
        </article>
    </main>
</div>
@* Components/Layout/NavMenu.razor *@
<div class="nav-item px-3">
    <NavLink class="nav-link" href="/" Match="NavLinkMatch.All">
        <span class="bi bi-house-door-fill" aria-hidden="true"></span> Home
    </NavLink>
</div>
<div class="nav-item px-3">
    <NavLink class="nav-link" href="/employees">
        <span class="bi bi-people-fill" aria-hidden="true"></span> Employees
    </NavLink>
</div>

8.3 Advanced Components — ErrorBoundary and DynamicComponent

@* Using ErrorBoundary to isolate errors *@
@foreach (var employee in Employees!)
{
    @* If EmployeeCard throws an exception, only that card displays the error *@
    @* The rest of the page continues working normally *@
    <ErrorBoundary>
        <ChildContent>
            <EmployeeCard Employee="@employee" />
        </ChildContent>
        <ErrorContent Context="exception">
            <div class="alert alert-warning">
                <strong>Invalid employee</strong> — Corrupted data
                @* In development, show details: *@
                @if (app.Environment.IsDevelopment())
                {
                    <p>@exception.Message</p>
                }
            </div>
        </ErrorContent>
    </ErrorBoundary>
}
@* Components/Pages/Home.razor — Dynamic widgets *@
@page "/"

<PageTitle>Dashboard — Bethany's Pie Shop</PageTitle>

<h1>Dashboard</h1>
<div class="dashboard-widgets row">
    @foreach (var widgetType in _widgetTypes)
    {
        <div class="col-md-4 mb-3">
            @* DynamicComponent instantiates the component by its type *@
            <DynamicComponent Type="@widgetType" 
                             Parameters="@GetWidgetParameters(widgetType)" />
        </div>
    }
</div>

@code {
    // List of widget types to display dynamically
    private List<Type> _widgetTypes = new()
    {
        typeof(EmployeeCountWidget),
        typeof(InboxWidget),
        typeof(HolidayWidget)
    };
    
    private Dictionary<string, object>? GetWidgetParameters(Type widgetType)
    {
        // Specific parameters for each widget type
        if (widgetType == typeof(EmployeeCountWidget))
        {
            return new Dictionary<string, object>
            {
                { "ShowTrend", true }
            };
        }
        return null;
    }
}

9. Azure App Service Deployment

9.1 Azure App Service — Concepts

flowchart TB
    subgraph Azure["Microsoft Azure"]
        RG[Resource Group\nBethanysPieShopHRM-RG]
        
        subgraph AppServicePlan["App Service Plan\nS1 Standard"]
            AS[App Service\nBethanysPieShopHRM]
        end
        
        subgraph Storage["Azure Storage (optional)"]
            Blobs[Blob Storage\nImages / Files]
        end
        
        subgraph DB["Azure SQL (optional)"]
            SQL[Azure SQL Database]
        end
        
        RG --> AppServicePlan
        RG --> Storage
        RG --> DB
        AS -->|"Connection"| SQL
        AS -->|"File storage"| Blobs
    end
    
    Dev[Visual Studio\nPublish] -->|"Deploy"| AS
    CI[GitHub Actions\nCI/CD] -->|"Automatic deploy"| AS

9.2 Deploying from Visual Studio

Steps:
1. Right-click on project → Publish
2. Target: Azure → Azure App Service (Windows)
3. Create new App Service:
   - Name: bethany-pieshop-hrm (must be globally unique)
   - Subscription: Your Azure subscription
   - Resource Group: Create new or existing
   - App Service Plan: Create new (choose S1 tier for production)
4. Create → Next → Finish
5. Publish → Application is deployed!

App Service Plan — Performance Tiers:

TierCPURAMEst. price/monthUse case
F1 FreeShared1 GBFreeDev/Test only
D1 SharedShared1 GB~$10Dev/Test
B1 Basic1 vCore1.75 GB~$54Small production
S1 Standard1 vCore1.75 GB~$75Production (with slots)
P1v3 Premium2 vCores8 GB~$138High-availability production

9.3 Production Configuration

// appsettings.json (non-secret values)
{
  "ConnectionStrings": {
    "Default": "placeholder — defined in App Service Configuration"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Warning",
      "Microsoft.AspNetCore": "Warning"
    }
  }
}

// appsettings.Development.json (development only)
{
  "ConnectionStrings": {
    "Default": "Server=(localdb)\\mssqllocaldb;Database=BethanysPieShopHRM;Trusted_Connection=True"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "Microsoft.AspNetCore": "Information"
    }
  }
}

Environment variables in Azure App Service:

In Azure portal → App Service → Configuration → Application settings
→ Add:
    ConnectionStrings__Default = Server=...;Database=...;User=...;Password=...
    (Double underscore __ = section separator in JSON hierarchy)

10. Best Practices and Productivity

BethanysPieShopHRM/
├── Components/
│   ├── Layout/              ← Application layouts
│   ├── Pages/               ← Navigable pages (@page)
│   ├── Shared/              ← Shared components (EmployeeCard, etc.)
│   └── Widgets/             ← Dashboard components
├── Contracts/               ← Interfaces (IEmployeeRepository, etc.)
│   ├── Repository/
│   └── Services/
├── Data/                    ← DbContext, migrations
├── Extensions/              ← Extension methods
├── Models/                  ← App-specific DTOs (if different from entities)
├── Repositories/            ← Repository implementations
├── Services/                ← Business services
├── State/                   ← Shared state classes
└── wwwroot/                 ← Static files

10.2 Hot Reload — Development Productivity

Blazor supports Hot Reload: code changes are applied without restarting the application:

Visual Studio 2026: F5 to launch → Modify code → Ctrl+Shift+Alt+F5 to apply
Or: CSS/HTML changes are often applied automatically

CLI: dotnet watch run
→ Watches for changes and automatically reloads

10.3 Blazor Debugging

// Set a breakpoint in code-behind
public partial class EmployeeDetail
{
    protected override async Task OnInitializedAsync()
    {
        // ← Breakpoint here — works exactly like in a console app!
        Employee = await EmployeeDataService.GetEmployeeByIdAsync(EmployeeId);
        // Inspect Employee in Local Variables, Autos, Watch
    }
}

Available debugging features:

  • Breakpoints in .razor.cs files
  • Local variables, Autos, Watch Windows
  • Step Into, Step Over, Step Out
  • Immediate Window for evaluating expressions
  • Exception settings to catch all exceptions

10.4 Component-Scoped CSS

/* EmployeeCard.razor.css — Styles ONLY for EmployeeCard */
/* Compiled automatically with a unique selector */
/* No conflicts with other components */

.employee-card {
    border-radius: 12px;
    transition: transform 0.2s ease;
}

.employee-card:hover {
    transform: translateY(-4px);
    box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}

.profile-picture {
    width: 80px;
    height: 80px;
    border-radius: 50%;
    object-fit: cover;
}

11. Practical Exercises

Exercise 1 — Basic Component (Beginner, ~45min)

Goal: Create an EmployeeCard component displaying employee information.

Steps:

  1. Create Components/EmployeeCard.razor and EmployeeCard.razor.cs
  2. Add a [Parameter] of type Employee
  3. Display: Photo, Full name, Email, Department
  4. Use isolated CSS to style the card

Starter code:

@* EmployeeCard.razor *@
<div class="employee-card">
    @* TODO: Display Employee information *@
    @* Hint: Employee.FirstName, Employee.LastName, Employee.Email *@
</div>

Exercise 2 — Streaming Rendering (Intermediate, ~1h)

Goal: Add a simulated delay and Streaming Rendering to the employee page.

Steps:

  1. Add await Task.Delay(3000) in OnInitializedAsync
  2. Add @attribute [StreamRendering(true)]
  3. Create a loading UI (skeleton or spinner)
  4. Observe the behavior in the browser

Exercise 3 — Interactive Mode (Intermediate, ~1h30)

Goal: Add an “On holiday” toggle to the employee detail page.

Steps:

  1. Add @rendermode InteractiveServer on the detail page
  2. Add an IsOnHoliday property to the employee
  3. Create a button that toggles the state
  4. Persist the change via the service
  5. Verify the UI updates without a page reload

Exercise 4 — ErrorBoundary (Advanced, ~1h)

Goal: Implement robust error handling.

Steps:

  1. Add an employee with invalid data in seeding
  2. Make EmployeeCard throw an exception for that employee
  3. Wrap each EmployeeCard in an <ErrorBoundary>
  4. Customize the displayed error message

12. Glossary

TermDefinition
App ServiceAzure service for hosting web applications
App Service PlanUnderlying infrastructure of an App Service (CPU, RAM, pricing)
CircuitPersistent SignalR connection between a browser and a Blazor server
Code-behind.razor.cs file containing logic separated from the Razor template
Component classPartial C# class containing a Razor component’s logic
DbContextEF Core session representing a database connection
DbContextFactoryThread-safe DbContext factory for Blazor Server
DOMDocument Object Model — tree representation of an HTML page
ErrorBoundaryBlazor component isolating exceptions to prevent page crashes
EventCallbackBlazor type for asynchronous callbacks between components
Hot ReloadHot code reloading without restarting
InMemory ProviderEF Core provider storing data in memory (for dev/tests)
LayoutComponent defining the common structure for multiple pages
LTSLong-Term Support — .NET version supported for 3 years
NavLinkBlazor component replacing <a> with active state management
NavigationManagerBlazor service for programmatic navigation
NuGet.NET package manager
ORMObject-Relational Mapper — EF Core translates C# ↔ SQL
Partial classClass split into multiple files (Razor + code-behind)
Razor Component.razor file combining HTML and C#
Razor syntaxSyntax for embedding C# in HTML with @
RenderFragmentType representing UI content passable as a parameter
Repository PatternPattern encapsulating data access in dedicated classes
Resource GroupAzure logical container grouping related resources
Scoped serviceService whose lifetime matches an HTTP request
Seed dataInitial data inserted into the DB at application startup
SignalRMicrosoft technology for real-time communication (WebSocket)
SingletonService with a single instance throughout the app’s lifetime
SSRServer-Side Rendering — HTML rendered on the server
Streaming RenderingTechnique sending the initial UI then updating with data
WASMWebAssembly — binary format running in the browser
wwwrootPublic static files folder (CSS, JS, images)
@pageRazor directive defining the route for a navigable component
@rendermodeRazor directive specifying the render mode for a component
@bindRazor directive for two-way data binding
@codeRazor block containing C# code in a component

Additional Resources


Quick Reference Table of Contents

  1. Why Blazor?
  2. Blazor Project Templates
  3. Blazor Web App Project Structure
  4. Components and Interactivity
  5. Navigation and Routing
  6. Entity Framework Core InMemory
  7. Azure App Service Deployment
  8. Summary and Key Points

Search Terms

asp.net · blazor · foundations · core · web · ui · c# · .net · development · app · component · configuration · data · inmemory · mode · rendering · architecture · azure · components · errorboundary · interactive · modes · navigation · parameters

Interested in this course?

Contact us to book it or get a custom training plan for your team.