Twitter About Home

IndexedDB in Blazor

Exploring the Reshiru.Blazor.IndexedDB package

Published Aug 3, 2019

Almost all rich client-side web apps (SPAs) involve interacting with a data store. Normally, that data store is held on some server, and the browser-based app queries it by making HTTP calls to some API endpoint. Another option, though, is to store a database client-side in the browser. The great benefit of doing so is that it permits completely instant querying, and can even work offline.

IndexedDB has been around for ages, and remains the dominant way to put a client-side database in your SPA. It’s an indexed store of JSON objects, which lets you configure your own versioned data schema and perform efficient queries against the indexes you’ve defined. Naturally, it works both offline and online.

Using IndexedDB with Blazor

You could use the native IndexedDB APIs through Blazor’s JS interop capability. But you’ll have a rough time, because - to put it kindly - the IndexedDB APIs are atrocious. IndexedDB came onto the scene before Promise, so it has an events-based asynchrony model, which is a disaster to work with.

So, I was pretty intrigued when I heard about Reshiru.Blazor.IndexedDB.Framework, a NuGet package described as:

An easy way to interact with IndexedDB and make it feel like EFCore

Taking the bizarre IndexedDB APIs and turning them into nice, idiomatic .NET ones? Let’s have a look.

Update: Internally, Reshiru.Blazor.IndexedDB.Framework is built on TG.Blazor.IndexedDB by William Tulloch, which surfaces the IndexedDB features in .NET. Reshiru’s package builds on this by adding an EF-like DB context API.

Getting started

First, in your Blazor app’s .csproj, add a reference to the Reshiru.Blazor.IndexedDB.Framework:

<PackageReference Include="Reshiru.Blazor.IndexedDB.Framework" Version="1.0.1" />

In your Startup.cs’s ConfigureServices, set up the IIndexedDbFactory. As far as I know, the IndexedDbFactory doesn’t maintain any state so it’s safe to make it singleton, but if in the future it becomes stateful, you’d want to use scoped:

services.AddScoped<IIndexedDbFactory, IndexedDbFactory>();

Now you’re ready to define your data schema. This package makes it dead easy, since as with EF, it’s just C# classes:

namespace MyApp.Data
{
    // Represents the database
    public class ExampleDb : IndexedDb
    {
        public ExampleDb(IJSRuntime jSRuntime, string name, int version) : base(jSRuntime, name, version) { }

        // These are like tables. Declare as many of them as you want.
        public IndexedSet<Person> People { get; set; }
    }

    public class Person
    {
        [Key]
        public long Id { get; set; }

        [Required]
        public string FirstName { get; set; }

        [Required]
        public string LastName { get; set; }
    }
}

The [Key] property will be populated automatically by the package using the auto-incrementing unique value given by the browser to each stored entity.

The [Required] annotations aren’t used by the data store at all, but I want them so my edit form will have validation rules later.

Querying for data

Next it’s time to build some UI that shows what’s in the DB and lets you add and remove items.

To make the Reshiru IndexedDB APIs available in your .razor files, add the following to _Imports.razor in the root folder of the app:

@using Blazor.IndexedDB.Framework
@using MyApp.Data // Or wherever your data classes are

Then, at the top of some .razor component that will fetch and display data, inject the DB service:

@inject IIndexedDbFactory DbFactory

You can now write a @code block that will get access to the database via the DbFactory and fetch some data from it:

@code {
    List<Person> people;

    protected override async Task OnInitAsync()
    {
        using var db = await DbFactory.Create<ExampleDb>();
        people = db.People.ToList();
    }
}

Notice that I’m using a C# using declaration. This is a brand-new C# language feature, so to make this compile, it’s necessary to use the latest version of the compiler. Enable this by putting the following into a <PropertyGroup> in your .csproj:

<LangVersion>preview</LangVersion>

Finally, we can add some Razor markup to display the contents of the people list:

<h1>People</h1>

@if (people != null)
{
    <table class="table">
        <thead>
            <tr>
                <th>ID</th>
                <th>First name</th>
                <th>Last name</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var person in people)
            {
                <tr>
                    <td>@person.Id</td>
                    <td>@person.FirstName</td>
                    <td>@person.LastName</td>
                </tr>
            }
        </tbody>
    </table>
}

Looks good! Running the application now, you’ll see it fetch and display zero items, because of course the database is empty. But if you use the browser’s dev tools, you can see that the Reshiru IndexedDB NuGet package did in fact create the database matching our schema:

Empty DB schema

Inserting data

Blank databases aren’t very interesting. Let’s make a form into which users can enter details for a new entity, and then insert it into the database. Here’s a very basic validated form using standard Blazor APIs, which I put into the same .razor file as above:

<fieldset>
    <legend>Add new person</legend>
    <EditForm Model="@newPerson" OnValidSubmit="@SaveNewPerson">
        <InputText @bind-Value="@newPerson.FirstName" placeholder="First name..." />
        <InputText @bind-Value="@newPerson.LastName" placeholder="Last name..." />
        <button type="submit">Add</button>

        <p><ValidationSummary /></p>
        <DataAnnotationsValidator />
    </EditForm>
</fieldset>

@code {
    Person newPerson = new Person();

    async Task SaveNewPerson()
    {
        // TODO
    }

    // ... rest as before
}

This displays as follows. As you can see, it enforces the validation rules defined on the model type:

Basic form

All that remains is to put in some implementation for the SaveNewPerson method:

async Task SaveNewPerson()
{
    using var db = await this.DbFactory.Create<ExampleDb>();
    db.People.Add(newPerson);
    await db.SaveChanges();

    // Refresh list and reset the form
    newPerson = new Person();
    await OnInitAsync();
}

… and that’s all there is to it. The user can now insert new records to their client-side database without any requests having to go to a server:

Adding people

In case you’re wondering, the browser’s dev tools also confirm that the data was saved:

IndexedDB with contents

Deleting data

To complete this example, let’s put in some Delete buttons. I added the following markup at the end of the <tr> that gets rendered for each row:

<td><button @onclick="@(() => DeletePerson(person))">Delete</button></td>

… and added the following C# handler method to the @code block:

async Task DeletePerson(Person person)
{
    using var db = await this.DbFactory.Create<ExampleDb>();
    db.People.Remove(person);
    await db.SaveChanges();
    await OnInitAsync();
}

A bigger example

The most compelling benefits of client-side databases are:

  • Instant querying (with no HTTP traffic per query)
  • Offline support

To experience this with the Reshiru.Blazor.IndexedDB.Framework package, I built a very simple “recipe database” app that:

  1. Fetches a database of recipes from the server, and uses it to populate a client-side database. This is only fetched once, and remains stored in the browser.
  2. Offers instant search-on-every-keystroke for recipes in that database

It would be pretty trivial to make this work offline using a service worker.

Likewise, it would be simple enough to make the app synchronize updates with changes coming from the server. Give each record a “modified date”, and have the SPA request all records modified since the last batch it received, and insert/update/delete those.

Here’s how it looks:

Recipe app

The source code is available here.

What I learned

After my quick experiment with the recipes app, I think there are some really great aspects of the Reshiru.Blazor.IndexedDB.Framework, and some aspects that make it not quite ready yet.

On the positive side, the package does a fantastic job of keeping the APIs simple and familiar for .NET developers. It’s as easy as, or perhaps easier than, using Entity Framework. The data schema is inferred directly from your model classes, and all aspects of serialization are handled for you. I’m convinced it’s possible to create an experience on par with EF backed by IndexedDB in the browser.

However, I suspect this package has a couple more steps to go before it’s really practical for use in a real application, both of which are already tracked in GitHub issues:

  • It doesn’t yet have any foreign key (FK) support (issue #8)
    • Currently it’s limited to JSON-serializing your entire entities, without any native way to represent associations between them
  • It can’t yet query against IndexedDB itself, but instead loads the entire DB into .NET memory (fixing this is a planned feature)
    • It’s pretty slow right now, and one of the major reasons is that when you do DbFactory.Create, it literally fetches all the data and deserializes it into an in-memory collection. Querying is then done in .NET memory via normal LINQ operations. So currently, it’s not actually using the native indexes on the IndexedDB.

Since both of these capabilities already exist in the underlying TG.Blazor.IndexedDB package, it’s probably quite feasible for them to be surfaced through Reshiru’s DB context APIs.

I’m greatly looking forwards to seeing this package mature, as it’s clearly showing that a great client-side database experience is possible on .NET on WebAssembly.

READ NEXT

Blazor: a technical introduction

Deeper technical details about Blazor

Published Feb 6, 2018