Blazor WebAssembly Hosted App Tutorial: Envelope Tracker System
Blazor WebAssembly Hosted App Tutorial: Envelope Tracker System
Blazor WebAssembly applications can be built as standalone client-side apps or as part of a **hosted solution**. The hosted model provides a powerful and convenient way to develop full-stack applications, where your Blazor WebAssembly frontend is served by an ASP.NET Core backend that also provides your APIs. This tutorial will guide you through building a simple **Envelope Tracker System** using this hosted Blazor WebAssembly model.
✨ Use Case: Envelope Tracker System
Imagine a system to track the status of physical or digital document envelopes, such as invoices, agreements, or contracts, as they move through different stages (e.g., Pending, Sent, Received, Retried, Completed).
- **API (Server Project):** Will store and update envelope statuses in a database.
- **WASM Client (Client Project):** Will display the list of envelopes and allow users to perform actions like retrying a pending envelope or deleting an envelope.
- **Shared Library (Shared Project):** Will contain common data models (like the `Envelope` class) that are used by both the API and the UI, ensuring type safety and consistency.
✍️ Exercise 1: Architectural Understanding (Hosted Model)
📅 Goal:
Understand the hosted Blazor WebAssembly project structure, where the API, UI, and Shared models coexist within a single solution.
🔍 Key Concepts:
- **Blazor WebAssembly (WASM) Client:** This project (`.Client`) is the frontend of your application. It runs entirely in the user's browser, handling the UI and making API calls to the server using `HttpClient`.
- **ASP.NET Core Web API (Server):** This project (`.Server`) is the backend. It serves two primary roles:
- It acts as the **API endpoint** for your Blazor client, handling business logic and data persistence (e.g., with Entity Framework Core).
- It **hosts and serves the static files** of your Blazor WebAssembly client application to the browser.
- **Shared Library:** This project (`.Shared`) is a class library that contains common models, Data Transfer Objects (DTOs), and any shared logic that needs to be accessible by both the Server and Client projects. This prevents code duplication and ensures type consistency.
🏠 Architecture Overview:
When you create a hosted Blazor WebAssembly solution, Visual Studio (or the .NET CLI) sets up a solution with three interconnected projects:
📦 EnvelopeApp (Solution)
├── 📁 Client <-- Blazor WASM frontend (UI logic, API calls)
├── 📁 Server <-- ASP.NET Core API, EF Core, Hosts UI (Backend logic, Data access)
└── 📁 Shared <-- Common models, DTOs (Shared types)
✍️ Exercise 2: Creating Hosted Blazor WASM Solution with CRUD API
📅 Goal:
Use Visual Studio (or the .NET CLI) to create a Blazor WebAssembly Hosted app with an ASP.NET Core Web API that supports basic CRUD (Create, Read, Update, Delete) operations for `Envelope` entities.
🔧 Step-by-Step:
1. Create the Project
Open Visual Studio:
- Go to `File > New > Project`.
- Search for "Blazor WebAssembly App" and select it.
- Click "Next".
- Name the project `EnvelopeApp`.
- **Crucially, ensure you check the "ASP.NET Core Hosted" checkbox.**
- Set the Target Framework to `.NET 8` (or your preferred version).
- Click "Create".
2. Add Envelope Model in Shared Project
In the `EnvelopeApp.Shared` project, create a new folder named `Models` and add a new C# class file named `Envelope.cs`. This model will represent our data entity.
// EnvelopeApp.Shared/Models/Envelope.cs
namespace EnvelopeApp.Shared.Models;
public class Envelope
{
public int Id { get; set; } // Primary Key
public string EnvelopeId { get; set; } = string.Empty; // Unique identifier for the envelope
public string Status { get; set; } = "Pending"; // Current status (e.g., Pending, Retried, Completed)
public DateTime ReceivedAt { get; set; } = DateTime.UtcNow; // Timestamp of receipt
}
3. Setup EF Core in Server Project
In the `EnvelopeApp.Server` project, we'll configure Entity Framework Core to interact with a SQL Server database.
**a. Install NuGet packages:** Open the NuGet Package Manager Console (Tools > NuGet Package Manager > Package Manager Console) and ensure `EnvelopeApp.Server` is selected as the Default Project. Run:
Install-Package Microsoft.EntityFrameworkCore.SqlServer
Install-Package Microsoft.EntityFrameworkCore.Tools
**b. Create Data/ApplicationDbContext.cs:** Create a new folder named `Data` in the `EnvelopeApp.Server` project and add a new C# class file named `ApplicationDbContext.cs`.
// EnvelopeApp.Server/Data/ApplicationDbContext.cs
using EnvelopeApp.Shared.Models; // Reference to our shared model
using Microsoft.EntityFrameworkCore;
namespace EnvelopeApp.Server.Data;
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }
// DbSet for our Envelopes table
public DbSet<Envelope> Envelopes => Set<Envelope>();
}
**c. Configure DB in Program.cs:** Open `EnvelopeApp.Server/Program.cs` and add the `DbContext` to the services collection and configure the connection string.
// EnvelopeApp.Server/Program.cs
using EnvelopeApp.Server.Data; // For ApplicationDbContext
using Microsoft.EntityFrameworkCore; // For UseSqlServer
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
// Configure SQL Server DbContext
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
var app = builder.Build();
// ... (rest of the Program.cs code)
**d. Add appsettings.json connection string:** Open `EnvelopeApp.Server/appsettings.json` and add your database connection string. Make sure your SQL Server instance is running.
// EnvelopeApp.Server/appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "Server=.;Database=EnvelopeDb;Trusted_Connection=True;TrustServerCertificate=True;"
}
}
**Note:** `Server=.` means local SQL Server Express. `Trusted_Connection=True` uses Windows Authentication. `TrustServerCertificate=True` is for local development with self-signed certificates.
4. Add API Controller in Server
In the `EnvelopeApp.Server` project, create a new folder named `Controllers` and add a new C# class file named `EnvelopeController.cs`. This controller will expose our CRUD operations.
// EnvelopeApp.Server/Controllers/EnvelopeController.cs
using EnvelopeApp.Server.Data; // For ApplicationDbContext
using EnvelopeApp.Shared.Models; // For Envelope model
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; // For ToListAsync, FindAsync
namespace EnvelopeApp.Server.Controllers;
[ApiController]
[Route("api/[controller]")] // Base route: /api/envelope
public class EnvelopeController : ControllerBase
{
private readonly ApplicationDbContext _context;
public EnvelopeController(ApplicationDbContext context)
{
_context = context;
}
// GET: api/envelope
[HttpGet]
public async Task<IEnumerable<Envelope>> GetAll()
{
return await _context.Envelopes.ToListAsync();
}
// POST: api/envelope
[HttpPost]
public async Task<IActionResult> Create(Envelope env)
{
// Generate a simple EnvelopeId for demo purposes
env.EnvelopeId = Guid.NewGuid().ToString().Substring(0, 8).ToUpper();
env.ReceivedAt = DateTime.UtcNow; // Ensure consistent timestamp
_context.Envelopes.Add(env);
await _context.SaveChangesAsync();
// Return 201 Created status with the newly created envelope
return CreatedAtAction(nameof(GetAll), env);
}
// DELETE: api/envelope/{id}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(int id)
{
var item = await _context.Envelopes.FindAsync(id);
if (item is null) return NotFound(); // Return 404 if not found
_context.Envelopes.Remove(item);
await _context.SaveChangesAsync();
return NoContent(); // Return 204 No Content for successful deletion
}
// POST: api/envelope/retry/{id}
[HttpPost("retry/{id}")] // Custom route for retry action
public async Task<IActionResult> Retry(int id)
{
var env = await _context.Envelopes.FindAsync(id);
if (env is null) return NotFound();
env.Status = "Retried"; // Update status
await _context.SaveChangesAsync();
return Ok(env); // Return 200 OK with the updated envelope
}
}
5. Run Migrations
Open the NuGet Package Manager Console, ensure `EnvelopeApp.Server` is selected as the Default Project, and run the following commands to create your database and table:
Add-Migration Init
Update-Database
✅ Your API is now set up and ready to be consumed by the Blazor client! It will be served at routes like `/api/envelope`.
✍️ Exercise 3: Developing the Blazor WASM Frontend
📅 Goal:
Develop the Blazor WebAssembly Client project to consume the Server API and display a simple CRUD dashboard for managing envelopes.
🔧 Step-by-Step:
1. Register HttpClient in Client/Program.cs
Open `EnvelopeApp.Client/Program.cs` and ensure `HttpClient` is registered with the base address pointing to your server. This is usually set up by default in hosted Blazor WASM projects, but it's good to verify.
// EnvelopeApp.Client/Program.cs
using System.Net.Http;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection; // For AddScoped
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
// This line registers HttpClient for API calls
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
await builder.Build().RunAsync();
2. Create EnvelopeService in Client/Services/EnvelopeService.cs
Create a new folder named `Services` in the `EnvelopeApp.Client` project and add a new C# class file named `EnvelopeService.cs`. This service will encapsulate all our API calls, making our UI components cleaner.
// EnvelopeApp.Client/Services/EnvelopeService.cs
using EnvelopeApp.Shared.Models; // Reference to our shared model
using System.Net.Http;
using System.Net.Http.Json; // For GetFromJsonAsync, PostAsJsonAsync
namespace EnvelopeApp.Client.Services; // Ensure correct namespace
public class EnvelopeService
{
private readonly HttpClient _http;
public EnvelopeService(HttpClient http)
{
_http = http;
}
// Get all envelopes
public async Task<List<Envelope>> GetAll()
{
return await _http.GetFromJsonAsync<List<Envelope>>("api/envelope") ?? new();
}
// Create a new envelope
public async Task Create(Envelope env)
{
await _http.PostAsJsonAsync("api/envelope", env);
}
// Delete an envelope by ID
public async Task Delete(int id)
{
await _http.DeleteAsync($"api/envelope/{id}");
}
// Retry an envelope by ID (custom action)
public async Task Retry(int id)
{
// For POST with no body, pass null for content
await _http.PostAsync($"api/envelope/retry/{id}", null);
}
}
**Register the service:** Open `EnvelopeApp.Client/Program.cs` and register your `EnvelopeService` for dependency injection.
// EnvelopeApp.Client/Program.cs (continued)
// ...
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
// Register our custom EnvelopeService
builder.Services.AddScoped<EnvelopeService>();
await builder.Build().RunAsync();
3. Create UI Page Pages/Dashboard.razor
Create a new Razor page file named `Dashboard.razor` in your `EnvelopeApp.Client/Pages` folder. This page will display the list of envelopes and provide buttons for actions.
<!-- EnvelopeApp.Client/Pages/Dashboard.razor -->
@page "/dashboard"
@inject EnvelopeApp.Client.Services.EnvelopeService EnvelopeService <!-- Inject our service -->
@using EnvelopeApp.Shared.Models <!-- Use our shared model -->
<h3 class="heading3">Envelope Dashboard</h3>
<!-- Button to add a new dummy envelope -->
<button class="btn btn-primary mb-3" @onclick="CreateNewEnvelope">Add New Envelope</button>
@if (envelopes == null)
{
<p>Loading envelopes...</p>
}
else if (!envelopes.Any())
{
<p>No envelopes found. Add one to get started!</p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Envelope ID</th>
<th>Status</th>
<th>Received At</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach (var e in envelopes)
{
<tr>
<td>@e.EnvelopeId</td>
<td>@e.Status</td>
<td>@e.ReceivedAt.ToLocalTime().ToString("g")</td>
<td>
<button class="btn btn-sm btn-primary" @onclick="() => Retry(e.Id)" disabled="@(e.Status == "Retried")">Retry</button>
<button class="btn btn-sm btn-danger" @onclick="() => Delete(e.Id)">Delete</button>
</td>
</tr>
}
</tbody>
</table>
}
@code {
List<Envelope>? envelopes; // Nullable list to indicate loading state
// Called when the component is initialized
protected override async Task OnInitializedAsync()
{
await LoadEnvelopes();
}
// Helper method to load envelopes from the service
private async Task LoadEnvelopes()
{
envelopes = await EnvelopeService.GetAll();
StateHasChanged(); // Force re-render after data is loaded
}
// Action to create a new dummy envelope
async Task CreateNewEnvelope()
{
var newEnvelope = new Envelope { Status = "Pending" }; // ID and EnvelopeId will be set by API
await EnvelopeService.Create(newEnvelope);
await LoadEnvelopes(); // Reload list after creation
}
// Action to retry an envelope
async Task Retry(int id)
{
await EnvelopeService.Retry(id);
await LoadEnvelopes(); // Reload list after update
}
// Action to delete an envelope
async Task Delete(int id)
{
// In a real app, you might add a confirmation dialog here
await EnvelopeService.Delete(id);
await LoadEnvelopes(); // Reload list after deletion
}
}
**Important:** To make the Dashboard page accessible from the navigation menu, open `EnvelopeApp.Client/Shared/NavMenu.razor` and add a `NavLink` for `/dashboard`:
<!-- EnvelopeApp.Client/Shared/NavMenu.razor -->
<div class="nav-item px-3">
<NavLink class="nav-link" href="dashboard">
<span class="oi oi-list-rich" aria-hidden="true"></span> Envelopes
</NavLink>
</div>
✅ Done!
You've successfully built a Blazor WebAssembly Hosted App:
- The **Server API** and **Client UI** are managed within one solution.
- A **Shared model** ensures consistency between frontend and backend.
- The system allows you to **track document envelopes**, perform CRUD operations, and manage their statuses.
To run the application, simply start the `EnvelopeApp.Server` project (e.g., by pressing F5 in Visual Studio). The server will host the Blazor WebAssembly client, and you can navigate to `/dashboard` to see your Envelope Tracker in action!
🎯 Learning Highlights for Students:
- **Hosted Blazor WebAssembly Architecture:** Practical understanding of how Client, Server, and Shared projects work together.
- **Full-Stack Development:** Experience integrating frontend (Blazor WASM) with backend (ASP.NET Core Web API + EF Core).
- **Data Modeling:** Defining shared models for consistent data structures across layers.
- **RESTful API Consumption:** Using `HttpClient` and `HttpClientJsonExtensions` to perform GET, POST, DELETE, and custom actions.
- **UI Development with Blazor:** Displaying dynamic data in tables and handling user interactions with buttons.
- **Dependency Injection:** Utilizing DI for `HttpClient` and `EnvelopeService` for cleaner, more maintainable code.
Comments
Post a Comment