Welcome to today’s post.
In today’s post I will be showing you how to include ASP.NET Core Web API controllers within a hosted Blazor WebAssembly application without having to separate the Blazor WebAssembly and Web API into separately hosted applications.
The feature of a hosted Blazor WebAssembly application has been available since .NET Core 5.0 to .NET Core 7.0. In the previous post I showed how to create a hosted Blazor WebAssembly application using the empty Visual Studio template. I then showed how to include simple services into the Server project and implement Razor components in the Client project to interact with the services within the Server project.
We will first look at the project structure of a hosted Blazor WebAssembly in the first section.
The Current Blazor Hosted WebAssembly Project Structure
The hosted option for Blazor WebAssembly applications scaffolds the following projects within the generated solution:
Client
Server
Shared
The Client project includes the Razor components and scripts that are hosted and rendered within the client browser.
The Server project includes any server-side resources, such as data services, Web APIs, and controllers that are hosted under ASP.NET Core. The components within the client project are hosted by ASP.NET Core within the server project.
The Shared project includes common classes such as Models and common utilities that can be used by both the server and client projects.
As I mentioned, without the hosted option, the only way to call Web API services from a WebAssembly would be to create standalone Blazor WebAssembly and ASP.NET Core Web API projects and then use the HttpClient class within System.Http.Net library to make HTTP REST calls to the separately running Web API.
To be able to make calls to Web APIs hosted in the same URL space from a client hosted within the same URL space within the same hosted Blazor WebAssembly application requires you to first add API controllers within the Server project. I will show how this is done in the next section.
Adding Controllers to an Existing Hosted Blazor WebAssembly Solution
Before you can reference shared classes and services from the Shared and Server projects, you will need to add their project references to the Client project through the reference manager in the Solution context menu.

Your controllers that may call service classes would be using and returning model classes. These can be declared within the Shared project.
The \BookLoanBlazorHostedWASMApp\Shared folder
Create a Models sub-folder and move the model class source files under the sub-folder.
BookViewModel.cs
BookEditModel.cs
BookCreateModel.cs
The \BookLoanBlazorHostedWASMApp\Shared\Models folder
Ensure the namespace for the solution is updated within each model class file. For example:
namespace BookLoanBlazorHostedWASMApp.Models
Setting up Services in the Server Project
In the Server project, if your controllers call existing service classes, they can go into a separate Service folder.
\BookLoanBlazorHostedWASMApp\Server\Services
I then move a service class and its interface into the above project sub-folder folder. They are shown below.
IBookService.cs
using System.Collections.Generic;
using BookLoanBlazorHostedWASMApp.Models;
namespace BookLoanBlazorHostedWASMApp.Services
{
public interface IBookService
{
Task<List<BookViewModel>> GetBooks();
Task<List<GenreViewModel>> GetGenres();
Task<List<BookViewModel>> GetBooksFilter(string filter);
Task<BookViewModel> GetBook(int id);
Task<int> SaveBook(BookViewModel vm);
Task<BookViewModel> UpdateBook(int id, BookViewModel vm);
Task<bool> DeleteBook(int id);
}
}
BookService.cs
using Microsoft.EntityFrameworkCore;
using BookLoanBlazorHostedWASMApp.Data;
using BookLoanBlazorHostedWASMApp.Models;
namespace BookLoanBlazorHostedWASMApp.Services
{
public class BookService : IBookService
{
readonly IDbContextFactory<ApplicationDbContext> _dbFactory;
private readonly ILogger _logger;
public BookService(
IDbContextFactory<ApplicationDbContext> dbContextFactory,
ILogger<BookService> logger)
{
_dbFactory = dbContextFactory;
_logger = logger;
}
public async Task<List<BookViewModel>> GetBooks()
{
using var context = _dbFactory.CreateDbContext();
return await context.Books.ToListAsync();
}
public async Task<List<BookViewModel>> GetBooksFilter(string filter)
{
using var context = _dbFactory.CreateDbContext();
List<BookViewModel> bookList = new List<BookViewModel>();
var books = await context.Books
.Where(b => b.Title!.Contains(filter)).ToListAsync();
return books;
}
public async Task<BookViewModel> GetBook(int id)
{
using var context = _dbFactory.CreateDbContext();
var book = await context.Books.Where(b => b.ID == id).SingleOrDefaultAsync();
return book!;
}
public async Task<int> SaveBook(BookViewModel vm)
{
try
{
using var context = _dbFactory.CreateDbContext();
var newBook = await context.Books.AddAsync(vm);
await context.SaveChangesAsync();
var newId = newBook.CurrentValues.GetValue<int>("ID");
return newId;
}
catch
{
return 0;
}
}
public async Task<BookViewModel> UpdateBook(int Id, BookViewModel vm)
{
using var context = _dbFactory.CreateDbContext();
var book = await context.Books
.Where(b => b.ID == Id)
.SingleOrDefaultAsync();
if (book != null)
{
string originalEdition = book.Edition!;
book.Title = vm.Title;
book.Author = vm.Author;
book.Edition = vm.Edition;
book.Genre = vm.Genre;
book.ISBN = vm.ISBN;
book.Location = vm.Location;
book.YearPublished = vm.YearPublished;
context.Update(book);
await context.SaveChangesAsync();
}
return book!;
}
public async Task<bool> DeleteBook(int id)
{
using var context = _dbFactory.CreateDbContext();
var book = await context.Books
.Where(b => b.ID == id)
.SingleOrDefaultAsync();
if ( book != null ) {
context.Books.Remove(book!);
await context.SaveChangesAsync();
}
return (book != null);
}
public async Task<List<GenreViewModel>> GetGenres()
{
using var context = _dbFactory.CreateDbContext();
return await context.Genres.ToListAsync();
}
}
}
The above services can then be injected into your controller and used from there. Alternatively, you can call the data provider directly from your controller, which is what I will show in the next sub-section. The choice and flexibility depend on the architecture and code organization you plan to follow.
Setting up Controllers in the Server Project
After we have added the services, we can then implement and add the controllers to the Server project.
What I have done is to implement a basic Web API controller that receives and handles HTTP REST requests.
First create a new Controllers sub-folder:
\BookLoanBlazorHostedWASMApp\Server\Controllers
Then implement (or move an existing) controller into the folder. One is shown below with the data context injected and used directly from within the controller methods:
BookLoanController.cs
using BookLoanBlazorHostedWASMApp.Models;
using BookLoanBlazorHostedWASMApp.Data;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace BookLoanBlazorHostedWASMApp.Server.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class BookLoanController : ControllerBase
{
readonly IDbContextFactory<ApplicationDbContext> _dbFactory;
private readonly ILogger<BookLoanController> _logger;
public BookLoanController(
IDbContextFactory<ApplicationDbContext> dbContextFactory,
ILogger<BookLoanController> logger)
{
this._dbFactory = dbContextFactory;
this._logger = logger;
}
[HttpGet]
public async Task<IActionResult> GetBooks()
{
using var context = _dbFactory.CreateDbContext();
var books = await context.Books.ToListAsync();
if (books is null)
return NotFound();
return Ok(books);
}
[HttpGet("api/[controller]/bookitems/{id}")]
public async Task<IActionResult> BookItem(int id)
{
using var context = _dbFactory.CreateDbContext();
var book = await context.Books.FindAsync(id);
if (book is null)
return NotFound();
if (book is BookViewModel)
return Ok(book);
return NotFound();
}
[HttpPost]
public async Task<IActionResult> CreateBook(BookViewModel book)
{
using var context = _dbFactory.CreateDbContext();
context.Books.Add(book);
await context.SaveChangesAsync();
return Created($"/bookitems/{book.ID}", book);
}
[HttpPut]
public async Task<IActionResult> UpdateBook(int id, BookViewModel inBook)
{
using var context = _dbFactory.CreateDbContext();
var book = await context.Books.FindAsync(id);
if (book is null)
return NotFound();
book.ISBN = inBook.ISBN;
book.Author = inBook.Author;
book.Edition = inBook.Edition;
book.Genre = inBook.Genre;
book.Location = inBook.Location;
book.MediaType = inBook.MediaType;
book.Title = inBook.Title;
book.YearPublished = inBook.YearPublished;
book.DateUpdated = DateTime.Now;
await context.SaveChangesAsync();
return NoContent();
}
[HttpDelete]
public async Task<IActionResult> DeleteBook(int id)
{
using var context = _dbFactory.CreateDbContext();
var book = await context.Books.FindAsync(id);
if (book is null)
return NotFound();
if (book is BookViewModel)
{
context.Books.Remove(book);
await context.SaveChangesAsync();
return Ok(book);
}
return NotFound();
}
}
}
Notice that I have ensured that references back to the namespaces for the model classes (in the Shared project) and the data provider (in the Server project) are added as namespaces at the top of the controller source:
using BookLoanBlazorHostedWASMApp.Models;
using BookLoanBlazorHostedWASMApp.Data;
To associate the above instances to instances we use dependency injection within the program startup as shown:
Program.cs
using Microsoft.AspNetCore.ResponseCompression;
using BookLoanBlazorHostedWASMApp.Data;
using BookLoanBlazorHostedWASMApp.Services;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
string connStr = "AppDbContext";
try
{
builder.Services.AddDbContextFactory<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString(connStr))
);
}
catch
{
Console.WriteLine("Error: Cannot connect to database.");
}
builder.Services.AddTransient<IBookService, BookService>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseWebAssemblyDebugging();
}
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
app.UseRouting();
app.MapRazorPages();
app.MapControllers();
app.MapFallbackToFile("index.html");
app.Run();
To enable controllers within the Server project I added the ControllersWithViews services to the service collection:
builder.Services.AddControllersWithViews();
I then mapped the controllers to the request endpoints in the HTTP pipeline using:
app.MapControllers();
In the next section I will show how to tie up the above API controller to the client.
Integrating API Controllers Calls into the Client Project
In this section, I will show how to configure and add an example Razor component to test out connectivity and return results from our API controller from the Server project.
What I will need to do first in the startup is to add the HttpClient service with named client connections to the service collection with the base address set to the address of the WebAssenbly application:
builder.Services.AddHttpClient("BookLoanBlazorHostedWASMApp.ServerAPI",
client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress));
Then add the HttpClientFactory service which allows me to create additional named http client connections for calls to the web API. The created client instances are set with a scoped application lifetime:
builder.Services.AddScoped(sp =>
sp.GetRequiredService<IHttpClientFactory>().CreateClient(
"BookLoanBlazorHostedWASMApp.ServerAPI")
);
The startup is shown below:
Program.cs
using BookLoanBlazorHostedWASMApp.Client;
using BookLoanBlazorHostedWASMApp.Data;
using BookLoanBlazorHostedWASMApp.Services;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddHttpClient("BookLoanBlazorHostedWASMApp.ServerAPI",
client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
);
// Supply HttpClient instances that include access tokens when making requests to the server project
builder.Services.AddScoped(sp =>
sp.GetRequiredService<IHttpClientFactory>().CreateClient(
"BookLoanBlazorHostedWASMApp.ServerAPI")
);
builder.Services.AddSingleton<WeatherForecastService>();
builder.Services.AddTransient<IBookService, BookService>();
await builder.Build().RunAsync();
I then implement a client service class to wrap the HTTP REST calls to the Web API controller methods. The interface is re-used by including the Models namespace within the Shared project. The source is in the sub-folder:
\BookLoanBlazorHostedWASMApp\Client\Services
Below is the client Book service:
using BookLoanBlazorHostedWASMApp.Models;
using System.Net.Http.Json;
namespace BookLoanBlazorHostedWASMApp.Services
{
public class BookService : IBookService
{
private readonly ILogger _logger;
private readonly IHttpClientFactory _clientFactory;
private readonly string _apiUri = "http://localhost:5188/api/BookLoan";
public BookService(
ILogger<BookService> logger, IHttpClientFactory clientFactory)
{
_logger = logger;
_clientFactory = clientFactory;
}
public async Task<List<BookViewModel>> GetBooks()
{
var client = _clientFactory.CreateClient("BookLoanBlazorHostedWASMApp.ServerAPI");
var books = await client.GetFromJsonAsync<List<BookViewModel>>(_apiUri);
return books!.ToList();
}
public async Task<BookViewModel> GetBook(int id)
{
var client = _clientFactory.CreateClient("BookLoanBlazorHostedWASMApp.ServerAPI");
var book = await client.GetFromJsonAsync<BookViewModel>($"{_apiUri}/{id}");
return book!;
}
public async Task<int> SaveBook(BookViewModel vm)
{
try
{
var client = _clientFactory.CreateClient("BookLoanBlazorHostedWASMApp.ServerAPI");
var response = await client.PostAsJsonAsync(_apiUri, vm);
var content = response.Content;
return 1;
}
catch
{
return 0;
}
}
public async Task<int> UpdateBook(int Id, BookViewModel vm)
{
try
{
var client = _clientFactory.CreateClient("BookLoanBlazorHostedWASMApp.ServerAPI");
var response = await client.PutAsJsonAsync($"{_apiUri}/{Id}", vm);
var content = response.Content;
return 1;
}
catch
{
return 0;
}
}
public async Task<bool> DeleteBook(int Id)
{
try
{
var client = _clientFactory.CreateClient("BookLoanBlazorHostedWASMApp.ServerAPI");
await client.DeleteAsync($"bookitems/{Id}");
return true;
}
catch
{
return false;
}
}
}
}
The navigation menu is updated in the same way I did for any WebAssembly client. After adding that and building and running the application, you should see the landing page and menu look like this:

I then implement a Razor component that will call the Web API to return data from the Web API to list the books and render and display them.
The page to list the books in in the sub-folder:
\BookLoanBlazorHostedWASMApp\Client\Pages The page source is shown below:
@page "/listbooks"
@using BookLoanBlazorHostedWASMApp.Services
@using BookLoanBlazorHostedWASMApp.Models
@inject IBookService LibraryBookService
<PageTitle>list books</PageTitle>
<h3>List Books</h3>
@if (books == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Title</th>
<th>Author</th>
<th>ISBN</th>
<th>Year Published</th>
<th>Genre</th>
<th colspan="2">Action</th>
</tr>
</thead>
<tbody>
@foreach (var book in books)
{
<tr>
<td>@book.Title</td>
<td>@book.Author</td>
<td>@book.ISBN</td>
<td>@book.YearPublished</td>
<td>@book.Genre</td>
<td>
<a href="editbook/?id=@book.ID">Edit</a>
</td>
<td>
<a href="viewbook/?id=@book.ID">View</a>
</td>
</tr>
}
</tbody>
</table>
}
@code {
private List<BookViewModel>? books = new List<BookViewModel>();
protected override async Task OnInitializedAsync()
{
List<BookViewModel>? tempList = await LibraryBookService.GetBooks();
foreach (var book in tempList)
books!.Add(book);
}
}
After the Razor component is added, you will then be able to run the app and re-test the HTTP calls to the API Controller we have in the Server project.
When the application is re-built and run, if successful you should see the following outcome after clicking on the List Books menu item:

To be able to re-test the above HTTP GET call, you can also enter the following URL (or similar, depending on the port your application is running on) into the browser navigation bar, and you will get a screenful of JSON:
http://localhost:5188/api/BookLoan/
The above overview has shown you how to implement and configure a basic API controller into a Server project in a hosted Blazor WebAssembly solution and make HTTP REST calls to the API Controller from within a Razor component within the Client project.
The above architecture is valid for the Blazor templates for .NET Core versions from 5.0 to 7.0. From .NET 9.0 onwards, the hosted option is not available in the Visual Studio 2022 project template, however there are ways to get around this and re-configure a .NET 8.0 Blazor WebAssembly solution to achieve the same architectural outcome. I will explain this in a future post.
That is all for today’s post.
I hope that you have found this post useful and informative.
Andrew Halil is a blogger, author and software developer with expertise of many areas in the information technology industry including full-stack web and native cloud based development, test driven development and Devops.