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
- Why Blazor — The .NET Web Development Revolution
- Blazor .NET 10 Project Templates
- Blazor Web App Project Structure
- Razor Components — The Heart of Blazor
- Render Modes and Interactivity
- Navigation and Routing
- Entity Framework Core InMemory
- Demo Application — Bethany’s Pie Shop HRM
- Azure App Service Deployment
- Best Practices and Productivity
- Practical Exercises
- 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:
- Learn JavaScript (or TypeScript)
- Master a JS framework (React, Angular, Vue)
- Manage two completely different codebases (C# server-side, JS client-side)
- Serialize/deserialize data between both worlds
- 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
| Aspect | Blazor | React | Angular | Vue |
|---|---|---|---|---|
| Language | C# | JavaScript/TypeScript | TypeScript | JavaScript/TypeScript |
| Runtime | .NET / WASM | JavaScript Engine | JavaScript Engine | JavaScript 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 |
| Popularity | Growing | Very high | High | High |
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:
| Template | Description | Use Case |
|---|---|---|
| Blazor Web App | The main .NET 10 template | Standard web applications (recommended) |
| Blazor WebAssembly Standalone App | Pure WASM, no server | Offline apps, PWA |
| .NET MAUI Blazor Hybrid App | Mobile + Desktop | Native cross-platform apps |
| Removed since .NET 8 | Use Blazor Web App with Server mode | |
| Removed since .NET 8 | Use 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"
}
6.4 NavLink — Links with Active Class
@* 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?
| Feature | InMemory | SQL Server | SQLite |
|---|---|---|---|
| Setup | NuGet only | SQL Server required | NuGet + 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:
| Tier | CPU | RAM | Est. price/month | Use case |
|---|---|---|---|---|
| F1 Free | Shared | 1 GB | Free | Dev/Test only |
| D1 Shared | Shared | 1 GB | ~$10 | Dev/Test |
| B1 Basic | 1 vCore | 1.75 GB | ~$54 | Small production |
| S1 Standard | 1 vCore | 1.75 GB | ~$75 | Production (with slots) |
| P1v3 Premium | 2 vCores | 8 GB | ~$138 | High-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
10.1 Recommended Blazor Project Structure
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.csfiles - 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:
- Create
Components/EmployeeCard.razorandEmployeeCard.razor.cs - Add a
[Parameter]of typeEmployee - Display: Photo, Full name, Email, Department
- 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:
- Add
await Task.Delay(3000)inOnInitializedAsync - Add
@attribute [StreamRendering(true)] - Create a loading UI (skeleton or spinner)
- Observe the behavior in the browser
Exercise 3 — Interactive Mode (Intermediate, ~1h30)
Goal: Add an “On holiday” toggle to the employee detail page.
Steps:
- Add
@rendermode InteractiveServeron the detail page - Add an
IsOnHolidayproperty to the employee - Create a button that toggles the state
- Persist the change via the service
- Verify the UI updates without a page reload
Exercise 4 — ErrorBoundary (Advanced, ~1h)
Goal: Implement robust error handling.
Steps:
- Add an employee with invalid data in seeding
- Make
EmployeeCardthrow an exception for that employee - Wrap each
EmployeeCardin an<ErrorBoundary> - Customize the displayed error message
12. Glossary
| Term | Definition |
|---|---|
| App Service | Azure service for hosting web applications |
| App Service Plan | Underlying infrastructure of an App Service (CPU, RAM, pricing) |
| Circuit | Persistent SignalR connection between a browser and a Blazor server |
| Code-behind | .razor.cs file containing logic separated from the Razor template |
| Component class | Partial C# class containing a Razor component’s logic |
| DbContext | EF Core session representing a database connection |
| DbContextFactory | Thread-safe DbContext factory for Blazor Server |
| DOM | Document Object Model — tree representation of an HTML page |
| ErrorBoundary | Blazor component isolating exceptions to prevent page crashes |
| EventCallback | Blazor type for asynchronous callbacks between components |
| Hot Reload | Hot code reloading without restarting |
| InMemory Provider | EF Core provider storing data in memory (for dev/tests) |
| Layout | Component defining the common structure for multiple pages |
| LTS | Long-Term Support — .NET version supported for 3 years |
| NavLink | Blazor component replacing <a> with active state management |
| NavigationManager | Blazor service for programmatic navigation |
| NuGet | .NET package manager |
| ORM | Object-Relational Mapper — EF Core translates C# ↔ SQL |
| Partial class | Class split into multiple files (Razor + code-behind) |
| Razor Component | .razor file combining HTML and C# |
| Razor syntax | Syntax for embedding C# in HTML with @ |
| RenderFragment | Type representing UI content passable as a parameter |
| Repository Pattern | Pattern encapsulating data access in dedicated classes |
| Resource Group | Azure logical container grouping related resources |
| Scoped service | Service whose lifetime matches an HTTP request |
| Seed data | Initial data inserted into the DB at application startup |
| SignalR | Microsoft technology for real-time communication (WebSocket) |
| Singleton | Service with a single instance throughout the app’s lifetime |
| SSR | Server-Side Rendering — HTML rendered on the server |
| Streaming Rendering | Technique sending the initial UI then updating with data |
| WASM | WebAssembly — binary format running in the browser |
| wwwroot | Public static files folder (CSS, JS, images) |
| @page | Razor directive defining the route for a navigable component |
| @rendermode | Razor directive specifying the render mode for a component |
| @bind | Razor directive for two-way data binding |
| @code | Razor block containing C# code in a component |
Additional Resources
- Recommended next course: “Building Web Applications with Blazor” — Gill Cleeren
- Official documentation: learn.microsoft.com/aspnet/core/blazor
- Blazor GitHub: github.com/dotnet/aspnetcore
- Azure App Service: learn.microsoft.com/azure/app-service
- EF Core InMemory: learn.microsoft.com/ef/core/providers/in-memory
- Bootstrap 5: getbootstrap.com — CSS framework used in the demo app
Quick Reference Table of Contents
- Why Blazor?
- Blazor Project Templates
- Blazor Web App Project Structure
- Components and Interactivity
- Navigation and Routing
- Entity Framework Core InMemory
- Azure App Service Deployment
- 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