5

Prerendering your Blazor WASM application with .NET 5 (part 2 - solving the miss...

 3 years ago
source link: https://jonhilton.net/blazor-wasm-prerendering-missing-http-client/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Prerendering your Blazor WASM application with .NET 5 (part 2 - solving the missing HttpClient problem)

October 9, 2020 · 8 minute read · Tags: blazor , prerendering

In part 1 we saw how to set up your Blazor WASM site for prerendering.

This means you can serve your application nice and quickly to your users without making them wait for Blazor WASM to finish loading.

However, we were left with a bit of a problem.

When you navigate to the /FetchData page and hit refresh you get an error that HttpClient hasn’t been registered.

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

Here’s the problem:

blazorprerendermissinghttp.svg

If you attempt to navigate directly to FetchData your Server app will attempt to prerender it (just as it did with Counter or any of the other components).

However, this means it will attempt to execute OnInitializedAsync on the server, at which point it will attempt to retrieve the weather data using HttpClient.

The problem is, we haven’t registered HttpClient as a dependency in the Server project, so it falls over.

Now we could just go ahead and register HttpClient in the Server project’s Startup.cs file, however then we’d be using HTTP to go from the Server project to itself, just to load the weather data!

Dan Roth’s .NET 5 samples demonstrate a handy alternative way to address this problem.

If we abstract away the logic for retrieving weather data we can make sure it works on both the server and the client.

Considering Blazor for your next project?

Learn my simple, repeatable process for transforming ideas into features using Blazor's component model.

4 days, 4 emails; enter your email in the box below and I'll send you Lesson #1.

Email address

Another layer of abstraction

Currently FetchData.razor uses the HTTPClient directly…

FetchData.razor (markup)

@page "/fetchdata"
@inject HttpClient Http

<!-- weather markup -->

@code {
    private WeatherForecast[] forecasts;

    protected override async Task OnInitializedAsync()
    {
        forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
    }
}

This makes a HTTP call to the API exposed by the Server project.

This call will fail when prerendering because the Server project hasn’t been configured to know anything about HttpClient.

If we adopt Dan’s approach we can replace this with a call to an interface.

FetchData.razor (markup)

@page "/fetchdata"
@inject IWeatherForecastService WeatherForecastService

<!-- weather markup -->

@code {
    private WeatherForecast[] forecasts;

    protected override async Task OnInitializedAsync()
    {
        forecasts = await WeatherForecastService.GetForecastAsync();
    }
}

We’ll need to define the interface itself. This needs to go in the Shared project so both Client and Server can access it.

Shared\IWeatherForecastService.cs

namespace ExampleApp.Shared
{
    public interface IWeatherForecastService
    {
        Task<WeatherForecast[]> GetForecastAsync();
    }
}

Now we can create two implementations of that interface.

Get weather data on the client (in the browser)

When running on the client we still need to fetch the data via an HTTP GET request to the server.

FetchDataViaHTTP.svg

So this implementation (Client.Data.WeatherForecastService) can make the exact same HTTP call our component was making directly before.

Client\Data\WeatherForecastService.cs

using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
using ExampleApp.Shared;

namespace ExampleApp.Client.Data
{
    public class WeatherForecastService : IWeatherForecastService
    {
        private readonly HttpClient _httpClient;

        public WeatherForecastService(HttpClient httpClient)
        {
            _httpClient = httpClient;
        }
        
        public async Task<WeatherForecast[]> GetForecastAsync()
        {
            return await _httpClient.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
        }
    }
}

Get weather data on the server

For the server implementation we don’t need to go via HTTP and can instead fetch the weather data directly (in a more realistic scenario this would likely come from a query to a database or similar).

FetchDataViaServer.svg

To make this work we can take the code which currently lives in the WeatherForecastController and move it into a new WeatherForecastService class.

Server\Data\WeatherForecastService.cs

namespace ExampleApp.Server.Data
{
    public class WeatherForecastService : IWeatherForecastService
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        public async Task<WeatherForecast[]> GetForecastAsync()
        {
            var rng = new Random();
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
                {
                    Date = DateTime.Now.AddDays(index),
                    TemperatureC = rng.Next(-20, 55),
                    Summary = Summaries[rng.Next(Summaries.Length)]
                })
                .ToArray();
        }
    }
}

This wil be invoked by the FetchData.razor component when it’s initially (pre)rendered on the server.

Now we need to replace the code in WeatherForecastController with a call to this service.

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private readonly IWeatherForecastService _weatherForecastService;

    public WeatherForecastController(IWeatherForecastService weatherService)
    {
        _weatherForecastService = weatherService;
    }

    [HttpGet]
    public async Task<IEnumerable<WeatherForecast>> Get()
    {
        return await _weatherForecastService.GetForecastAsync();
    }
}

We still need a controller

If the component is going to call WeatherForecastService directly you might wonder why we need the controller at all?

Don’t forget, when our app runs in the browser (the client) it will still be using HTTP to fetch this data. This controller acts as the API endpoint for these requests, so the client can continue to interact with our application over HTTP.

It just so happens that on the server we can invoke the WeatherForecastService directly (without going via an HTTP request).

Register the two implementations of IWeatherForecastService

Now we need to make sure we register the relevant implementations in each project.

Client\Program.cs

using ExampleApp.Client.Data;
using ExampleApp.Shared;

public class Program
{
    public static async Task Main(string[] args){
        
        //other code
        
        builder.Services.AddScoped<IWeatherForecastService, WeatherForecastService>();

        await builder.Build().RunAsync();
    }
}

Server\Startup.cs

using ExampleApp.Server.Data;
using ExampleApp.Shared;

 public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            // other code

            services.AddScoped<IWeatherForecastService, WeatherForecastService>();
        }
    }

With that, when the app is prerendered it will use Server.Data.WeatherForecastService on the server.

When running in the browser it will use Client.Data.WeatherForecastService (which uses HttpClient but ultimately executes the same code in the server project, via Web API).

OnInitializedAsync is called twice

The final thing to note here is that data will be retrieved twice when prerendering this way; once on the server when prerendering, then again on the client when Blazor WASM has finished loading.

This is really obvious when viewing the weather example because it’s random data so you see it appear then change when Blazor WASM kicks in…

fetchdata.gif

Why the double call?

When you opt to use prerendering your components are rendered once using the “static prerenderer” on the server. At this point OnInitializedAsync is invoked.

The resulting static HTML is sent back to be displayed in the browser.

After the browser has displayed this HTML it continues to load the Blazor WASM app. Once that’s done it renders the component (on the client) and calls OnInitializedAsync again, hence the new data being retrieved and displayed.

At the time of writing the only guidance I can find from Microsoft about how to handle this refers to Blazor Server app prerendering, but unless I’m missing something it would apply here too…

The suggested solution is to cache the data on the server.

This doesn’t stop the second call being made to but it does reduce round trips to your database and also ensures the data is consistent between the prerender and subsequent client-side render.

I’ve seen two ways to do this:

Use memory cache

One option is to add a little memory caching to the server version implementation of IWeatherForecastService.

Server\Data\WeatherForecastService

public class WeatherForecastService : IWeatherForecastService
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    private readonly IMemoryCache _memoryCache;

    public WeatherForecastService(IMemoryCache memoryCache)
    {
        _memoryCache = memoryCache;
    }

    public async Task<WeatherForecast[]> GetForecastAsync()
    {
        return _memoryCache.GetOrCreate("WeatherData", e =>
        {
            e.SetOptions(new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(30)
            });
            
            var rng = new Random();
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
                {
                    Date = DateTime.Now.AddDays(index),
                    TemperatureC = rng.Next(-20, 55),
                    Summary = Summaries[rng.Next(Summaries.Length)]
                })
                .ToArray();
        });
    }
}

This ensures the exact same data will be returned from memory for the second request (or indeed for any other requests made within the 30 seconds before the cached entry expires).

Make the service a singleton

In Dan Roth’s .NET 5 samples it wasn’t initially clear to me why the “random” data remained consistent between the two renders, then I spotted the answer!

He has the WeatherForecastService registered as a singleton in Server\Startup.cs.

services.AddSingleton<IWeatherForecastService, WeatherForecastService>();

The GetForecast method in Server.Data.WeatherForecastService then adds forecasts to a _forecasts field, until a certain count has been reached.

Server\Data\WeatherForecastService.cs

private readonly List<WeatherForecast> forecasts = new List<WeatherForecast>();

public async Task<WeatherForecast[]> GetForecastAsync(int daysFromNow, int count)
{
    await Task.Delay(Math.Min(count * 20, 2000));

    var rng = new Random();

    while (forecasts.Count < daysFromNow + count)
    {
        forecasts.Add(new WeatherForecast
        {
            Date = DateTime.Today.AddDays(forecasts.Count),
            TemperatureC = rng.Next(-20, 55),
            Summary = Summaries[rng.Next(Summaries.Length)]
        });
    }

    return forecasts.Skip(daysFromNow).Take(count).ToArray();
}

This means there’s just one instance of this service in memory running on the server, so it doesn’t matter how many times you call this method, if you’re passing the same arguments you’ll get the same data back.

Effectively this is also an in-memory cache, just implemented in a slightly different way…

In summary

It’s entirely possible to make your data fetching (and calls to other services) work when you opt to prerender your Blazor WASM application.

You just have to be aware that your component’s OnInitializedAsync method will be called twice.

You can use an interface to abstract the logic for this, then have a client implementation which delegates to HTTP to fetch the data (when running on the client) and a server implementation which fetches the data directly.

When prerendering OnInitializedAsync will be called twice (once on the server, then again on the client) for any components that are prerendered on the server.

You can mitigate this by using caching to avoid roundtrips to your database etc. and to ensure consistent data is returned in both cases.

Grab the source code for the completed example here.

Further reading

Considering Blazor for your next project?

Learn my simple, repeatable process for transforming ideas into features using Blazor's component model.

4 days, 4 emails; enter your email in the box below and I'll send you Lesson #1.

Email address

Next up

Dark mode for your web applications (using Blazor and Tailwind CSS)
Eyestrain is a real problem; help your users by adapting your site to their dark mode preferences
Is it possible to render components “dynamically” using Blazor?
Because life’s not complicated enough already!
Render Blazor WASM components in your existing MVC/Razor Pages applications
You can render individual Blazor WASM components in your existing Razor Pages (or MVC) Core app.
Login
AMerkuri
1 point
49 days ago

Would like to see auth + prerendering solution for blazor wasm.

Fredrik Nilsson
1 point
21 days ago

So the only reason for the single ton is that it is random data? Should it also work with transient if the data isn't random?

Jon Hilton
0 points
21 days ago

Essentially yes.

The singleton is primarily there because that services returns "hardcoded" data. In a more typical scenario you'd probably be fetching data from a database or something.

In that case, you'd probably be better to use the memory cache option.

Saying that, you could still have a singleton service which fetches data then stores it (in a field or some such) which every subsequent request would then use.

But if you go down that route you're essentially implementing a kind of "poor man's caching" anyway and the memory cache might be the better option.

Scott Hero
1 point
35 days ago

So pre rendering doesnt work with Aunthentication?

Jon Hilton
0 points
29 days ago

There are ways to make it work, but it does makes things slightly more complicated (as Auth so often does...) I'll try to share a version with Auth working soon...

Kot
0 points
19 days ago

Should the service implement actions that write as well? So the controller can forward all requests to the service. Should the service also implement validation?

Jon Hilton
1 point
17 days ago

Hey Kot,

I'd hesitate to prescribe a definitive approach (as different contexts might require different solutions) but generally yes, services like this in the Blazor client application can be useful as a lightweight abstraction for querying the system and/or issuing commands (so, changing the data).

The service is especially handy in this context because it can then be used to either make the calls via http (for Blazor WASM) or more directly (for Blazor Server).

In terms of validation, you can see one approach to that in action here: https://jonhilton.net/blazor-client-server-validation-with-fluentvalidation/


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK