DexieCloudNET is a .NET wrapper for dexie.js minimalist wrapper for IndexedDB see https://dexie.org with cloud support see https://dexie.org/cloud/ .
'DexieNET' used with permission of David Fahlander
and made with Support for Open-Source Projects !
DexieNET aims to be a feature complete .NET wrapper for Dexie.js the famous Javascript IndexedDB wrapper from David Fahlander including support for cloud sync.
I consists of two parts, a source generator converting a C# record, class, struct to a DB store and a set of wrappers around the well known Dexie.js API constructs such as Table, WhereClause, Collection, ...
It's designed to work within a Blazor Webassembly application with minimal effort.
Program.cs
using DexieNET;
using YourNamspace.Pages;
....
builder.Services.AddDexieNET<FriendsDB>();
HelloWorld.razor
@page "/helloWorld"
@using DexieNET.Component
@inherits DexieNET<FriendsDB>
Friends:
@if (_friends is null)
{
<p>Loading...</p>
}
else if (_friends.Count() == 0)
{
<p>No items...</p>
}
else
{
<ul style="list-style: square inside;">
@foreach (var friend in _friends)
{
<li>
Name: @friend.Name, Age: @friend.Age
</li>
}
</ul>
}
<hr />
Logs:
@if (_logs is null)
{
<p>Loading...</p>
}
else if (_logs.Count() == 0)
{
<p>No items...</p>
}
else
{
<ul style="list-style: square inside;">
@foreach (var logEntry in _logs)
{
<li>
Message: @logEntry.Message, TimeStamp: @logEntry.TimeStamp.ToLongTimeString();
</li>
}
</ul>
}
<hr />
<div style="display: flex; column-gap: 50px">
<button class="btn btn-primary" style="flex: 0 1 auto" @onclick="PopulateDatabase">
PopulateDatabase
</button>
<button class="btn btn-secondary" style="flex: 0 1 auto" @onclick="GoodTransaction">
GoodTransaction
</button>
<button class="btn btn-secondary" style="flex: 0 1 auto" @onclick="FailedTransaction">
FailedTransaction
</button>
<button class="btn btn-secondary" style="flex: 0 1 auto" @onclick="ClearDatabase">
ClearDatabase
</button>
</div>
HelloWorld.razor.cs
using DexieNET;
using System.ComponentModel.DataAnnotations;
using System.Runtime.InteropServices;
using System.Xml.Linq;
namespace DexieNETHelloWorld.Pages
{
public interface IFriendsDB : IDBStore { };
public partial record Friend
(
[property: Index] string Name,
[property: Index] int Age
) : IFriendsDB;
public partial record LogEntry
(
[property: Index] string? Message,
[property: Index] DateTime TimeStamp
) : IFriendsDB;
public partial class HelloWorld
{
private IEnumerable<Friend>? _friends;
private IEnumerable<LogEntry>? _logs;
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
await Dexie.Version(1).Stores();
await FillTables();
}
private async Task FillTables()
{
_friends = await Dexie.Friends().ToArray();
_logs = await Dexie.LogEntries().OrderBy(l => l.TimeStamp).Reverse().ToArray();
await InvokeAsync(StateHasChanged);
}
private async Task LogMessage(string? message)
{
await Dexie.Transaction(async _ =>
{
await Dexie.LogEntries().Add(new LogEntry(message, DateTime.Now));
}, TAType.TopLevel);
}
private async Task ClearDatabase()
{
await Dexie.Friends().Clear();
await Dexie.LogEntries().Clear();
await FillTables();
}
private async Task PopulateDatabase()
{
await LogMessage("PopulateDatabase");
Random rand = new();
await Dexie.Friends().Add(new Friend("Jane Doe", rand.Next(1, 99)));
await Dexie.Friends().Add(new Friend("John Doe", rand.Next(1, 99)));
await FillTables();
}
private async Task GoodTransaction()
{
await LogMessage("GoodTransaction");
await Dexie.Transaction(async ta =>
{
Random rand = new();
var key = await Dexie.Friends().Add(new Friend("Luke", rand.Next(1, 99)));
var friend = await Dexie.Friends().Get(key);
if (friend?.Name == "Luke" || ta.Collecting)
// ta.Collecting, this means the first pass of the transaction, in which the table names are collected
// if a second table is hidden behind a conditional statement, it must also be visited in the first pass
{
await Dexie.Friends().Add(new Friend("John", rand.Next(1, 99)));
await Dexie.LogEntries().Add(new LogEntry("TA executed", DateTime.Now));
}
});
await FillTables();
}
private async Task FailedTransaction()
{
await LogMessage("ProvokeFail");
try
{
await Dexie.Transaction(async ta =>
{
await Dexie.Friends().Clear();
var key = await Dexie.Friends().Add(new Friend("Test", 33));
var friend = await Dexie.Friends().Get(key);
if (friend?.Name == "Test" || ta.Collecting)
{
await LogMessage("TA will fail");
}
await Dexie.Friends().Add(friend); // this will fail
});
}
catch (Exception ex)
{
var firstDot = ex.Message.IndexOf('.');
var message = firstDot <= 0 ? ex.Message : ex.Message[..firstDot];
await LogMessage($"TA failed: {message}");
}
await FillTables();
}
}
}
the Source Generator will create the following classes from an IDBStore derived class, struct, record:
// Record
public partial record Friend
(
[property: Index] string Name,
[property: Index] int Age
) : IDBStore;
......
// Service
builder.Services.AddDexieNET<FriendsDB>();
......
[Inject]
public IDexieNETService<FriendsDB>? DB { get; set; }
......
// Table
var table = await DB.Friends();
You can have multiple stores in one database
[DBName("TestDB")] // optional -> default name = interface name without leading 'I' -> PersonsDB
public interface IPersonsDB : IDBStore
{
}
// Records
[CompoundIndex("FirstName", "LastName")]
public partial record Person
(
[property: Index] string FirstName,
[property: Index] string LastName,
Guid? AddressKey
) : IPersonsDB;
[CompoundIndex("City", "Street")]
[CompoundIndex("Zip", "Street")]
public partial record Address
(
[property: Index] string Street,
[property: Index] string Housenumber,
[property: Index] string City,
[property: Index] string ZIP,
[property: Index] string Country
) : IPersonsDB;
......
// Service
using DexieNET;
.......
builder.Services.AddDexieNET<TestDB>();
// Component
[Inject]
public IDexieNETService<TestDB>? TestDB { get; set; }
......
// Table
var persons = await TestDB.Persons();
var addresses = await TestDB.Addresses();
The tests from TestCases will cover all possible DexieNET Api calls. Those calls are as close as possible modelled after the original Dexie.js API.