Exploring Blazor Changes in .NET 8 - Interactive Components using Blazor WASM

August 9, 2023 · 8 minute read · Tags: blazor

This is the latest in a number of posts where we’ll explore the changes coming for Blazor in .NET 8 (due later this year - 2023).

Blazor Server-Side Rendering makes it possible to use Razor components which are rendered on the server (with plain old HTML sent back to the browser).

But there’s a catch… the component is rendered once, and once only.

You can max out your component with as many buttons, sliders, and toggles as you like. No matter how many times you interact with them, nothing is going to happen.

Your component isn’t actually running anywhere. It was rendered once, and the resulting HTML was sent to the browser.

But all is not lost! You can choose to make certain components interactive, even if you’re using SSR for the majority of your site.

In the last article we saw how to do this using Blazor Server, now let’s figure out where Blazor WASM fits in to the picture.

It’s complicated…

As we dive in here we should probably acknowledge the added complexity involved in making this work.

It was pretty straightforward to make a component run interactively using Blazor Server (with minimal changes needed to the component).

<RelatedProducts Id="Id" @rendermode="@RenderMode.Server" />

Achieving the same with Blazor WASM is quite a bit more involved.

This is fundamentally because we’re talking about taking part of your app and shipping it to the browser.

Once there your components have no direct access to your backend services, so they have to make calls via an API.

This requires a bit more work and thought around the architecture needed to make everything work.

As of the current preview release you need to put your component in a separate client project for it to work with Blazor WASM.

We can’t use the existing “main” project because it includes server-side logic (including calls to backend databases) which we can’t (and shouldn’t) ship to the browser.

So the first step is to:

  • Create a new empty Blazor WASM project
  • Reference it from the existing (Server) project

Here is a visualisation of our online store demo app, and the RelatedProducts component we want to run using WASM.

Alt text

We need to move the RelatedProducts component we created in the last post to this new Client project.

But what about accessing backend services?

When components are rendered on the server (either via SSR or Blazor Server) they can interact with business logic and/or access databases etc. directly.

But when there’s a chance they might render in the in the browser, via WASM (as with RelatedProducts here) they’re going to need to go via an API instead (which can then delegate to the relevant backend service).

So first things first, let’s adapt our example from the last post, and expose some API endpoints (in the server project):

[ApiController]
[Route("/api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly ProductStore _store;

    public ProductsController(ProductStore store)
    {
        _store = store;
    }

    [HttpGet("related")]
    public async Task<IActionResult> Related([FromQuery]int productId, [FromQuery] int pageNumber)
    {
        return Ok(await _store.ListRelated(productId, pageNumber));
    }
}

This is fairly standard API code which then delegates to the IProductStore (which we were able to use directly when rendering on the server).

Now we can make our RelatedProducts component use HttpClient to make a call to that endpoint.

RelatedProducts.razor

@using BlazorDemoApp.Shared.Data
@inject HttpClient Http

...
@code {

    private RelatedProductsList? related;

    int currentPage = 0;

    [Parameter]
    public int Id { get; set; }

    ...

    private async Task LoadRelatedProducts()
    {
        var uri = $"/api/products/related?productId={Id}&pageNumber={currentPage}";
        related = await Http.GetFromJsonAsync<RelatedProductsList>(uri);
    }

}

Finally we can render this RelatedProducts component in the app:

Details.razor

<RelatedProducts Id="Id" @rendermode="@RenderMode.WebAssembly" />

So long as we’ve got an HttpClient configured in Program.cs for the client project, you might expect this would work.

But, if we try to navigate directly to the product details page we get a mysterious error…

An unhandled exception occurred while processing the request.

InvalidOperationException: Cannot provide a value for property ‘Http’ on type ‘BlazorDemoApp.Client.RelatedProducts’. There is no registered service of type ‘System.Net.Http.HttpClient’.

So what gives?

Watch out for pre-rendering

Remember how we’re rendering this component:

<RelatedProducts Id="Id" @rendermode="@RenderMode.WebAssembly" />

By default, components rendered this way will be prerendered.

So RelatedProducts will actually be rendered twice: first on the server (so the initial HTML can be sent to the browser along with the rest of the page), and then again, in the browser, when the component is loaded via WASM.

The problem is, when that first (pre)render happens on the server, ASP.NET is attempting to inject an instance of HttpClient but it can’t, because we didn’t register that service in the server project.

The client project has it, but the server project doesn’t.

To fix this, we have a couple of options…

Register HttpClient on the server

One option is to directly address this by registering HttpClient in the server project’s Program.cs file:

Program.cs

builder.Services.AddScoped(sp => new HttpClient 
{ 
    BaseAddress = new Uri("http://localhost:5023/") 
});

For ease I’ve hardcoded the Base URI here. In practice you could use config to store this (so you can easily set it to the correct URI in production).

Now, with HttpClient registered our RelatedProducts component will work during prerendering.

Another option is to embrace polymorphism!

If we use an interface we can have one implementation that uses HttpClient for WASM, and another which goes direct to the ProductStore for Server.

Use An Interface (Bypass The Network Calls On Server)

First we need to define the interface.

I already have a ProductStore implementation (that we used on the server last time out) so let’s pull that ListRelated method up to an interface.

public interface IProductStore
{
    Task<RelatedProductsList?> ListRelated(int productId, int pageNumber);
    
    ...
}

We can now use this in place of the HttpClient call in RelatedProducts.

Client/RelatedProducts.razor

@using BlazorDemoApp.Shared.Data
@inject IProductStore ProductStore

...
@code {

    ...
    
    private async Task LoadRelatedProducts()
    {
        related = await ProductStore.ListRelated(Id, currentPage);
    }

}

To make this work we need two implementations of the IProductStore interface.

First, in the client project, we’ll create one that uses HttpClient

Client/ClientProductStore.cs

public class ClientProductStore : IProductStore
{
    private readonly HttpClient _httpClient;

    public ProductStore(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }
    
    public async Task<RelatedProductsList?> ListRelated(int productId, int pageNumber)
    {
        var uri = $"/api/products/related?productId={productId}&pageNumber={pageNumber}";
        return await _httpClient.GetFromJsonAsync<RelatedProductsList>(uri);
    }
    
    ...
}

Which we can register in the client’s Program.cs.

builder.Services.AddScoped<IProductStore, ClientProductStore>();

Meanwhile on the server we have this implementation (the same service we were already using before). Note it now also implements the IProductStore interface.

public class ProductStore : IProductStore
{
    public async Task<RelatedProductsList?> ListRelated(int productId, int pageNumber)
    {
        // fetch and return related products from DB
    }
    
    ...
}

Which we can register in the server’s Program.cs.

builder.Services.AddScoped<IProductStore, ProductStore>();

Phew! It’s more steps, but we now have an approach that works whether we’re rendering on the server (using SSR, Blazor Server, or prerendering) and also on the client when we opt to use Blazor WASM.

When we run this in the browser and access the product details page we’ll see the API call to the backend.

Alt text

Making it work in Preview 6

This being pre-release software and all there are a few extra hoops to jump through to get interactive components working using Blazor WASM in this preview.

Here’s what you’ll need to do if you want to try it out:

  • Reference the Microsoft.AspNetCore.Components.WebAssembly.Server Nuget package in your server project
  • Reference the Microsoft.AspNetCore.Components.WebAssembly Nuget package in your client project

With these in place you’ll need to update Program.cs in your server project to add the necessary services to support rending components using WebAssembly:

builder.Services.AddRazorComponents()
    .AddServerComponents()
    .AddWebAssemblyComponents();

You also need to update the call to MapRazorComponents:

app.MapRazorComponents<App>()
    .AddServerRenderMode()
    .AddWebAssemblyRenderMode();

Finally, for Preview 6 you’ll need to modify your .csproj for the Server project to explicitly specify the correct Razor Language Version. This requirement should disappear in the next preview:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
      
  	<!-- Specify the Razor Language Version -->
    <RazorLangVersion>8.0</RazorLangVersion>
      
  </PropertyGroup>
  
  ...
  
</Project>

WASM, Server, or no interactivity at all…

SSR looks set to be a compelling option for building many Line Of Business apps using Blazor and .NET 8 come November.

When it comes to making specific components interactive we have options.

Blazor Server offers a straightforward approach, where your components run in the same environment (the server) whether they’re being rendered via SSR or Blazor Server.

Or, you can opt for Blazor WASM.

If you’re keen to avoid the latency of sending DOM interactions (and receiving DOM updates) via Blazor Server’s socket connection it may just be your best bet.

Or, of course, you could just use all the things - SSR for non-interactive pages, Blazor Server for components where latency isn’t an issue, and Blazor WASM for everything else! 😅

Join the Practical ASP.NET Newsletter

Get every .NET 8 article first, delivered straight to your inbox.

I respect your email privacy. Unsubscribe with one click.

    Next up

    How to upload a file with Blazor SSR in .NET 8?
    How to handle file uploads without using an interactive render mode?
    3 simple design tips to improve your Web UI
    Spruce up your features
    The quickest way to integrate PayPal checkout with Blazor SSR in .NET 8
    JavaScript Interop works differently with Blazor Server-side rendering