DrUalcman-BlazorIndexedDb
1.11.52
dotnet add package DrUalcman-BlazorIndexedDb --version 1.11.52
NuGet\Install-Package DrUalcman-BlazorIndexedDb -Version 1.11.52
<PackageReference Include="DrUalcman-BlazorIndexedDb" Version="1.11.52" />
<PackageVersion Include="DrUalcman-BlazorIndexedDb" Version="1.11.52" />
<PackageReference Include="DrUalcman-BlazorIndexedDb" />
paket add DrUalcman-BlazorIndexedDb --version 1.11.52
#r "nuget: DrUalcman-BlazorIndexedDb, 1.11.52"
#:package DrUalcman-BlazorIndexedDb@1.11.52
#addin nuget:?package=DrUalcman-BlazorIndexedDb&version=1.11.52
#tool nuget:?package=DrUalcman-BlazorIndexedDb&version=1.11.52
BlazorIndexedDb
Manage IndexedDB from C# with Blazor. A simple way to interact with IndexedDB, similar to how you can do with Entity Framework.
NuGet installation
PM> Install-Package DrUalcman-BlazorIndexedDb
Schema changes keep your data
Adding, removing or changing a StoreSet no longer means deleting the database. The schema in the
browser is compared with your context on every start, and only what differs is applied:
| What you changed | What happens to the data |
|---|---|
Added a StoreSet |
Nothing. The store is created, every other store is untouched. |
Removed a StoreSet |
Its store is dropped. The rows in it are gone, and how many is written to the console. |
| Added or removed a property | The index is created or dropped. Rows are not touched. |
| Changed the primary key | The store is rebuilt and the rows are carried across. A row the new key cannot be taken from is the only one that cannot come, and it is reported with its old key. |
You do not have to bump the version for this. A model that changed without a new version still gets
one, because a version change is the only place IndexedDB lets a schema be modified — so the number
in the browser can end up ahead of the one in your settings, and the library takes that into account
everywhere instead of failing with a VersionError.
It all happens inside the version change transaction, which is what makes it safe: the rows are read from the old store and written to the new one without leaving that transaction, so there is nothing to copy anywhere else first, and anything that goes wrong aborts the whole thing and leaves the database exactly as it was. Half-migrated is not a state you can end up in.
The console is only told about what was lost. A migration that keeps everything says nothing.
What changed in this version
Offline writes work. AddAsync(row, isOffline: true) and UpdateAsync(row, isOffline: true) never wrote anything, in any version: they came back with [Insert] StoreName can't be null and never reached the database. The offline write adds an OffLine flag to each row by turning it into an ExpandoObject, and then handed the result back to the ordinary insert — where the generic argument is no longer your entity but object, so looking up which store it belongs to asked for a model named "Object" and got nothing. The database was never involved, which is why deleting and recreating it changed nothing. If you ever gave up on the offline flag because it did not work, this is the release to try again.
Updating a row that is not there no longer discards the rest of the batch. Update reads the stored row first so it can keep the columns you left out, and from a row that does not exist that merge produced an empty object — which IndexedDB refuses, from a place where the error aborts the whole transaction and rolls back every other row in the call. It is a put, so a row that is not stored yet is now simply written as it is. Updating a row that something else had just deleted was enough to hit this.
What changed in 1.10.50
Writes got dramatically faster, a broken filter got fixed, and failures that used to be reported as success are now reported as failures. Nothing in your code has to change, but two of these are visible from the outside.
Writes no longer wait on a guess. Insert and Update used to resolve on a timer of 50ms per row, so a batch of 4944 rows took 247 seconds no matter how fast IndexedDB really was. They now resolve when the transaction actually completes. That same batch takes milliseconds of database time.
New SelectAsync(column, value), resolved by the index. The indexed lookup existed but nothing could reach it: no method on StoreSet called it, and the interop wrapper was calling the wrong JavaScript function anyway, so the filter was dropped and the whole store came back. Both are fixed and the overload is now public.
GetAsync(id) works. Looking a record up by its primary key built its key range from a variable that does not exist, so it threw before reaching the store and the error was swallowed into a null. It always came back empty, and never said why. If you worked around it by reading the whole store and filtering in C#, you can stop.
A read that cannot open the database no longer hangs. Select handled a failed open by calling a method that does not exist, so the promise was never settled at all.
A blocked database is no longer reported as an error. Blocked means your operation is queued behind another connection that has not closed yet, and it goes ahead on its own once that happens. DropDatabase rejected on it, reporting a failure for a delete that then completed anyway.
A write that failed no longer looks like a success. Nothing handled an aborted transaction or a failed database open, and the timer resolved regardless, so rows that were never written were reported as written. Those cases are now reported as failures.
Deleting many rows takes one transaction. See DeleteAsync with a list of ids below.
A refused row no longer discards the rest of the batch. See "Insert, update and duplicated keys" below.
Everything crossing to JavaScript now travels as UTF-8 bytes in both directions instead of strings. This is invisible from C#.
Current features
Create StoreContext<TStore> from abstract class. Allow multiple StoreContext but database name should be different names. StoreSet per each model you need into a database. Set PrimaryKey in the model. Using convention if have property Id or TableNameId or IdTableName then this is used like PrimaryKey AutoIncremental (only if it's a number is autoincremental) CRUD from StoreSet Select all or one by PrimaryKey or property from StoreSet, filtering either in C# or through the store index Clean all data in a StoreSet Delete many rows in a single transaction Drop Database
How to use
BlazorIndexedDb requires an instance of IJSRuntime, which should normally already be registered.
Create a code-first database model and inherit from IndexedDb. You must use the FieldAttribute to configure the properties.
Your model (e.g., PlayList) should contain an Id property or a property marked with the key attribute.
public class PlayList
{
[FieldAttribute(IsKeyPath = true, IsAutoIncremental = false, IsUnique = true)] //not required from version 1.5.18
public string Id { get; set; }
public string Url { get; set; }
public string Title { get; set; }
public string Ownner { get; set; }
}
namespace
BlazorIndexedDb
BlazorIndexedDb.Attributes
BlazorIndexedDb.Commands
BlazorIndexedDb.Models
BlazorIndexedDb.Store
Then create a DBContext class inheriting from StoreContext<TStore> to manage the database, similar to Entity Framework. Define properties that represent your tables.
public class DBContext : StoreContext<DBContext>
{
#region properties
public StoreSet<PlayList> PlayList { get; set; }
#endregion
#region constructor
public DBContext(IJSRuntime js) : base(js, new Settings { DBName = "MyDBName", Version = 1 }) { }
#endregion
}
In Program.cs add the service for the DBContext
using BlazorIndexedDb;
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
//INJECT THE DBContext or use your implementation
builder.AddBlazorIndexedDbContext<DBContext>();
var app = builder.Build();
await app.RunAsync();
}
}
In index.html, there is no need to add any JavaScript reference. In previous versions this was required, but now it is added dynamically when needed.
In the component, inject DBContext to access IndexedDB.
[Inject]
public DBContext DB { get; set; }
void Select()
{
var PlayList = await DB.PlayList.SelectAsync();
}
void SelectWhere()
{
//resolved by the store index, only the matching rows cross into .NET
var mine = await DB.PlayList.SelectAsync(nameof(PlayList.Ownner), "me");
//this other overload reads the whole store and filters it in C#
var also = await DB.PlayList.SelectAsync(x => x.Ownner == "me");
}
void Add()
{
var NewItems = new List<PlayList>();
CommandResponse response = await DB.PlayList.AddAsync(NewItems);
Console.WriteLine(response.Message);
Console.WriteLine(response.Result);
//Get result per each element
foreach (var item in response.Response)
{
Console.WriteLine(item.Message);
Console.WriteLine(item.Result);
}
}
void Update()
{
var NewItem = new PlayList();
CommandResponse response = await DB.PlayList.UpdateAsync(NewItem);
Console.WriteLine(response.Message);
Console.WriteLine(response.Result);
//Get result per each element
foreach (var item in response.Response)
{
Console.WriteLine(item.Message);
Console.WriteLine(item.Result);
}
}
void Delete()
{
int id = 1;
CommandResponse response = await DB.PlayList.DeleteAsync(id);
Console.WriteLine(response.Message);
Console.WriteLine(response.Result);
//Get result per each element
foreach (var item in response.Response)
{
Console.WriteLine(item.Message);
Console.WriteLine(item.Result);
}
}
void DeleteMany()
{
//all the ids are removed inside a single transaction
var ids = new List<int> { 1, 2, 3 };
CommandResponse response = await DB.PlayList.DeleteAsync(ids);
Console.WriteLine(response.Message);
Console.WriteLine(response.Result);
}
void Drop()
{
CommandResponse response = await DB.DropDatabaseAsync();
Console.WriteLine(response.Message);
Console.WriteLine(response.Result);
}
void Init()
{
//if you delete a db and want to initialize again
await DB.Init();
}
You can modify the model classes any time, but if the model you will pass don't match with the model created when create the IndexDb this will return a exception.
Working with records
In all select actions, you will receive a List<TModel>, except when querying by a single key, in which case you will receive a single model instance.
All actions return either a ResponseJsDb or a List<ResponseJsDb> when multiple rows are processed.
public class ResponseJsDb
{
public bool Result { get; set; }
public string Message { get; set; }
}
Working with records from StoreSet
The store set always returns the model or list of the model for all select actions and CommandResponse record for the commands actions
public sealed record CommandResponse(bool Result, string Message, List<ResponseJsDb> Response)
{
public IEnumerable<ResponseJsDb> Errors { get; }
public CommandResponse EnsureSuccess();
}
Result is false when any row was refused. Errors lists only those rows, so you do not have to walk the whole Response looking for them.
CommandResponse response = await DB.PlayList.AddAsync(NewItems);
foreach (var error in response.Errors)
{
//names the record and the reason, e.g. duplicated key
Console.WriteLine(error.Message);
}
Insert, update and duplicated keys
AddAsync is an insert, not an upsert. A key that already exists is refused, the same way a primary key behaves in SQL. Use UpdateAsync to overwrite an existing row.
A refused row does not take the rest of the batch with it: everything else in the call is written, and the response carries one entry per refused row naming its key and the reason. There is no need to split a write into chunks in order to find out which rows failed.
Exceptions
Writes report failures in the response instead of throwing, because the detail of which row failed cannot survive an exception raised on the JavaScript side. Call EnsureSuccess() when you would rather handle a failed write the way Entity Framework handles a failed SaveChanges. It returns the same response when everything went in, so it can be chained.
//throws ResponseException if any row was refused
CommandResponse response = (await DB.PlayList.AddAsync(NewItems)).EnsureSuccess();
When an exception occurs, a ResponseException will be returned.
public class ResponseException : Exception
{
public string Command { get; set; }
public string StoreName { get; set; }
public string TransactionData { get; set; }
}
More info
Check website for more info.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.AspNetCore.Components.Web (>= 10.0.10)
- Microsoft.AspNetCore.Components.WebAssembly (>= 10.0.10)
-
net6.0
- Microsoft.AspNetCore.Components.Web (>= 6.0.36)
- Microsoft.AspNetCore.Components.WebAssembly (>= 6.0.36)
-
net8.0
- Microsoft.AspNetCore.Components.Web (>= 8.0.29)
- Microsoft.AspNetCore.Components.WebAssembly (>= 8.0.29)
-
net9.0
- Microsoft.AspNetCore.Components.Web (>= 9.0.18)
- Microsoft.AspNetCore.Components.WebAssembly (>= 9.0.18)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated | |
|---|---|---|---|
| 1.11.52 | 276 | 8/7/2026 | |
| 1.10.51 | 114 | 8/6/2026 | |
| 1.10.50 | 154 | 7/21/2026 | |
| 1.9.49 | 441 | 4/28/2026 | |
| 1.8.48 | 506 | 6/1/2025 | |
| 1.8.47 | 724 | 10/10/2024 | |
| 1.7.46 | 358 | 9/5/2024 | |
| 1.6.45 | 302 | 9/4/2024 | |
| 1.6.44 | 289 | 8/30/2024 | |
| 1.6.43 | 298 | 8/17/2024 | |
| 1.6.42 | 294 | 8/7/2024 | |
| 1.6.41 | 460 | 6/22/2024 | |
| 1.6.40 | 456 | 4/16/2024 | |
| 1.6.39 | 378 | 3/26/2024 | |
| 1.6.38 | 405 | 2/28/2024 | |
| 1.6.37 | 346 | 2/27/2024 | |
| 1.6.36 | 412 | 2/27/2024 | |
| 1.6.35 | 417 | 2/18/2024 | |
| 1.6.34 | 764 | 10/10/2023 | |
| 1.6.33 | 513 | 8/18/2023 |
1.11.52 — a schema change keeps the data it can.
- Adding, removing or changing a StoreSet no longer means deleting the database. The schema
in the browser is compared with the context on every start and only the difference is
applied: a new store is created, a store the model dropped is dropped, a property added or
removed is an index created or dropped with the rows untouched, and a store whose primary
key changed is rebuilt with its rows carried across one by one. All of it inside the version
change transaction, so the rows never leave it and anything that fails aborts the lot and
leaves the database as it was — half migrated is not reachable.
- The version does not have to be bumped for a schema change to be applied. A model that
changed without a new version takes the next one, so the database can end up ahead of the
version in the settings, and reads and writes no longer ask for a version at all instead of
failing with a VersionError when they disagree.
- Only what was lost is reported. A store dropped from the model says how many rows went
with it, and a rebuilt store names the rows the new key could not be taken from with the key
they used to have. A migration that keeps everything says nothing.