Loading data in a Blazor web app, without multiple database or API calls

The code described here is available in the Pixata.Blazor Nuget package (v1.30.0 onwards, GitHub page here if you want to see what else is in the package). If aren’t interested in my ramblings, and just want to see how to use it, install the package and scroll past the waffle here to the code samples below.

Update: As of Pixata.Blazor version 1.31.0, the memory cache feature has been removed from this utility. Whilst it seemed like a good idea at the time, it had many problems, not least of which was the question if this was the best way to cache the data anyway. Given that Blazor is based on MVVM, it’s normal to use ObservableCollections for collections. Unfortunately, the default JSON serialiser that .NET’s IMemoryCache implementation doesn’t support these, so the collections weren’t serialised, losing the data. I looked at writing a custom memory cache with a custom serialiser, but that got so complex that I decided it really wasn’t worth the time or effort.

I know I’m a bit slow off the mark, but I keep looking at the Blazor web app that was introduced with .NET8, having a brief play, but not having enough time to get my head round the way it works.

I started a new project a couple of weeks ago, and thought it would be a good opportunity to use the web app. This meant getting my head around render modes, and how the app switches between them. I found this blog post by Jon Hilton to be very helpful.

One of the big issues that this approach brings is loading data. As the page renders twice, once on the server and once in the WASM client, there is a danger of loading the data twice. It’s not obvious (at first at least) how you could share the data between the two renderings. The other problem is that you need to remember that the two data access calls will be done in different places, so whilst your server code can access the DbContext directly, the client code needs to call an API.

Again, Jon Hilton to the rescue, with another helpful blog post. The discussion is clear, and the code he shows works nicely. He used persistent storage to hold on to the data that the first rendering (server) loads, meaning that the second rendering (client) could pick up the persisted data and not make an API call. I didn’t try his “simpler approach” as I wasn’t convinced that this approach would scale well.

However, using his approach as-is meant a large amount of boilerplate code in every page. I decided to have a go at wrapping the code into a reusable class. That way, it could be injected into a component and used with a single line of code.

I’m not going to explain the implementation in detail, mainly as the bulk of the code is based on Jon’s code. However, a few comments are in order…

Assuming a key

In order to store data in persistent storage, you need to supply a key (like any cache). Given that the vast majority of uses for this utility would be loading data for a page, it made sense to assume the URI as the key, unless the developer supplied one explicitly. This made the call cleaner.

In order to avoid issues with HTTP vs HTTPS (which is common when debugging in Visual Studio), or issues with port numbers changing (not so common, but not unlikely) I stripped the URI back to just the path.

Accounting for any type of data

Jon’s code was hard-wired to the data type that the page used. In order to make the code reusable, I needed to make the helper class generic.

Caching the data – Removed in v.1.31.0, see note above

As I was writing this utility, it occurred to me that I could go a stage further. The persistent storage allows us to pass the data from the server rendering to the client, but doesn’t persist it any further than that. If you navigate to another page and back, you’ll need to reload the data. I thought it would be useful to include a proper cache in the utility, so navigating around the WASM pages wouldn’t require reloading any data.

As it happens, just the previous day, I had been watching a video about the new hybrid cache that was introduced in .NET9. It looked like something I would probably never use, but was worth keeping in mind in case. Turned out to be just what I needed here.

If you look at the code, you’ll notice that I actually used IMemoryCache instead. The reason for that is because the hybrid cache is still in preview, and I have this aversion to using preview software in production code. From what I can see, the hybrid cache is a drop-in replacement for the IMemoryCache, with some improvements, and the option to use distributed storage if that were needed (highly unlikely in this scenario). So, I decided to use IMemoryCache, and replace it with a hybrid cache when it comes out of preview.

So, with the cache in place, once the data has been loaded once (during the server rendering), it never needs to be loaded again, unless you explicitly remove it from the cache, or navigate out of the WASM.

Examining the cache

As I was writing the code, I found the need to examine the contents of the cache. Oddly enough, Microsoft didn’t add any methods for this, so you can’t check what is in there. You can create an entry, remove an entry, or try and get an entry with a known key, but you can’t list the keys. Seems like a very obvious requirement to have missed, but what do I know!

There is a method to get statistics, but this was disappointingly devoid of much useful information. It told you how may entries there are in the cache (and three other integers, none of which seemed useful), but that was it.

Anyway, with a bit of help from a Stack Overflow answer, and some reflection, I managed to add a GetKeys method that returns an array of the keys. This was extremely useful during debugging. I doubt you’d want to use it in production code.

Using the utility

So, after that long ramble, the (probably only) useful bit of the post, how to use the utility.

First you need to install the Pixata.Blazor package.

In both your server and client projects, you need to add the following line to Program.cs

builder.Services.AddMemoryCache();

If you want to use the statistics I mentioned above (can’t see why you would, but who knows), you’ll need to tell the cache to track them…

builder.Services.AddMemoryCache(opt => {
  opt.TrackStatistics = true;
  opt.TrackLinkedCacheEntries = true;
});

Next you’ll need to set up the injection. Again, in both Program.cs files add the following…

builder.Services.AddScoped(typeof(PersistentStateHelper<>));

You’ll need a using for Pixata.Blazor.Extensions. I added this in a global using class.

I’m assuming at this point that you have followed the standard method of data access in Blazor web apps, namely creating an interface for the service that will provide the data, and adding an implementation of this in both projects. The server project’s implementation will access the DbContext, and the client’s implementation will call an API on the server (that will probably make use of the server’s implementation to avoid repeating code).

So you need to inject an instance of your service interface, as well as the state helper into your component…

@inject CompaniesServiceInterface CompaniesService
@inject PersistentStateHelper<ObservableCollection<CompanyOverviewDto>> StateHelper

<rant>
No, I didn’t call it ICompaniesService as that’s a stupid name for it. I have no idea why people are so insistent on hanging on to this one naming convention from the well outdated Hungarian notation that most of you won’t remember. I didn’t like it when it was in fashion (decades ago when I first started coding) and I still don’t like it!
</rant>

Using it as as simple as…

@code {
  public ObservableCollection<CompanyDto> CompaniesList { get; set; } = [];

  protected override async Task OnInitializedAsync() =>
    CompaniesList = await StateHelper.Get(CompaniesService.GetCompanies);

}

When the page loads, the companies list is loaded into both persistent storage (so the client rendering can use it without reloading it) and the memory cache. If you watch your database profiler whilst using your app you’ll see that there is only one database call, even when navigation between pages.

Sometimes you might want to invalidate the cache, say after some data was updated. In my case, the companies list page only shows the company’s name and primary role, so unless they have changed, there is no need to invalidate the cache. I save these when the data loads, and check them after saving..

private async Task Save() {
  await CompaniesService.SaveCompany(_company);
  if (_company.Id == 0 
   || _company.Name != _originalName 
   || _company.PrimaryRole != _originalRole) {
    StateHelper.Remove(RoutesHelper.AdminCompanies);
  }
  NavManager.NavigateTo(RoutesHelper.AdminCompanies);
}

After saving the company, it checks if the Id is zero, meaning a new company, or if either of the two bits of data shown on the list have changed. If so, the entry is removed from the cache before navigating back to the company list page. This means that if they didn’t make any changes that would affect the cached list, it doesn’t need to be loaded again.

Note #1 – The code above assumes that the call to the service was successful. In reality, the service call would indicate success or failure, so the user can be notified. I left that bit out for simplicity.
Note #2 – The RoutesHelper class is a collection of strings that hold all the routes in the app. This allows me to have one central place where routes are stored. That way, I can be sure that I don’t make typos when using routes in y code, and allows me to change the routes with ease. I strongly recommend this approach.

What’s next?

The only other feature I can see that might be useful is an option to tell the helper not to cache the data. There may be times when you want the persistent storage, but don’t want the caching, as the data is not useful anywhere else, and/or is too short-lived to be worth caching. That’s something I will probably add at some point.

Be First to Comment

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.