Scaffolding the Project with the .NET CLI
To create a new hosted Blazor WebAssembly project using the command line, open a terminal and navigate to your desired output directory. Execute the following command, which specifies the blazorwasm template, includes the --hosted flag to generate an accompanying ASP.NET Core server project, and outputs everything into a folder named BlazorStarterApp.
dotnet new blazorwasm --hosted -o BlazorStarterAppAfter the NuGet packages are restored, navigate into the newly created directory and compile the solution:
cd BlazorStarterApp
dotnet buildTo launch the application, move into the Server project directory and run it:
cd BlazorStarterApp/Server
dotnet runThe console output will display the local hosting URLs:
info: Microsoft.Hosting.Lifetime[14]
Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5000Open your browser and navigate to the HTTPS endpoint to view the running application.
Creating the Solution in Visual Studio
Launch Visual Studio and select Create a new project. Search for Blazor and choose the Blazor WebAssembly App template. Click Next, name the project BlazorStarterApp, and choose a location. On the subsequent screen, pick the latest available .NET framework, set Authentication to None, check the ASP.NET Core hosted option, and click Create. Once the scaffolding completes, press F5 to build and launch the application in your default browser.
Running via Visual Studio Code
With the project generated via the CLI, open the root folder in VS Code. You can achieve this by running code . from the terminal inside the project directory, or by using File > Open Folder. Upon loading, VS Code may prompt to add required build and debug assets. Accepting this generates a .vscode folder containing the necessary launch configurations. You can then start the app by pressing F5.
Exploring the Default Application
When launched, the single-page application displays a sidebar navigation menu and a main content area. The default Home page renders an Index component featuring a greeting and a survey prompt. Clicking Counter in the menu navigates to a component with an interactive button that increments a numeric value. The Fetch data link shows a dynamically generated weather forecast table, demonstrating how the client retrieves data from the server API.
Anatomy of the Solution
A hosted Blazor WebAssembly solution consists of three distinct projects: Server, Client, and Shared. This structure allows the tooling to compile dependencies in the correct order.
The Server Project
The server is responsible for serving files to the browser upon request. The built-in ASP.NET Core web server, Kestrel, handles this. The request processing pipeline is configured in Program.cs using middleware components.
var webApp = builder.Build();
if (webApp.Environment.IsDevelopment())
{
webApp.UseDeveloperExceptionPage();
webApp.UseWebAssemblyDebugging();
}
else
{
webApp.UseExceptionHandler("/Error");
webApp.UseHsts();
}
webApp.UseHttpsRedirection();
webApp.UseBlazorFrameworkFiles();
webApp.UseStaticFiles();
webApp.UseRouting();
webApp.MapRazorPages();
webApp.MapControllers();
webApp.MapFallbackToFile("index.html");Middleware executes sequentially. During development, detailed error pages and WebAssembly debugging tools are enabled. In production, exceptions are redirected to an error page, and HSTS is enforced. The ASPNETCORE_ENVIRONMENT variable determines the current environment, typically set to Development inside Properties/launchSettings.json. MapFallbackToFile ensures that unrecognized URLs fallback to index.html, enabling client-side routing.
The Shared Project
Because both the frontend and backend run on C#, data models can exist in a single location. For instance, the weather data model is defined once in the Shared project and utilized by both the Client and Server.
namespace BlazorStarterApp.Shared;
public class ClimateObservation
{
public DateTime RecordDate { get; set; }
public int Celsius { get; set; }
public string? Condition { get; set; }
public int Fahrenheit => 32 + (int)(Celsius / 0.5556);
}The Client Project
The client project represents the actual Blazor application running in the browser. Inside wwwroot/index.html, a specific div acts as the mounting point:
<div id="wasm-root">Loading...</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>The Blazor runtime is loaded via a script tag: <script src="_framework/blazor.webassembly.js"></script>.
In Program.cs, the root component is mapped to this HTML element using a CSS selector:
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using BlazorStarterApp.Client;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#wasm-root");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
await builder.Build().RunAsync();The App.razor component establishes the Router, which handles URL mapping and renders the appropriate page component inside a default layout.
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>Layout Components
Layouts provide shared UI structures, like navigation menus. The MainLayout.razor file inherits from LayoutComponentBase and defines a sidebar and a main content area where the active page is rendered via the @Body directive.
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
<a href="http://blazor.net" target="_blank" class="ml-md-auto">About</a>
</div>
<article class="content px-4">
@Body
</article>
</main>
</div>Debugging Blazor WebAssembly
Debugging WebAssembly applications is supported in both Visual Studio and VS Code, though it is currently limited to Chromium-based browsers. The debugger relies on a proxy configured in launchSettings.json under the inspectUri property.
{
"profiles": {
"BlazorStarterApp.Server": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}After launching with the debugger attached (F5), you can set breakpoints in your C# code, such as inside the AddToTotal method of the Counter component. When the button is clicked in the UI, execution will pause, allowing you to inspect local variables.
Leveraging Hot Reload
.NET 6 introduced Hot Reload, allowing code and markup modifications to be applied to a running application without losing state. To use this via the CLI, run:
dotnet watchWith the application running, modify the Counter component. For instance, change the heading:
<h1>Interactive Counter</h1>Or update the increment logic:
private void AddToTotal()
{
currentValue += 5;
}Upon saving the file, the browser will reflect the changes instantly while retaining the current count value.
Blazor WebAssembly Bootstrapping Sequence
When a user initially visits a Blazor WebAssembly site, the browser fetches index.html, followed by CSS and the blazor.webassembly.js script. This script downloads blazor.boot.js and subsequently dotnet.wasm, which is the .NET runtime compiled for WebAssembly. Once the runtime initializes, the application DLLs (like BlazorStarterApp.Client.dll) and standard .NET libraries are downloaded. The initial payload can be substantial (often around 10MB), but subsequent visits utilize browser caching to drastically reduce download times.
Blazor Server Bootstrapping Sequence
Blazor Server operates differently. Create one using the command:
dotnet new blazorserver -o BlazorServerAppWhen running a Blazor Server app, the initial download size is significantly smaller because the UI logic executes on the server. Instead of downloading the .NET runtime and application DLLs, the browser establishes a SignalR WebSocket connection. UI events are sent to the server over this socket, and the server sends back the minimal DOM diffs required to update the interface.
Nullable Reference Types
C# allows developers to opt into nullable reference types to help prevent NullReferenceException errors. When enabled, the compiler warns if a reference type is assigned a null value without explicit indication. Value types can be made nullable using the ? suffix (e.g., int?), and the same syntax now applies to reference types.
string? nullableString = null; // Valid
string nonNullableString = null; // Compiler warningThis feature is activated in the project file:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>Handling Nullable Properties
Consider a class with reference type properties:
public class Employee
{
public string GivenName { get; set; }
public string FamilyName { get; set; }
}Without constructor enforcement, the compiler warns that the properties could be null. You can resolve this by introducing a constructor that guarantees assignment:
public class Employee
{
public Employee(string givenName, string familyName)
{
GivenName = givenName;
FamilyName = familyName;
}
public string GivenName { get; set; }
public string FamilyName { get; set; }
}Alternatively, if the class requires a parameterless constructor (e.g., for ORM frameworks), you can declare the properties as explicitly nullable:
public class Employee
{
public string? GivenName { get; set; }
public string? FamilyName { get; set; }
}The Null-Forgiving Operator
Sometimes you know a property will be populated at runtime, even if it is initially null. The null-forgiving operator (!) suppresses compiler warnings for that assignment. This is commonly used to initialize properties that frameworks will later populate:
public class Employee
{
public string? GivenName { get; set; } = null!;
public string? FamilyName { get; set; } = null!;
}For string properties, assigning string.Empty is another valid approach to satisfy the compiler:
public class Employee
{
public string? GivenName { get; set; } = string.Empty;
public string? FamilyName { get; set; } = string.Empty;
}