Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ An easy, fast way to use IndexedDB in a Blazor application.
Name = "Person",
PrimaryKey = "id",
PrimaryKeyAuto = true,
UniqueIndexes = new List<string> { "name" }
UniqueIndexes = new List<string> { "guid" },
Indexes = new List<string> { "name" }
}
};
});
Expand All @@ -40,20 +41,25 @@ An easy, fast way to use IndexedDB in a Blazor application.
await manager.AddRecord(new StoreRecord<object>()
{
StoreName = storeName,
Record = new { Name = "MyName", Age = 20 }
Record = new { Name = "MyName", Age = 20, Guid = System.Guid.NewGuid() }
});
```

### How it works
You defined your databases and store schemes in the `ServicesCollection`. We add a helper called `AddBlazorDB` where you can build ` DbStore`. From there, we ensure that an instance of `IBlazorDbFactory` is available in your service provider and will automatically add all `DbStores` and `IndexedDbManagers` based on what is defined with `AddBlazorDb`. This allows you to have multiple databases if needed.
You defined your databases and store schemes in the `ServicesCollection`. We add a helper called `AddBlazorDB` where you can build ` DbStore`. From there, we ensure that an instance of `IBlazorDbFactory` is available in your service provider and will automatically add all `DbStores` and `IndexedDbManagers` based on what is defined with `AddBlazorDb`. This allows you to have multiple databases if needed. The databases and store schemes in the `ServicesCollection` are "static" because they cannot be created outside of the `WebAssemblyHostBuilder`.

To access any database/store, all you need to do is inject the `IBlazorDbFactory` in your page and call `GetDbManager(string dbName)`. We will pull your `IndexedDbManager` from the factory and make sure it's created and ready.

If you call `GetDbManager(string dbName)` with a database name that does not exist within the `ServicesCollection`, a "dynamic" database will be created. The database will not have any schemas until you call the manager's `AddSchemaAsync(StoreSchema storeSchema)` method to define schemas.

Most calls will either be true `Async` or let you pass an `Action` in. If it lets you pass in an `Action`, it is optional. This `Action` will get called when the method is complete. This way, it isn't blocking on `WASMs` single thread. All of these calls return a `Guid` which is the "transaction" identifier to IndexeDB (it's not a true database transaction, just the call between C# and javascript that gets tracked).

If the call is flagged as `Async`, we will wait for the JS callback that it is complete and then return the data. The library takes care of this connection between javascript and C# for you.

### Available Calls
- `Task<string[]> GetDbNames()` - Get an array of all existing databases, both static and dynamic
- `Task<Guid> AddSchema(StoreSchema storeSchema, Action<BlazorDbEvent> action)` - Add a new store schema to a dynamic database, with an optional callback when complete
- `Task<BlazorDbEvent> AddSchemaAsync(StoreSchema storeSchema)` - Add a new store schema to a dynamic database, and wait for it to complete
- `Task<Guid> OpenDb(Action<BlazorDbEvent> action)` - Open the IndexedDb and make sure it is created, with an optional callback when complete
- `Task<Guid> DeleteDb(string dbName, Action<BlazorDbEvent> action)` - Delete the database, with an optional callback when complete
- `Task<BlazorDbEvent> DeleteDbAsync(string dbName)` - Delete the database and wait for it to complete
Expand Down
39 changes: 39 additions & 0 deletions example/BlazorDB.Example/Pages/Index.razor
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,28 @@
<button @onclick="WhereNameAndId">Where Id and Name</button>
<button @onclick="ViolateUniqueness">Violate index uniqueness</button>

<h1>Employee (Dynamic)</h1>
<button @onclick="SwitchToEmployee">Switch</button>
<button @onclick="AddSchema">Add Schema</button>
<button @onclick="Add">Add</button>
<button @onclick="Put">Put</button>
<button @onclick="Add100">Add 100</button>
<button @onclick="Add100Async">Add 100 (Async)</button>
<button @onclick="BulkAdd">BulkAdd 10,000</button>
<button @onclick="BulkAddAsync">BulkAdd 10,000 (Async)</button>
<button @onclick="Update">Update</button>
<button @onclick="UpdateAsync">Update (Async)</button>
<button @onclick="Delete">Delete</button>
<button @onclick="DeleteAsync">Delete (Async)</button>
<button @onclick="GetItem">Get Item</button>
<button @onclick="ToArray">ToArray</button>
<button @onclick="AddWithCallback">Add With Callback</button>
<button @onclick="DeleteDb">Delete DB</button>
<button @onclick="WhereId">Where Id</button>
<button @onclick="WhereNameAndId">Where Id and Name</button>
<button @onclick="ViolateUniqueness">Violate index uniqueness</button>


@code {
IndexedDbManager manager;

Expand Down Expand Up @@ -76,6 +98,23 @@
manager = await _dbFactory.GetDbManager(dbName);
}

protected async void SwitchToEmployee()
{
dbName = "Test3";
storeName = "Employee";
manager = await _dbFactory.GetDbManager(dbName);
manager.ActionCompleted += (_, __) =>
{
Console.WriteLine(__.Message);
};
}

protected async void AddSchema()
{
var schema = new StoreSchema() { Name = storeName, PrimaryKey = "id", PrimaryKeyAuto = true, Indexes = { "name"}, UniqueIndexes = { "guid" } };
await manager.AddSchema(schema);
}

protected async Task Create()
{
Console.WriteLine("Create");
Expand Down
51 changes: 48 additions & 3 deletions src/BlazorDB/BlazorDbFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public class BlazorDbFactory : IBlazorDbFactory
readonly IJSRuntime _jsRuntime;
readonly IServiceProvider _serviceProvider;
readonly IDictionary<string, IndexedDbManager> _dbs = new Dictionary<string, IndexedDbManager>();
const string InteropPrefix = "window.blazorDB";

public BlazorDbFactory(IServiceProvider serviceProvider, IJSRuntime jSRuntime)
{
Expand All @@ -25,8 +26,12 @@ public async Task<IndexedDbManager> GetDbManager(string dbName)
await BuildFromServices();
if(_dbs.ContainsKey(dbName))
return _dbs[dbName];

return null;
if ((await GetDbNames()).Contains(dbName))
await BuildExistingDynamic(dbName);
else
await BuildNewDynamic(dbName);

return _dbs[dbName];
}

public Task<IndexedDbManager> GetDbManager(DbStore dbStore)
Expand All @@ -40,11 +45,51 @@ async Task BuildFromServices()
foreach(var dbStore in dbStores)
{
Console.WriteLine($"{dbStore.Name}{dbStore.Version}{dbStore.StoreSchemas.Count}");
var db = new IndexedDbManager(dbStore, _jsRuntime);
var db = new IndexedDbManager(dbStore, _jsRuntime, _dbs);
await db.OpenDb();
_dbs.Add(dbStore.Name, db);
}
}
}

async Task BuildExistingDynamic(string dbName)
{
var dbStore = await GetDbStoreDynamic(dbName);
var db = new IndexedDbManager(dbStore, _jsRuntime, _dbs);
_dbs.Add(dbStore.Name, db);
}

async Task BuildNewDynamic(string dbName)
{
var newDbStore = new DbStore() { Name = dbName, StoreSchemas = new List<StoreSchema>(), Version = 0, Dynamic = true };
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than open the DB here and have your logic in the javascript version to get around the Dexie bug, should we not allowed a DB to be opened if there is no schema? What's the point to open it anyway?

Thinking out loud, maybe in AddSchema we detect if any previous schemas and then open after at least 1 schema is added?

var newDb = new IndexedDbManager(newDbStore, _jsRuntime, _dbs);
await newDb.OpenDb();
newDbStore.Version = 1;
_dbs.Add(newDbStore.Name, newDb);
}

public async Task<string[]> GetDbNames()
{
try
{
return await _jsRuntime.InvokeAsync<string[]>($"{InteropPrefix}.{IndexedDbFunctions.GET_DB_NAMES}");
}
catch (JSException jse)
{
throw new Exception($"Could not get database names. JavaScript exception: {jse.Message}");
}
}

async Task<DbStore> GetDbStoreDynamic(string dbName)
{
try
{
return await _jsRuntime.InvokeAsync<DbStore>($"{InteropPrefix}.{IndexedDbFunctions.GET_DB_STORE_DYNAMIC}", new object[] { dbName });
}
catch (JSException jse)
{
throw new Exception($"Could not get dynamic database store configuration. JavaScript exception: {jse.Message}");
}
}
}
}
1 change: 1 addition & 0 deletions src/BlazorDB/DbStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ public class DbStore
public string Name { get; set; }
public int Version { get; set; }
public List<StoreSchema> StoreSchemas { get; set; }
public bool Dynamic { get; set; } = false;
}
}
60 changes: 59 additions & 1 deletion src/BlazorDB/IndexDbManager.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.JSInterop;

Expand All @@ -12,6 +13,7 @@ public class IndexedDbManager
{
readonly DbStore _dbStore;
readonly IJSRuntime _jsRuntime;
readonly IDictionary<string, IndexedDbManager> _dbs;
const string InteropPrefix = "window.blazorDB";
DotNetObjectReference<IndexedDbManager> _objReference;
IDictionary<Guid, WeakReference<Action<BlazorDbEvent>>> _transactions = new Dictionary<Guid, WeakReference<Action<BlazorDbEvent>>>();
Expand All @@ -27,11 +29,12 @@ public class IndexedDbManager
/// </summary>
/// <param name="dbStore"></param>
/// <param name="jsRuntime"></param>
internal IndexedDbManager(DbStore dbStore, IJSRuntime jsRuntime)
internal IndexedDbManager(DbStore dbStore, IJSRuntime jsRuntime, IDictionary<string, IndexedDbManager> dbs)
{
_objReference = DotNetObjectReference.Create(this);
_dbStore = dbStore;
_jsRuntime = jsRuntime;
_dbs = dbs;
}

public List<StoreSchema> Stores => _dbStore.StoreSchemas;
Expand Down Expand Up @@ -63,6 +66,10 @@ public async Task<Guid> DeleteDb(string dbName, Action<BlazorDbEvent> action = n
}
var trans = GenerateTransaction(action);
await CallJavascriptVoid(IndexedDbFunctions.DELETE_DB, trans, dbName);
if (_dbStore.Dynamic)
{
_dbs.Remove(dbName);
}
return trans;
}

Expand All @@ -80,6 +87,57 @@ public async Task<BlazorDbEvent> DeleteDbAsync(string dbName)
}
var trans = GenerateTransaction();
await CallJavascriptVoid(IndexedDbFunctions.DELETE_DB, trans.trans, dbName);
if (_dbStore.Dynamic)
{
_dbs.Remove(dbName);
}
return await trans.task;
}

/// <summary>
/// Adds a new store schema to a dynamic database
/// </summary>
/// <param name="storeSchema">New schema to add</param>
/// <returns></returns>
public async Task<Guid> AddSchema(StoreSchema storeSchema, Action<BlazorDbEvent> action = null)
{
if (!_dbStore.Dynamic)
{
throw new ArgumentException($"Database is not dynamic. Cannot add schema.");
}

var trans = GenerateTransaction(action);
await CallJavascriptVoid(IndexedDbFunctions.ADD_SCHEMA, trans, _dbStore, storeSchema);
_dbStore.StoreSchemas.Add(storeSchema);
_dbStore.Version = _dbStore.Version + 1;

return trans;
}

/// <summary>
/// Adds a new store schema to a dynamic database
/// Waits for response
/// </summary>
/// <param name="storeSchema">New schema to add</param>
/// <returns></returns>
public async Task<BlazorDbEvent> AddSchemaAsync(StoreSchema storeSchema)
{
if (!_dbStore.Dynamic)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why just for dynamic?

{
throw new ArgumentException($"Database is not dynamic. Cannot add schema.");
}

var trans = GenerateTransaction();
try
{
await CallJavascriptVoid(IndexedDbFunctions.ADD_SCHEMA, trans.trans, _dbStore, storeSchema);
_dbStore.StoreSchemas.Add(storeSchema);
_dbStore.Version = _dbStore.Version + 1;
}
catch (JSException jse)
{
RaiseEvent(trans.trans, true, jse.Message);
}
return await trans.task;
}

Expand Down
3 changes: 3 additions & 0 deletions src/BlazorDB/IndexedDbFunctions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ internal struct IndexedDbFunctions
{
public const string CREATE_DB = "createDb";
public const string DELETE_DB = "deleteDb";
public const string GET_DB_NAMES = "getDbNames";
public const string GET_DB_STORE_DYNAMIC = "getDbStoreDynamic";
public const string ADD_SCHEMA = "addSchema";
public const string ADD_ITEM = "addItem";
public const string BULKADD_ITEM = "bulkAddItem";
public const string PUT_ITEM = "putItem";
Expand Down
85 changes: 82 additions & 3 deletions src/BlazorDB/wwwroot/blazorDB.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,81 @@
window.blazorDB = {
databases: [],
createDb: function(dotnetReference, transaction, dbStore) {
if(window.blazorDB.databases.find(d => d.name == dbStore.name) !== undefined)
getDbNames: function () {
return new Promise(resolve => Dexie.getDatabaseNames().then(resolve));
},
getDbStoreDynamic: function (dbName) {
return new Promise((resolve, reject) => {
Dexie.getDatabaseNames().then(dbNames => {
if (dbNames.find(n => n === dbName) === undefined) {
console.error("Database not found");
reject("No database");
} else {
var db = new Dexie(dbName);

db.open().then(db => {
const dbStore = {
name: db.name,
version: db.verno,
storeSchemas: db.tables.map(table => {
return {
name: table.name,
primaryKey: table.schema.primKey.name,
primaryKeyAuto: table.schema.primKey.auto,
uniqueIndexes: table.schema.indexes
.filter(index => index.unique)
.map(index => index.name),
indexes: table.schema.indexes
.filter(index => !index.unique)
.map(index => index.name)
};
}),
dynamic: true
};
// Problem: when we create a dynamic db, we make it version 1 in createDb() below.
// However, if we close/exit our session without adding any store schemas,
// the next time we call db.open() on this dynamic db, the reported verno is 0,
// rather than 1. Yet browser devtools DOES report the correct version.
// Also, calling indexedDB directly will also report correct version.
// (note, for unrelated reasons, Dexie version is the indexedDB version / 10,
// see https://github.com/dfahlander/Dexie.js/issues/59)
// To fix: when we come across verno = 0, change the version number in dbStore to 1.
// dbStore needs correct version so we can increment it when a store schema is added.
// This is not ideal; need to understand why Dexie reports verno 0 instead of 1.
if (db.verno === 0) {
dbStore.version = 1;
indexedDB.databases().then(databases => {
console.warn(`Dexie reports version of ${db.name} is ${db.verno}, but indexedDB reports version is ${databases.find(db1 => db1.name === db.name).version / 10}. BlazorDB will use indexedDB version number.`);
});
}
window.blazorDB.databases.push({
name: dbName,
db: db
});
resolve(dbStore);
})
}
})
});
},
addSchema: function (dotnetReference, transaction, dbStore, newStoreSchema) {
if (!dbStore.dynamic || window.blazorDB.databases.find(d => d.name === dbStore.name) === undefined) {
console.error(`Blazor.IndexedDB.Framework - Dynamic database ${dbStore.name} not found`);
dotnetReference.invokeMethodAsync('BlazorDBCallback', transaction, true, `Dynamic database ${dbStore.name} could not be found`);
return;
}
var db = window.blazorDB.databases.find(d => d.name === dbStore.name).db
if (db.tables.map(table => table.name).find(name => name === newStoreSchema.name) !== undefined) {
console.error(`Blazor.IndexedDB.Framework - schema for store ${newStoreSchema.name} already exists`);
dotnetReference.invokeMethodAsync('BlazorDBCallback', transaction, true, 'Schema already exists');
return;
}
db.close();

dbStore.storeSchemas.push(newStoreSchema);
this.createDb(dotnetReference, transaction, dbStore);
},
createDb: function (dotnetReference, transaction, dbStore) {
if(!dbStore.dynamic && window.blazorDB.databases.find(d => d.name === dbStore.name) !== undefined)
console.warn("Blazor.IndexedDB.Framework - Database already exists");

var db = new Dexie(dbStore.name);
Expand Down Expand Up @@ -32,6 +106,9 @@ window.blazorDB = {

stores[schema.name] = def;
}
if (dbStore.dynamic) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think belongs in createDb. Shouldn't it be handled in C# or in the addSchema function?

dbStore.version = dbStore.version + 1;
}
db.version(dbStore.version).stores(stores);
if(window.blazorDB.databases.find(d => d.name == dbStore.name) !== undefined) {
window.blazorDB.databases.find(d => d.name == dbStore.name).db = db;
Expand All @@ -41,7 +118,9 @@ window.blazorDB = {
db: db
});
}
db.open().then(_ => {
db.open().then(db => {
if (dbStore.dynamic)
window.blazorDB.databases.find(d => d.name === dbStore.name).db = db;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this already happen on L113/L114?

dotnetReference.invokeMethodAsync('BlazorDBCallback', transaction, false, 'Database opened');
}).catch(e => {
console.error(e);
Expand Down