b-straub / DexieNET

DexieNET is a .NET wrapper for dexie.js minimalist wrapper for IndexedDB
Apache License 2.0
44 stars 3 forks source link

DexieNET

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 Rider logo. Support for Open-Source Projects !

News


Basic

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.

Hello World

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();
        }
    }
}

Advanced

Naming

Transactions

Samples

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.