Skip to content

Introduction

Getting started

One package, one registration and a context factory - then EF Core runs against a SQLite database in the user's browser tab, on the storage backend your preference order picks. Every page after this one runs against that database, in this tab.

Install

Bit.Bsqlite is the whole install: the ADO.NET surface, the EF Core integration, the worker transport and every storage backend - OPFS, IndexedDB, Cache Storage and in-memory. It targets .NET 8, 9 and 10, each with the EF Core of the same major version. Browser-free unit tests add Bit.Bsqlite.Testing to the test project.

shell Terminal
dotnet add package Bit.Bsqlite

Register the services

Register the backends you ship, the order you prefer them in, and the database. The first available backend wins, but existing data outranks preference: a database created on IndexedDB stays there once OPFS becomes available. AllowNonPersistentFallback() is what makes in-memory acceptable at all.

csharp Program.cs
using Bit.Bsqlite.Storage.IndexedDb;
using Bit.Bsqlite.Storage.InMemory;
using Bit.Bsqlite.Storage.Opfs;

builder.Services.AddBitBsqliteServices(sqlite => sqlite
    .AddStorageProvider<BsqliteOpfsStorageProvider>()
    .AddStorageProvider<BsqliteIndexedDbStorageProvider>()
    .AddStorageProvider<BsqliteInMemoryStorageProvider>()
    .UseStorage(s => s
        .Prefer(BsqliteOpfsStorageProvider.ProviderName)
        .Fallback(BsqliteIndexedDbStorageProvider.ProviderName)
        .Fallback(BsqliteInMemoryStorageProvider.ProviderName)
        .AllowNonPersistentFallback())
    .AddDatabase("app.db"));

builder.Services.AddBitBsqliteDbContext<AppDbContext>();
In a Blazor Web App whose pages are prerendered, register the same services in the server project too: the prerender pass instantiates the pages there, so what they inject has to resolve. Nothing touches the browser until a page opens the database. This site keeps both registrations in one AddDemoServices extension method that both Program.cs files call.

Open a context and query

Inject the context factory wherever you would have opened a DbContext. The first context opens the database and runs the migrations once per session; later ones reuse the session's connection. From there it is ordinary EF Core - with the async API throughout, because the engine runs in a worker the UI thread cannot wait on.

razor Pages/Products.razor
@page "/products"
@using Bit.Bsqlite.EntityFrameworkCore
@using Microsoft.EntityFrameworkCore
@inject IBsqliteDbContextFactory<AppDbContext> Contexts

@code {
    private List<Product> _products = [];

    protected override async Task OnInitializedAsync()
    {
        // The first context opens the database and applies the migrations; later ones reuse the session.
        await using var db = await Contexts.CreateDbContextAsync();
        _products = await db.Products.OrderBy(p => p.Name).ToListAsync();
    }

    private async Task AddAsync()
    {
        await using var db = await Contexts.CreateDbContextAsync();
        db.Products.Add(new Product { Name = "Widget", Price = 19.99m });
        await db.SaveChangesAsync();
    }
}
Synchronous members - SaveChanges(), ToList(), DbConnection.Open() - throw BsqliteSynchronousApiNotSupportedException naming the async overload. The Limits page runs that on purpose.

Prerendering and render modes

A prerendered page runs once on the server, where there is no browser to open a database in. Open it only once the page is interactive - RendererInfo.IsInteractive on Interactive WebAssembly, and OperatingSystem.IsBrowser() on Interactive Auto, where a page on the server circuit is interactive too. This site renders <Routes> with InteractiveWebAssembly and lets its SessionLifecycle component do the check for every page.

csharp A page that is prerendered
protected override async Task OnInitializedAsync()
{
    // Prerendering runs this on the server first, where there is no browser to open a database in.
    // It runs again once the page is interactive on WebAssembly.
    if (RendererInfo.IsInteractive is false) return;

    await using var db = await Contexts.CreateDbContextAsync();
    _orders = await db.Orders.Where(o => o.Status == OrderStatus.Draft).ToListAsync();
}

Interactive Server is not supported: the engine runs in the user's tab, and EF Core on the server would need a native SQLite of its own.

Nothing in the host page

The engine (.wasm), the worker and the backends' VFSes are static web assets under _content/Bit.Bsqlite/, each imported on demand: registering a backend costs nothing until it is selected, and a session downloads one engine build and the VFS of the backend it selected. There is no script tag or stylesheet to add.

Where to go next

An unhandled error has occurred. Reload 🗙