DrUalcman-BlazorIndexedDb 1.11.52

dotnet add package DrUalcman-BlazorIndexedDb --version 1.11.52
                    
NuGet\Install-Package DrUalcman-BlazorIndexedDb -Version 1.11.52
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="DrUalcman-BlazorIndexedDb" Version="1.11.52" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DrUalcman-BlazorIndexedDb" Version="1.11.52" />
                    
Directory.Packages.props
<PackageReference Include="DrUalcman-BlazorIndexedDb" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add DrUalcman-BlazorIndexedDb --version 1.11.52
                    
#r "nuget: DrUalcman-BlazorIndexedDb, 1.11.52"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package DrUalcman-BlazorIndexedDb@1.11.52
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=DrUalcman-BlazorIndexedDb&version=1.11.52
                    
Install as a Cake Addin
#tool nuget:?package=DrUalcman-BlazorIndexedDb&version=1.11.52
                    
Install as a Cake Tool

Nuget Nuget

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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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.36 is deprecated because it has critical bugs.
1.6.35 417 2/18/2024
1.6.34 764 10/10/2023
1.6.33 513 8/18/2023
Loading failed

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.