diff --git a/.github/workflows/publish-nuget.yml b/.github/workflows/publish-nuget.yml
new file mode 100644
index 0000000..a5e088d
--- /dev/null
+++ b/.github/workflows/publish-nuget.yml
@@ -0,0 +1,109 @@
+name: Publish NuGet
+
+on:
+ push:
+ branches:
+ - release
+
+concurrency:
+ group: publish-nuget-release
+ cancel-in-progress: false
+
+permissions:
+ contents: read
+
+env:
+ DOTNET_CLI_TELEMETRY_OPTOUT: true
+ DOTNET_NOLOGO: true
+
+jobs:
+ core-validation:
+ uses: ./.github/workflows/validate-dotnet.yml
+ permissions:
+ contents: read
+
+ browser-validation:
+ uses: ./.github/workflows/validate-browsers.yml
+ permissions:
+ contents: read
+
+ publish:
+ name: Build and publish package
+ needs:
+ - core-validation
+ - browser-validation
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ permissions:
+ contents: read
+ id-token: write
+
+ steps:
+ - name: Check out release commit
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
+ with:
+ persist-credentials: false
+
+ - name: Set up .NET 10
+ uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6
+ with:
+ dotnet-version: 10.0.x
+
+ - name: Calculate package version
+ id: version
+ shell: bash
+ run: |
+ current_version="$(sed -nE 's:.*([^<]+).*:\1:p' Magic.IndexedDb/Magic.IndexedDb.csproj)"
+
+ if [[ ! "${current_version}" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
+ echo "Expected a stable major.minor.patch Version in Magic.IndexedDb.csproj, found '${current_version}'." >&2
+ exit 1
+ fi
+
+ major="${BASH_REMATCH[1]}"
+ minor="${BASH_REMATCH[2]}"
+ patch="${BASH_REMATCH[3]}"
+ next_patch="$((10#${patch} + GITHUB_RUN_NUMBER))"
+ package_version="${major}.${minor}.${next_patch}"
+
+ echo "package_version=${package_version}" >> "${GITHUB_OUTPUT}"
+ echo "Publishing Magic.IndexedDb ${package_version} from ${GITHUB_SHA}."
+
+ - name: Restore package project
+ run: dotnet restore Magic.IndexedDb/Magic.IndexedDb.csproj
+
+ - name: Pack
+ run: >-
+ dotnet pack Magic.IndexedDb/Magic.IndexedDb.csproj
+ --configuration Release
+ --no-restore
+ --output artifacts
+ -p:Version=${{ steps.version.outputs.package_version }}
+ -p:PackageVersion=${{ steps.version.outputs.package_version }}
+ -p:ContinuousIntegrationBuild=true
+
+ - name: Upload package artifact
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
+ with:
+ name: Magic.IndexedDb-${{ steps.version.outputs.package_version }}
+ path: artifacts/*.nupkg
+ if-no-files-found: error
+ overwrite: true
+
+ - name: Request temporary NuGet API key
+ id: login
+ uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 # v1
+ with:
+ user: ${{ secrets.NUGET_USER }}
+
+ - name: Publish to NuGet.org
+ run: >-
+ dotnet nuget push artifacts/*.nupkg
+ --api-key "${{ steps.login.outputs.NUGET_API_KEY }}"
+ --source https://api.nuget.org/v3/index.json
+ --skip-duplicate
+
+ - name: Write release summary
+ run: |
+ echo "## Published Magic.IndexedDb ${{ steps.version.outputs.package_version }}" >> "${GITHUB_STEP_SUMMARY}"
+ echo "Source commit: \`${GITHUB_SHA}\`" >> "${GITHUB_STEP_SUMMARY}"
diff --git a/.github/workflows/validate-browsers.yml b/.github/workflows/validate-browsers.yml
new file mode 100644
index 0000000..5caec5d
--- /dev/null
+++ b/.github/workflows/validate-browsers.yml
@@ -0,0 +1,147 @@
+name: Browser validation
+
+on:
+ workflow_call:
+ pull_request:
+ push:
+ branches:
+ - master
+ workflow_dispatch:
+
+concurrency:
+ group: browser-validation-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+env:
+ DOTNET_CLI_TELEMETRY_OPTOUT: true
+ DOTNET_NOLOGO: true
+
+jobs:
+ browser-validation:
+ name: ${{ matrix.name }}
+ timeout-minutes: 35
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - name: Chrome integration
+ os: ubuntu-latest
+ browser: chromium
+ - name: Firefox integration
+ os: ubuntu-latest
+ browser: firefox
+ - name: Linux WebKit integration
+ os: ubuntu-latest
+ browser: webkit
+ - name: macOS WebKit integration
+ os: macos-15
+ browser: webkit
+ runs-on: ${{ matrix.os }}
+
+ steps:
+ - name: Check out source
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
+ with:
+ persist-credentials: false
+
+ - name: Set up .NET 10
+ uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6
+ with:
+ dotnet-version: 10.0.x
+
+ - name: Restore browser tests
+ run: dotnet restore E2eTest/E2eTest.csproj
+
+ - name: Build browser tests
+ run: dotnet build E2eTest/E2eTest.csproj --configuration Release --no-restore
+
+ - name: Install browser and Linux dependencies
+ if: ${{ runner.os == 'Linux' }}
+ shell: pwsh
+ run: E2eTest/bin/Release/net10.0/playwright.ps1 install --with-deps ${{ matrix.browser }}
+
+ - name: Install browser on macOS
+ if: ${{ runner.os == 'macOS' }}
+ shell: pwsh
+ run: E2eTest/bin/Release/net10.0/playwright.ps1 install ${{ matrix.browser }}
+
+ - name: Run browser integration tests
+ if: ${{ runner.os == 'Linux' }}
+ run: >-
+ dotnet test E2eTest/E2eTest.csproj
+ --configuration Release
+ --no-build
+ --no-restore
+ --logger "trx;LogFileName=${{ matrix.browser }}-${{ runner.os }}.trx"
+ --results-directory artifacts/test-results
+ --
+ Playwright.BrowserName=${{ matrix.browser }}
+ Playwright.LaunchOptions.Headless=true
+
+ - name: Run macOS WebKit open and registration tests
+ if: ${{ runner.os == 'macOS' }}
+ run: >-
+ dotnet test E2eTest/E2eTest.csproj
+ --configuration Release
+ --no-build
+ --no-restore
+ --filter "FullyQualifiedName~E2eTest.OpenTest"
+ --logger "trx;LogFileName=webkit-macOS-open.trx"
+ --results-directory artifacts/test-results
+ --
+ Playwright.BrowserName=webkit
+ Playwright.LaunchOptions.Headless=true
+
+ - name: Run macOS WebKit CRUD and streaming tests
+ if: ${{ runner.os == 'macOS' }}
+ run: >-
+ dotnet test E2eTest/E2eTest.csproj
+ --configuration Release
+ --no-build
+ --no-restore
+ --filter "FullyQualifiedName~E2eTest.SingleRecordBasicTest"
+ --logger "trx;LogFileName=webkit-macOS-crud-streaming.trx"
+ --results-directory artifacts/test-results
+ --
+ Playwright.BrowserName=webkit
+ Playwright.LaunchOptions.Headless=true
+
+ - name: Run macOS WebKit query tests
+ if: ${{ runner.os == 'macOS' }}
+ run: >-
+ dotnet test E2eTest/E2eTest.csproj
+ --configuration Release
+ --no-build
+ --no-restore
+ --filter "FullyQualifiedName~E2eTest.WhereTest"
+ --logger "trx;LogFileName=webkit-macOS-query.trx"
+ --results-directory artifacts/test-results
+ --
+ Playwright.BrowserName=webkit
+ Playwright.LaunchOptions.Headless=true
+
+ - name: Run macOS WebKit cursor tests
+ if: ${{ runner.os == 'macOS' }}
+ run: >-
+ dotnet test E2eTest/E2eTest.csproj
+ --configuration Release
+ --no-build
+ --no-restore
+ --filter "FullyQualifiedName~E2eTest.CursorTest"
+ --logger "trx;LogFileName=webkit-macOS-cursor.trx"
+ --results-directory artifacts/test-results
+ --
+ Playwright.BrowserName=webkit
+ Playwright.LaunchOptions.Headless=true
+
+ - name: Upload browser test results
+ if: ${{ always() }}
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
+ with:
+ name: browser-${{ matrix.browser }}-${{ runner.os }}-${{ github.run_attempt }}
+ path: artifacts/test-results/*.trx
+ if-no-files-found: warn
+ overwrite: true
diff --git a/.github/workflows/validate-dotnet.yml b/.github/workflows/validate-dotnet.yml
new file mode 100644
index 0000000..027f6a8
--- /dev/null
+++ b/.github/workflows/validate-dotnet.yml
@@ -0,0 +1,78 @@
+name: Core validation
+
+on:
+ workflow_call:
+ pull_request:
+ push:
+ branches:
+ - master
+ workflow_dispatch:
+
+concurrency:
+ group: core-validation-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+env:
+ DOTNET_CLI_TELEMETRY_OPTOUT: true
+ DOTNET_NOLOGO: true
+
+jobs:
+ core-validation:
+ name: Core validation
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+
+ steps:
+ - name: Check out source
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
+ with:
+ persist-credentials: false
+
+ - name: Set up .NET 10
+ uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6
+ with:
+ dotnet-version: 10.0.x
+
+ - name: Restore unit tests
+ run: dotnet restore Magic.IndexedDb.UnitTests/Magic.IndexedDb.UnitTests.csproj
+
+ - name: Run unit and contract tests
+ run: >-
+ dotnet test Magic.IndexedDb.UnitTests/Magic.IndexedDb.UnitTests.csproj
+ --configuration Release
+ --no-restore
+ --logger "trx;LogFileName=core-validation.trx"
+ --results-directory artifacts/test-results
+
+ - name: Pack the NuGet artifact
+ run: >-
+ dotnet pack Magic.IndexedDb/Magic.IndexedDb.csproj
+ --configuration Release
+ --no-restore
+ --output artifacts/package
+ -p:ContinuousIntegrationBuild=true
+ -p:GeneratePackageOnBuild=false
+
+ - name: Verify package contents
+ shell: bash
+ run: |
+ package="$(find artifacts/package -maxdepth 1 -name '*.nupkg' -print -quit)"
+ test -n "${package}"
+ unzip -Z1 "${package}" | grep -Fx README.md
+ unzip -Z1 "${package}" | grep -Fx LICENSE.txt
+ unzip -Z1 "${package}" | grep -Fx wizardHatIcon.png
+ unzip -Z1 "${package}" | grep -F 'staticwebassets/'
+
+ - name: Upload test results and package
+ if: ${{ always() }}
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
+ with:
+ name: core-validation-${{ github.run_attempt }}
+ path: |
+ artifacts/test-results/*.trx
+ artifacts/package/*.nupkg
+ if-no-files-found: warn
+ overwrite: true
diff --git a/E2eTest/Extensions/PageExtensions.cs b/E2eTest/Extensions/PageExtensions.cs
index a9e3d88..a00bb8e 100644
--- a/E2eTest/Extensions/PageExtensions.cs
+++ b/E2eTest/Extensions/PageExtensions.cs
@@ -1,5 +1,4 @@
-using E2eTest.Entities;
-using Microsoft.Playwright;
+using Microsoft.Playwright;
namespace E2eTest.Extensions;
internal static class PageExtensions
@@ -14,7 +13,5 @@ public static async ValueTask DeleteDatabaseAsync(this IPage page, string databa
request.onblocked = () => reject(new Error(`Deletion of ${database} was blocked.`));
})
""", database);
- var databases = await page.EvaluateAsync("indexedDB.databases()");
- Assert.IsFalse(databases!.Any(x => x.Name == database));
}
}
diff --git a/E2eTest/OpenTest.cs b/E2eTest/OpenTest.cs
index b3b0984..bbe73b0 100644
--- a/E2eTest/OpenTest.cs
+++ b/E2eTest/OpenTest.cs
@@ -1,5 +1,4 @@
-using E2eTest.Entities;
-using E2eTest.Extensions;
+using E2eTest.Extensions;
using TestBase.Models;
using E2eTestWebApp.TestPages;
using Magic.IndexedDb;
@@ -19,10 +18,21 @@ public async Task DirectOpenTest()
var result = await this.RunTestPageMethodAsync(p => p.DirectOpen);
Assert.AreEqual("OK", result);
- var databases = await page.EvaluateAsync("indexedDB.databases()");
+ var version = await page.EvaluateAsync("""
+ database => new Promise((resolve, reject) => {
+ const request = indexedDB.open(database);
+ request.onsuccess = () => {
+ const version = request.result.version;
+ request.result.close();
+ resolve(version);
+ };
+ request.onerror = () => reject(request.error);
+ request.onblocked = () => reject(new Error(`Opening ${database} was blocked.`));
+ })
+ """, "Employee");
// The actual version will be 10:
// https://dexie.org/docs/Dexie/Dexie.version()
- Assert.IsTrue(databases!.Any(x => x.Name == "Employee" && x.Version == 10));
+ Assert.AreEqual(10, version);
}
diff --git a/E2eTest/Program.cs b/E2eTest/Program.cs
index c9dd55b..940c9b2 100644
--- a/E2eTest/Program.cs
+++ b/E2eTest/Program.cs
@@ -1,4 +1,5 @@
-using System.Diagnostics;
+using System.Collections.Concurrent;
+using System.Diagnostics;
namespace E2eTest;
@@ -19,24 +20,36 @@ public static async Task InitializeAsync(TestContext context)
return;
using var currentProcess = Process.GetCurrentProcess();
- var dotnetRunArguments = "--no-build --project ../../../../E2eTestWebApp";
+ var appDll = Path.Combine(AppContext.BaseDirectory, "E2eTestWebApp.dll");
+ var appContentRoot = Path.GetFullPath(
+ Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "E2eTestWebApp"));
var webAppArguments = $"--E2eTest {currentProcess.Id}";
+ var output = new ConcurrentQueue();
+ var errors = new ConcurrentQueue();
var server = new Process
{
StartInfo = new ProcessStartInfo()
{
FileName = "dotnet",
- Arguments = $"run {dotnetRunArguments} -- {webAppArguments}",
+ Arguments = $"\"{appDll}\" {webAppArguments} --urls http://127.0.0.1:0 --contentRoot \"{appContentRoot}\"",
+ WorkingDirectory = appContentRoot,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true
}
};
+ server.ErrorDataReceived += (_, eventArgs) =>
+ {
+ if (eventArgs.Data is not null)
+ errors.Enqueue(eventArgs.Data);
+ };
+ server.StartInfo.Environment["ASPNETCORE_ENVIRONMENT"] = "Development";
try
{
if (!server.Start())
throw new Exception("Failed to start E2eTestWebApp. Process.Start returns false.");
+ server.BeginErrorReadLine();
}
catch
{
@@ -46,7 +59,6 @@ public static async Task InitializeAsync(TestContext context)
try
{
- var lines = new List();
for (; ; )
{
var line = await server.StandardOutput.ReadLineAsync();
@@ -55,15 +67,18 @@ public static async Task InitializeAsync(TestContext context)
throw new Exception(
$"Failed to start E2eTestWebApp. The output stream ended accidentally.{Environment.NewLine}" +
$"The previous message is:{Environment.NewLine}" +
- string.Join(Environment.NewLine, lines));
+ string.Join(Environment.NewLine, output) + Environment.NewLine +
+ $"Standard error:{Environment.NewLine}" +
+ string.Join(Environment.NewLine, errors));
}
- lines.Add(line);
+ EnqueueRecent(output, line);
line = line.TrimStart();
if (line.StartsWith("Now listening on: http://"))
{
BaseUrl = line.Substring("Now listening on: ".Length).TrimEnd();
Program.server = server;
+ _ = DrainOutputAsync(server.StandardOutput, output);
return;
}
}
@@ -77,6 +92,26 @@ public static async Task InitializeAsync(TestContext context)
}
}
+ private static async Task DrainOutputAsync(StreamReader reader, ConcurrentQueue output)
+ {
+ try
+ {
+ while (await reader.ReadLineAsync() is { } line)
+ EnqueueRecent(output, line);
+ }
+ catch (Exception exception) when (exception is ObjectDisposedException or IOException)
+ {
+ // Assembly cleanup owns the process and its redirected streams.
+ }
+ }
+
+ private static void EnqueueRecent(ConcurrentQueue output, string line)
+ {
+ output.Enqueue(line);
+ while (output.Count > 200)
+ output.TryDequeue(out _);
+ }
+
[AssemblyCleanup]
public static void Cleanup()
{
diff --git a/E2eTest/SingleRecordBasicTest.cs b/E2eTest/SingleRecordBasicTest.cs
index 03b0650..3b2931e 100644
--- a/E2eTest/SingleRecordBasicTest.cs
+++ b/E2eTest/SingleRecordBasicTest.cs
@@ -47,4 +47,73 @@ public async Task YieldAllTest()
var result = await this.RunTestPageMethodAsync(p => p.YieldAll);
Assert.AreEqual("OK", result);
}
+
+ [TestMethod]
+ public async Task DictionaryPropertyRoundTripTest()
+ {
+ var result = await this.RunTestPageMethodAsync(p => p.DictionaryPropertyRoundTrip);
+ Assert.AreEqual("OK", result);
+ }
+
+ [TestMethod]
+ public async Task NumericEnumWhereTest()
+ {
+ var result = await this.RunTestPageMethodAsync(p => p.NumericEnumWhere);
+ Assert.AreEqual("OK", result);
+ }
+
+ [TestMethod]
+ public async Task NamedEnumWhereTest()
+ {
+ var result = await this.RunTestPageMethodAsync(p => p.NamedEnumWhere);
+ Assert.AreEqual("OK", result);
+ }
+
+ [TestMethod]
+ public async Task RangeCrudTest() =>
+ Assert.AreEqual("OK", await this.RunTestPageMethodAsync(page => page.RangeCrud));
+
+ [TestMethod]
+ public async Task ClearAndPopulatedCountTest() =>
+ Assert.AreEqual("OK", await this.RunTestPageMethodAsync(page => page.ClearAndPopulatedCount));
+
+ [TestMethod]
+ public async Task UniqueConstraintFailureIsRecoverableTest() =>
+ Assert.AreEqual("OK", await this.RunTestPageMethodAsync(page => page.UniqueConstraintFailureIsRecoverable));
+
+ [TestMethod]
+ public async Task DatabaseLifecycleTest() =>
+ Assert.AreEqual("OK", await this.RunTestPageMethodAsync(page => page.DatabaseLifecycle));
+
+ [TestMethod]
+ public async Task MultipleDatabaseIsolationTest() =>
+ Assert.AreEqual("OK", await this.RunTestPageMethodAsync(page => page.MultipleDatabaseIsolation));
+
+ [TestMethod]
+ public async Task ExactMaterializedOrderingTest() =>
+ Assert.AreEqual("OK", await this.RunTestPageMethodAsync(page => page.ExactMaterializedOrdering));
+
+ [TestMethod]
+ public async Task InMemoryWhereAfterPaginationTest() =>
+ Assert.AreEqual("OK", await this.RunTestPageMethodAsync(page => page.InMemoryWhereAfterPagination));
+
+ [TestMethod]
+ public async Task CompoundKeyCrudAndQueryTest() =>
+ Assert.AreEqual("OK", await this.RunTestPageMethodAsync(page => page.CompoundKeyCrudAndQuery));
+
+ [TestMethod]
+ public async Task LargeUnicodeStreamTest() =>
+ Assert.AreEqual("OK", await this.RunTestPageMethodAsync(page => page.LargeUnicodeStream));
+
+ [TestMethod]
+ public async Task StreamCancellationAndRecoveryTest() =>
+ Assert.AreEqual("OK", await this.RunTestPageMethodAsync(page => page.StreamCancellationAndRecovery));
+
+ [TestMethod]
+ public async Task ConcurrentStreamsRemainIsolatedTest() =>
+ Assert.AreEqual("OK", await this.RunTestPageMethodAsync(page => page.ConcurrentStreamsRemainIsolated));
+
+ [TestMethod]
+ public async Task StorageEstimateTest() =>
+ Assert.AreEqual("OK", await this.RunTestPageMethodAsync(page => page.StorageEstimate));
}
diff --git a/E2eTestWebApp/Program.cs b/E2eTestWebApp/Program.cs
index 1197ba5..df878eb 100644
--- a/E2eTestWebApp/Program.cs
+++ b/E2eTestWebApp/Program.cs
@@ -16,7 +16,8 @@ public static async Task Main(string[] args)
var builder = WebApplication.CreateBuilder(args);
_ = builder.Services.AddRazorComponents().AddInteractiveServerComponents();
- _ = builder.Services.AddMagicBlazorDB(BlazorInteropMode.WASM, true);
+ // Deliberately tiny in tests so every streamed entity exercises multi-chunk transport.
+ _ = builder.Services.AddMagicBlazorDB(64, true);
_ = builder.Services.AddSingleton(new DbStore()
{
Name = "OpenTest.RegisteredOpen2",
diff --git a/E2eTestWebApp/TestPages/CursorTestPage.cs b/E2eTestWebApp/TestPages/CursorTestPage.cs
index f7fd95b..253c9e2 100644
--- a/E2eTestWebApp/TestPages/CursorTestPage.cs
+++ b/E2eTestWebApp/TestPages/CursorTestPage.cs
@@ -50,14 +50,14 @@ public async Task TestWhere48() {
public async Task TestWhere54() {
var result = RunTest("Take & With Index Test", await (await SetupData()).Cursor(x => x.Name.StartsWith("J"))
.OrderBy(x => x.Name).Take(2).ToListAsync(),
- PersonData.persons.Where(x => x.Name.StartsWith("J")).OrderBy(x => x.Name).ThenBy(x => x._Id).Take(2));
+ PersonData.persons.Where(x => x.Name.StartsWith("J")).OrderBy(x => x.Name).ThenBy(x => x._Id).Take(2), ordered: true);
return result.Success ? "OK" : result.Message;
}
public async Task TestWhere55() {
var result = RunTest("TakeLast & With Index Test", await (await SetupData()).Cursor(x => x.Name.StartsWith("J"))
.OrderBy(x => x.Name).TakeLast(2).ToListAsync(),
- PersonData.persons.Where(x => x.Name.StartsWith("J")).OrderBy(x => x.Name).ThenBy(x => x._Id).TakeLast(2));
+ PersonData.persons.Where(x => x.Name.StartsWith("J")).OrderBy(x => x.Name).ThenBy(x => x._Id).TakeLast(2), ordered: true);
return result.Success ? "OK" : result.Message;
}
@@ -81,7 +81,7 @@ public async Task TestWhere69() {
.Take(3)
.Skip(2)
.ToListAsync(),
- PersonData.persons.Where(x => x._Age > 30).OrderBy(x => x._Age).ThenBy(x => x._Id).Skip(2).Take(3));
+ PersonData.persons.Where(x => x._Age > 30).OrderBy(x => x._Age).ThenBy(x => x._Id).Skip(2).Take(3), ordered: true);
return result.Success ? "OK" : result.Message;
}
@@ -114,7 +114,7 @@ public async Task TestWhere72() {
.Take(3)
.Skip(2)
.ToListAsync(),
- PersonData.persons.Where(x => x.Name.StartsWith("J")).OrderBy(x => x._Id).ThenBy(x => x._Id).Skip(2).Take(3));
+ PersonData.persons.Where(x => x.Name.StartsWith("J")).OrderBy(x => x._Id).ThenBy(x => x._Id).Skip(2).Take(3), ordered: true);
return result.Success ? "OK" : result.Message;
}
@@ -125,7 +125,7 @@ public async Task TestWhere73() {
.Take(3)
.Skip(2)
.ToListAsync(),
- PersonData.persons.Where(x => x.Name.StartsWith("J")).OrderByDescending(x => x._Id).ThenByDescending(x => x._Id).Skip(2).Take(3));
+ PersonData.persons.Where(x => x.Name.StartsWith("J")).OrderByDescending(x => x._Id).ThenByDescending(x => x._Id).Skip(2).Take(3), ordered: true);
return result.Success ? "OK" : result.Message;
}
@@ -135,7 +135,7 @@ public async Task TestWhere74() {
.OrderByDescending(x => x._Age)
.TakeLast(2)
.ToListAsync(),
- PersonData.persons.Where(x => x._Age < 60).OrderByDescending(x => x._Age).ThenByDescending(x => x._Id).TakeLast(2));
+ PersonData.persons.Where(x => x._Age < 60).OrderByDescending(x => x._Age).ThenByDescending(x => x._Id).TakeLast(2), ordered: true);
return result.Success ? "OK" : result.Message;
//await Task.Delay(10000);
@@ -154,7 +154,7 @@ public async Task TestWhere91() {
.OrderBy(x => x._Id)
.TakeLast(2)
.ToListAsync(),
- PersonData.persons.Where(x => x.TestInt > 2).OrderBy(x => x._Id).ThenBy(x => x._Id).TakeLast(2));
+ PersonData.persons.Where(x => x.TestInt > 2).OrderBy(x => x._Id).ThenBy(x => x._Id).TakeLast(2), ordered: true);
return result.Success ? "OK" : result.Message;
}
@@ -165,7 +165,7 @@ public async Task TestWhere92() {
.Take(3)
.Skip(1)
.ToListAsync(),
- PersonData.persons.Where(x => x.TestInt > 2 && x.TestInt == 9).OrderBy(x => x._Id).ThenBy(x => x._Id).Skip(1).Take(3));
+ PersonData.persons.Where(x => x.TestInt > 2 && x.TestInt == 9).OrderBy(x => x._Id).ThenBy(x => x._Id).Skip(1).Take(3), ordered: true);
return result.Success ? "OK" : result.Message;
@@ -178,7 +178,7 @@ public async Task TestWhere93() {
.Take(3)
.Skip(1)
.ToListAsync(),
- PersonData.persons.Where(x => x._Age > 30 && x.TestInt == 9).OrderBy(x => x._Age).ThenBy(x => x._Id).Skip(1).Take(3));
+ PersonData.persons.Where(x => x._Age > 30 && x.TestInt == 9).OrderBy(x => x._Age).ThenBy(x => x._Id).Skip(1).Take(3), ordered: true);
return result.Success ? "OK" : result.Message;
}
-}
\ No newline at end of file
+}
diff --git a/E2eTestWebApp/TestPages/SingleRecordBasicTestPage.cs b/E2eTestWebApp/TestPages/SingleRecordBasicTestPage.cs
index bac7fb6..eac2250 100644
--- a/E2eTestWebApp/TestPages/SingleRecordBasicTestPage.cs
+++ b/E2eTestWebApp/TestPages/SingleRecordBasicTestPage.cs
@@ -82,4 +82,298 @@ await db.AddRangeAsync([
return yielded.Count == 3 ? "OK" : "Incorrect";
}
+
+ public async Task DictionaryPropertyRoundTrip()
+ {
+ var db = await magic.Query();
+ await db.AddAsync(new ContractRecord
+ {
+ Name = "Dictionary",
+ Metadata = new Dictionary
+ {
+ ["count"] = 2,
+ ["enabled"] = false,
+ ["label"] = "value"
+ }
+ });
+
+ var record = (await db.ToListAsync()).Single();
+ return record.Metadata.Count == 3 &&
+ ((JsonElement)record.Metadata["count"]!).GetInt32() == 2 &&
+ !((JsonElement)record.Metadata["enabled"]!).GetBoolean() &&
+ ((JsonElement)record.Metadata["label"]!).GetString() == "value"
+ ? "OK"
+ : "Incorrect";
+ }
+
+ public async Task NumericEnumWhere()
+ {
+ var db = await magic.Query();
+ await db.AddRangeAsync([
+ new ContractRecord { Name = "Readable", NumericAccess = ContractRecord.NumericStatus.Read },
+ new ContractRecord { Name = "Writable", NumericAccess = ContractRecord.NumericStatus.Write }
+ ]);
+
+ var matches = await db
+ .Where(record => record.NumericAccess == ContractRecord.NumericStatus.Write)
+ .ToListAsync();
+
+ return matches.Count == 1 && matches[0].Name == "Writable"
+ ? "OK"
+ : "Incorrect";
+ }
+
+ public async Task NamedEnumWhere()
+ {
+ var db = await magic.Query();
+ await db.AddRangeAsync([
+ new ContractRecord { Name = "Inactive", NamedAccess = ContractRecord.NamedStatus.Inactive },
+ new ContractRecord { Name = "Active", NamedAccess = ContractRecord.NamedStatus.Active }
+ ]);
+
+ var matches = await db
+ .Where(record => record.NamedAccess == ContractRecord.NamedStatus.Active)
+ .ToListAsync();
+
+ return matches.Count == 1 &&
+ matches[0].Name == "Active" &&
+ matches[0].NamedAccess == ContractRecord.NamedStatus.Active
+ ? "OK"
+ : "Incorrect";
+ }
+
+ public async Task RangeCrud()
+ {
+ var db = await magic.Query();
+ Person[] records =
+ [
+ new() { _Id = 1, Name = "one", _Age = 10 },
+ new() { _Id = 2, Name = "two", _Age = 20 },
+ new() { _Id = 3, Name = "three", _Age = 30 }
+ ];
+ await db.AddRangeAsync(records);
+
+ records[0]._Age = 11;
+ records[2]._Age = 33;
+ var updated = await db.UpdateRangeAsync([records[0], records[2]]);
+ var afterUpdate = await db.OrderBy(person => person._Id).ToListAsync();
+ var deleted = await db.DeleteRangeAsync([records[0], records[1]]);
+ var remaining = await db.ToListAsync();
+
+ return updated == 2 && deleted == 2 &&
+ afterUpdate.Select(person => person._Age).SequenceEqual([11, 20, 33]) &&
+ remaining.Count == 1 && remaining[0]._Id == 3
+ ? "OK"
+ : "Incorrect";
+ }
+
+ public async Task ClearAndPopulatedCount()
+ {
+ var db = await magic.Query();
+ await db.AddRangeAsync([
+ new Person { _Id = 1, Name = "one" },
+ new Person { _Id = 2, Name = "two" }
+ ]);
+ var before = await db.CountAsync();
+ await db.ClearTable();
+ var after = await db.CountAsync();
+
+ return before == 2 && after == 0 && (await db.ToListAsync()).Count == 0
+ ? "OK"
+ : "Incorrect";
+ }
+
+ public async Task UniqueConstraintFailureIsRecoverable()
+ {
+ var db = await magic.Query();
+ var unique = Guid.NewGuid();
+ await db.AddAsync(new Person { _Id = 1, Name = "first", GUIY = unique });
+
+ var rejected = false;
+ try
+ {
+ await db.AddAsync(new Person { _Id = 2, Name = "duplicate", GUIY = unique });
+ }
+ catch
+ {
+ rejected = true;
+ }
+
+ await db.AddAsync(new Person { _Id = 3, Name = "after-error" });
+ var rows = await db.OrderBy(person => person._Id).ToListAsync();
+ return rejected && rows.Select(person => person._Id).SequenceEqual([1, 3])
+ ? "OK"
+ : "Incorrect";
+ }
+
+ public async Task DatabaseLifecycle()
+ {
+ var database = await magic.Database(TestBase.Repository.IndexDbContext.Animal);
+ var existsInitially = await database.DoesExistAsync();
+ var openInitially = await database.IsOpenAsync();
+ await database.CloseAsync();
+ var closed = !await database.IsOpenAsync();
+ var persistedAfterClose = await database.DoesExistAsync();
+ await database.OpenAsync();
+ var reopened = await database.IsOpenAsync();
+ await database.DeleteAsync();
+ var deleted = !await database.DoesExistAsync();
+
+ return existsInitially && openInitially && closed && persistedAfterClose && reopened && deleted
+ ? "OK"
+ : "Incorrect";
+ }
+
+ public async Task MultipleDatabaseIsolation()
+ {
+ var client = await magic.Query();
+ var employee = await magic.Query(person => person.Databases.Employee);
+ await client.AddAsync(new Person { _Id = 1, Name = "client" });
+ await employee.AddAsync(new Person { _Id = 2, Name = "employee" });
+
+ var clientRows = await client.ToListAsync();
+ var employeeRows = await employee.ToListAsync();
+ return clientRows.Count == 1 && clientRows[0].Name == "client" &&
+ employeeRows.Count == 1 && employeeRows[0].Name == "employee"
+ ? "OK"
+ : "Incorrect";
+ }
+
+ public async Task ExactMaterializedOrdering()
+ {
+ var db = await magic.Query();
+ await db.AddRangeAsync([
+ new Person { _Id = 1, Name = "oldest", _Age = 50 },
+ new Person { _Id = 2, Name = "youngest", _Age = 20 },
+ new Person { _Id = 3, Name = "middle", _Age = 30 }
+ ]);
+
+ var ascending = await db.OrderBy(person => person._Age).ToListAsync();
+ var descending = await db.OrderByDescending(person => person._Age).ToListAsync();
+ var ascendingIds = ascending.Select(person => person._Id).ToArray();
+ var descendingIds = descending.Select(person => person._Id).ToArray();
+ return ascendingIds.SequenceEqual([2, 3, 1]) && descendingIds.SequenceEqual([1, 3, 2])
+ ? "OK"
+ : $"Ascending: {string.Join(',', ascendingIds)}; descending: {string.Join(',', descendingIds)}";
+ }
+
+ public async Task InMemoryWhereAfterPagination()
+ {
+ var db = await magic.Query();
+ await db.AddRangeAsync(Enumerable.Range(1, 10)
+ .Select(value => new Person { _Id = value, Name = $"person-{value}", _Age = value }));
+
+ var rows = await db.OrderBy(person => person._Id)
+ .Take(6)
+ .WhereAsync(person => person._Id % 2 == 0);
+
+ return rows.Select(person => person._Id).SequenceEqual([2, 4, 6])
+ ? "OK"
+ : "Incorrect";
+ }
+
+ public async Task CompoundKeyCrudAndQuery()
+ {
+ var db = await magic.Query();
+ CompositeRecord[] rows =
+ [
+ new() { Tenant = "alpha", Sequence = 1, Category = "work", Value = "one" },
+ new() { Tenant = "alpha", Sequence = 2, Category = "work", Value = "two" },
+ new() { Tenant = "beta", Sequence = 1, Category = "home", Value = "three" }
+ ];
+ await db.AddRangeAsync(rows);
+
+ var matches = await db.Where(row => row.Tenant == "alpha" && row.Category == "work").ToListAsync();
+ rows[1].Value = "updated";
+ var updated = await db.UpdateAsync(rows[1]);
+ await db.DeleteAsync(rows[0]);
+ var remaining = await db.OrderBy(row => row.Sequence).ToListAsync();
+
+ return matches.Count == 2 && updated == 1 && remaining.Count == 2 &&
+ remaining.Any(row => row.Tenant == "alpha" && row.Sequence == 2 && row.Value == "updated") &&
+ remaining.All(row => row.Tenant != "alpha" || row.Sequence != 1)
+ ? "OK"
+ : "Incorrect";
+ }
+
+ public async Task LargeUnicodeStream()
+ {
+ var db = await magic.Query();
+ const string payload = "🧙♂️\n雪\t\\quoted\"";
+ await db.AddRangeAsync(Enumerable.Range(1, 64)
+ .Select(value => new Person
+ {
+ _Id = value,
+ Name = $"person-{value}",
+ Secret = string.Concat(Enumerable.Repeat(payload, 16))
+ }));
+
+ var streamed = new Dictionary();
+ await foreach (var person in db.AsAsyncEnumerable())
+ streamed.Add(person._Id, person.Secret);
+
+ return streamed.Count == 64 && streamed.Values.All(value => value == string.Concat(Enumerable.Repeat(payload, 16)))
+ ? "OK"
+ : "Incorrect";
+ }
+
+ public async Task StreamCancellationAndRecovery()
+ {
+ var db = await magic.Query();
+ await db.AddRangeAsync(Enumerable.Range(1, 50)
+ .Select(value => new Person { _Id = value, Name = $"person-{value}" }));
+
+ using var cancellation = new CancellationTokenSource();
+ cancellation.Cancel();
+ var canceled = false;
+ try
+ {
+ await foreach (var _ in db.AsAsyncEnumerable(cancellation.Token))
+ {
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ canceled = true;
+ }
+
+ return canceled && await db.CountAsync() == 50 ? "OK" : "Incorrect";
+ }
+
+ public async Task ConcurrentStreamsRemainIsolated()
+ {
+ var db = await magic.Query();
+ await db.AddRangeAsync(Enumerable.Range(1, 40)
+ .Select(value => new Person
+ {
+ _Id = value,
+ Name = $"person-{value}",
+ TestInt = value % 2
+ }));
+
+ static async Task> ReadIds(IAsyncEnumerable stream)
+ {
+ var values = new List();
+ await foreach (var item in stream)
+ values.Add(item._Id);
+ return values;
+ }
+
+ var evensTask = ReadIds(db.Where(person => person.TestInt == 0).AsAsyncEnumerable());
+ var oddsTask = ReadIds(db.Where(person => person.TestInt == 1).AsAsyncEnumerable());
+ await Task.WhenAll(evensTask, oddsTask);
+
+ return evensTask.Result.Count == 20 && oddsTask.Result.Count == 20 &&
+ evensTask.Result.All(id => id % 2 == 0) && oddsTask.Result.All(id => id % 2 == 1)
+ ? "OK"
+ : "Incorrect";
+ }
+
+ public async Task StorageEstimate()
+ {
+ var estimate = await magic.GetStorageEstimateAsync();
+ return estimate.Quota >= 0 && estimate.Usage >= 0 && estimate.Quota >= estimate.Usage
+ ? "OK"
+ : "Incorrect";
+ }
}
diff --git a/E2eTestWebApp/TestPages/TestPageBase.razor.cs b/E2eTestWebApp/TestPages/TestPageBase.razor.cs
index b46113a..935a819 100644
--- a/E2eTestWebApp/TestPages/TestPageBase.razor.cs
+++ b/E2eTestWebApp/TestPages/TestPageBase.razor.cs
@@ -41,8 +41,10 @@ private void Clear()
}
public TestResponse RunTest(string testName,
- IEnumerable indexDbResults, IEnumerable correctResults) where T : class
+ IEnumerable indexDbResults,
+ IEnumerable correctResults,
+ bool ordered = false) where T : class
{
- return TestValidator.ValidateLists(correctResults, indexDbResults);
+ return TestValidator.ValidateLists(correctResults, indexDbResults, ordered);
}
}
diff --git a/E2eTestWebApp/TestPages/WhereTestPage.cs b/E2eTestWebApp/TestPages/WhereTestPage.cs
index d1beb03..b15177f 100644
--- a/E2eTestWebApp/TestPages/WhereTestPage.cs
+++ b/E2eTestWebApp/TestPages/WhereTestPage.cs
@@ -340,13 +340,13 @@ public async Task TestWhere49() {
public async Task TestWhere50() {
var result = RunTest("Ordering Test", await (await SetupData()).OrderBy(x => x._Age).ToListAsync(),
- PersonData.persons.OrderBy(x => x._Age).ThenBy(x => x._Id));
+ PersonData.persons.OrderBy(x => x._Age).ThenBy(x => x._Id), ordered: true);
return result.Success ? "OK" : result.Message;
}
public async Task TestWhere51() {
var result = RunTest("Order Descending Test", await (await SetupData()).OrderByDescending(x => x._Age).ToListAsync(),
- PersonData.persons.OrderByDescending(x => x._Age).ThenByDescending(x => x._Id));
+ PersonData.persons.OrderByDescending(x => x._Age).ThenBy(x => x._Id), ordered: true);
return result.Success ? "OK" : result.Message;
var asdfsdfdsfsdf = await (await SetupData()).OrderBy(x => x._Age).Skip(3).ToListAsync();
@@ -354,13 +354,13 @@ public async Task TestWhere51() {
public async Task TestWhere52() {
var result = RunTest("Skip Test", await (await SetupData()).OrderBy(x => x._Age).Skip(3).ToListAsync(),
- PersonData.persons.OrderBy(x => x._Age).ThenBy(x => x._Id).Skip(3));
+ PersonData.persons.OrderBy(x => x._Age).ThenBy(x => x._Id).Skip(3), ordered: true);
return result.Success ? "OK" : result.Message;
}
public async Task TestWhere53() {
var result = RunTest("Take Test", await (await SetupData()).OrderBy(x => x._Age).Take(2).ToListAsync(),
- PersonData.persons.OrderBy(x => x._Age).ThenBy(x => x._Id).Take(2));
+ PersonData.persons.OrderBy(x => x._Age).ThenBy(x => x._Id).Take(2), ordered: true);
return result.Success ? "OK" : result.Message;
}
@@ -371,7 +371,7 @@ public async Task TestWhere56() {
* Take last is special operation that changes order,
* but this altered version replicates the LINQ to SQL desired result
*/
- PersonData.persons.OrderBy(x => x.Name).ThenBy(x => x._Id).Take(2));
+ PersonData.persons.OrderBy(x => x.Name).ThenBy(x => x._Id).Take(2), ordered: true);
return result.Success ? "OK" : result.Message;
}
@@ -382,7 +382,7 @@ public async Task TestWhere57() {
* Take last is special operation that changes order,
* but this altered version replicates the LINQ to SQL desired result
*/
- PersonData.persons.OrderBy(x => x.Name).ThenBy(x => x._Id).TakeLast(2));
+ PersonData.persons.OrderBy(x => x.Name).ThenBy(x => x._Id).TakeLast(2), ordered: true);
return result.Success ? "OK" : result.Message;
var totalPersons = await (await SetupData()).CountAsync();
@@ -430,7 +430,7 @@ public async Task TestWhere64() {
public async Task TestWhere66() {
var result = RunTest("TakeLast Cursor Test", await (await SetupData()).OrderBy(x => x._Age).TakeLast(2).ToListAsync(),
- PersonData.persons.OrderBy(x => x._Age).ThenBy(x => x._Id).TakeLast(2));
+ PersonData.persons.OrderBy(x => x._Age).ThenBy(x => x._Id).TakeLast(2), ordered: true);
return result.Success ? "OK" : result.Message;
}
@@ -538,7 +538,7 @@ public async Task TestWhere80() {
.Take(5)
.Skip(3)
.ToListAsync(),
- PersonData.persons.OrderBy(x => x._Age).ThenBy(x => x._Id).Skip(3).Take(5));
+ PersonData.persons.OrderBy(x => x._Age).ThenBy(x => x._Id).Skip(3).Take(5), ordered: true);
return result.Success ? "OK" : result.Message;
}
@@ -547,7 +547,7 @@ public async Task TestWhere81() {
await (await SetupData()).OrderByDescending(x => x._Age)
.TakeLast(5)
.ToListAsync(),
- PersonData.persons.OrderByDescending(x => x._Age).ThenByDescending(x => x._Id).TakeLast(5));
+ PersonData.persons.OrderByDescending(x => x._Age).ThenByDescending(x => x._Id).TakeLast(5), ordered: true);
return result.Success ? "OK" : result.Message;
}
@@ -614,4 +614,4 @@ public async Task TestWhere90() {
[]);
return result.Success ? "OK" : result.Message;
}
-}
\ No newline at end of file
+}
diff --git a/Magic.IndexedDb.UnitTests/ChunkProcessorTests.cs b/Magic.IndexedDb.UnitTests/ChunkProcessorTests.cs
index d08161c..8ea8048 100644
--- a/Magic.IndexedDb.UnitTests/ChunkProcessorTests.cs
+++ b/Magic.IndexedDb.UnitTests/ChunkProcessorTests.cs
@@ -28,4 +28,67 @@ public void Chunks_AreReassembledAndDrainedBeforeCompletion()
MagicJsChunkProcessor.RemoveInstance(instanceId);
}
}
+
+ [TestMethod]
+ public void CompletionMarker_WaitsForEveryCompletedItemToDrain()
+ {
+ var instanceId = Guid.NewGuid().ToString("N");
+ MagicJsChunkProcessor.RegisterInstance(instanceId);
+
+ try
+ {
+ MagicJsChunkProcessor.AddChunk(instanceId, "last", 2, "third", 0, 1);
+ MagicJsChunkProcessor.AddChunk(instanceId, "STREAM_COMPLETE", -1, "", 0, 1);
+ MagicJsChunkProcessor.AddChunk(instanceId, "first", 0, "first", 0, 1);
+ MagicJsChunkProcessor.AddChunk(instanceId, "middle", 1, "second", 0, 1);
+
+ CollectionAssert.AreEqual(
+ new[] { "first", "second", "third", "STREAM_COMPLETE" },
+ Enumerable.Range(0, 4)
+ .Select(_ => MagicJsChunkProcessor.GetCompletedItem(instanceId))
+ .ToArray());
+ }
+ finally
+ {
+ MagicJsChunkProcessor.RemoveInstance(instanceId);
+ }
+ }
+
+ [TestMethod]
+ public void ConcurrentStreamInstances_RemainIsolated()
+ {
+ var first = Guid.NewGuid().ToString("N");
+ var second = Guid.NewGuid().ToString("N");
+ MagicJsChunkProcessor.RegisterInstance(first);
+ MagicJsChunkProcessor.RegisterInstance(second);
+
+ try
+ {
+ MagicJsChunkProcessor.AddChunk(first, "item", 0, "one", 0, 1);
+ MagicJsChunkProcessor.AddChunk(second, "item", 0, "two", 0, 1);
+
+ Assert.AreEqual("one", MagicJsChunkProcessor.GetCompletedItem(first));
+ Assert.AreEqual("two", MagicJsChunkProcessor.GetCompletedItem(second));
+ Assert.IsNull(MagicJsChunkProcessor.GetCompletedItem(first));
+ Assert.IsNull(MagicJsChunkProcessor.GetCompletedItem(second));
+ }
+ finally
+ {
+ MagicJsChunkProcessor.RemoveInstance(first);
+ MagicJsChunkProcessor.RemoveInstance(second);
+ }
+ }
+
+ [TestMethod]
+ public void RemovedInstance_DoesNotLeakPreviouslyCompletedItems()
+ {
+ var instanceId = Guid.NewGuid().ToString("N");
+ MagicJsChunkProcessor.RegisterInstance(instanceId);
+ MagicJsChunkProcessor.AddChunk(instanceId, "item", 0, "secret", 0, 1);
+
+ MagicJsChunkProcessor.RemoveInstance(instanceId);
+
+ Assert.IsNull(MagicJsChunkProcessor.GetCompletedItem(instanceId));
+ MagicJsChunkProcessor.RemoveInstance(instanceId);
+ }
}
diff --git a/Magic.IndexedDb.UnitTests/ExpressionBuilderTests.cs b/Magic.IndexedDb.UnitTests/ExpressionBuilderTests.cs
index e3a8bce..ffeadbd 100644
--- a/Magic.IndexedDb.UnitTests/ExpressionBuilderTests.cs
+++ b/Magic.IndexedDb.UnitTests/ExpressionBuilderTests.cs
@@ -1,20 +1,93 @@
using Magic.IndexedDb.LinqTranslation.Extensions;
+using Magic.IndexedDb.Helpers;
+using Magic.IndexedDb.Models;
+using System.Text.Json;
+using System.Text.Json.Serialization;
namespace Magic.IndexedDb.UnitTests;
[TestClass]
public sealed class ExpressionBuilderTests
{
+ [TestMethod]
+ public void PlainEnumEquality_IsRecognizedAndSerializedNumerically()
+ {
+ var node = new UniversalExpressionBuilder(
+ record => record.Status == RecordStatus.Active).Build();
+
+ AssertNumericEnumCondition(node);
+ }
+
[TestMethod]
public void ExplicitEnumConversions_AreRecognizedAsPropertyComparisons()
{
var node = new UniversalExpressionBuilder(
record => (int)record.Status == (int)RecordStatus.Active).Build();
+ AssertNumericEnumCondition(node);
+ }
+
+ [TestMethod]
+ public void NullableAndReversedEnumEquality_AreRecognized()
+ {
+ var nullable = new UniversalExpressionBuilder(
+ record => record.Status == RecordStatus.Active).Build();
+ var reversed = new UniversalExpressionBuilder(
+ record => RecordStatus.Active == record.Status).Build();
+
+ Assert.AreEqual(RecordStatus.Active, nullable.Condition!.Value.value);
+ Assert.AreEqual(RecordStatus.Active, reversed.Condition!.Value.value);
+ }
+
+ [TestMethod]
+ public void StringEnumConverter_UsesTheSameRepresentationForRecordsAndFilters()
+ {
+ var settings = new MagicJsonSerializationSettings { UseCamelCase = true };
+ var node = new UniversalExpressionBuilder(
+ record => record.Status == NamedStatus.Active).Build();
+
+ var recordJson = MagicSerializationHelper.SerializeObject(
+ new NamedEnumRecord { Status = NamedStatus.Active }, settings);
+ var filterJson = MagicSerializationHelper.SerializeObject(node, settings);
+
+ Assert.AreEqual("Active", JsonDocument.Parse(recordJson).RootElement
+ .GetProperty("status").GetString());
+ Assert.AreEqual("Active", JsonDocument.Parse(filterJson).RootElement
+ .GetProperty("condition").GetProperty("value").GetString());
+ }
+
+ [TestMethod]
+ public void StringBackedEnums_RejectRangeComparisons()
+ {
+ var exception = Assert.ThrowsExactly(() =>
+ new UniversalExpressionBuilder(
+ record => (int)record.Status > 0).Build());
+
+ StringAssert.Contains(exception.Message, "persisted as a JSON string");
+ }
+
+ [TestMethod]
+ public void NonEnumMemberConversions_RemainUnsupported()
+ {
+ var exception = Assert.ThrowsExactly(() =>
+ new UniversalExpressionBuilder(
+ record => (int)record.Amount == 1).Build());
+
+ StringAssert.Contains(exception.Message, "Unsupported binary expression");
+ }
+
+ private static void AssertNumericEnumCondition(
+ Magic.IndexedDb.LinqTranslation.Models.FilterNode node)
+ {
Assert.IsTrue(node.Condition.HasValue);
Assert.AreEqual(nameof(EnumRecord.Status), node.Condition.Value.property);
Assert.AreEqual("Equal", node.Condition.Value.operation);
- Assert.AreEqual(1, node.Condition.Value.value);
+ Assert.AreEqual(RecordStatus.Active, node.Condition.Value.value);
+
+ var settings = new MagicJsonSerializationSettings { UseCamelCase = true };
+ var json = MagicSerializationHelper.SerializeObject(node, settings);
+ Assert.AreEqual(1, JsonDocument.Parse(json).RootElement
+ .GetProperty("condition").GetProperty("value").GetInt32());
}
private sealed class EnumRecord
@@ -22,9 +95,31 @@ private sealed class EnumRecord
public RecordStatus Status { get; set; }
}
+ private sealed class NullableEnumRecord
+ {
+ public RecordStatus? Status { get; set; }
+ }
+
+ private sealed class NamedEnumRecord
+ {
+ public NamedStatus Status { get; set; }
+ }
+
+ private sealed class NumericRecord
+ {
+ public decimal Amount { get; set; }
+ }
+
private enum RecordStatus
{
Inactive = 0,
Active = 1
}
+
+ [JsonConverter(typeof(JsonStringEnumConverter))]
+ private enum NamedStatus
+ {
+ Inactive = 0,
+ Active = 1
+ }
}
diff --git a/Magic.IndexedDb.UnitTests/ExpressionContractMatrixTests.cs b/Magic.IndexedDb.UnitTests/ExpressionContractMatrixTests.cs
new file mode 100644
index 0000000..e21fb5a
--- /dev/null
+++ b/Magic.IndexedDb.UnitTests/ExpressionContractMatrixTests.cs
@@ -0,0 +1,172 @@
+using System.Linq.Expressions;
+using Magic.IndexedDb.LinqTranslation.Extensions;
+using Magic.IndexedDb.LinqTranslation.Models;
+using Magic.IndexedDb.SchemaAnnotations;
+
+namespace Magic.IndexedDb.UnitTests;
+
+[TestClass]
+public sealed class ExpressionContractMatrixTests
+{
+ [TestMethod]
+ public void NumericComparisonMatrix_PreservesOperationAndOperandDirection()
+ {
+ (Expression> Predicate, string Operation, object Value)[] cases =
+ [
+ (record => record.Age == 18, "Equal", 18),
+ (record => record.Age != 18, "NotEqual", 18),
+ (record => record.Age > 18, "GreaterThan", 18),
+ (record => record.Age >= 18, "GreaterThanOrEqual", 18),
+ (record => record.Age < 18, "LessThan", 18),
+ (record => record.Age <= 18, "LessThanOrEqual", 18),
+ (record => 18 < record.Age, "GreaterThan", 18),
+ (record => 18 <= record.Age, "GreaterThanOrEqual", 18),
+ (record => 18 > record.Age, "LessThan", 18),
+ (record => 18 >= record.Age, "LessThanOrEqual", 18)
+ ];
+
+ foreach (var (predicate, operation, value) in cases)
+ {
+ var condition = Condition(predicate);
+ Assert.AreEqual("persisted_age", condition.property, predicate.ToString());
+ Assert.AreEqual(operation, condition.operation, predicate.ToString());
+ Assert.AreEqual(value, condition.value, predicate.ToString());
+ }
+ }
+
+ [TestMethod]
+ public void CapturedValues_AreEvaluatedWithoutChangingThePredicateShape()
+ {
+ var minimum = 21;
+
+ var condition = Condition(record => record.Age >= minimum);
+
+ Assert.AreEqual("GreaterThanOrEqual", condition.operation);
+ Assert.AreEqual(21, condition.value);
+ }
+
+ [TestMethod]
+ public void BooleanMember_IsTranslatedAsEqualityWithTrue()
+ {
+ var condition = Condition(record => record.Enabled);
+
+ Assert.AreEqual(nameof(QueryRecord.Enabled), condition.property);
+ Assert.AreEqual("Equal", condition.operation);
+ Assert.AreEqual(true, condition.value);
+ }
+
+ [TestMethod]
+ public void StringMethodMatrix_PreservesOperationAndCaseSensitivity()
+ {
+ (Expression> Predicate, string Operation, bool CaseSensitive)[] cases =
+ [
+ (record => record.Name.Contains("ab"), "Contains", true),
+ (record => record.Name.Contains("ab", StringComparison.OrdinalIgnoreCase), "Contains", false),
+ (record => record.Name.StartsWith("ab", StringComparison.Ordinal), "StartsWith", true),
+ (record => record.Name.EndsWith("ab", StringComparison.OrdinalIgnoreCase), "EndsWith", false),
+ (record => !record.Name.Contains("ab"), "NotContains", true),
+ (record => !record.Name.StartsWith("ab"), "NotStartsWith", true),
+ (record => !record.Name.EndsWith("ab"), "NotEndsWith", true)
+ ];
+
+ foreach (var (predicate, operation, caseSensitive) in cases)
+ {
+ var condition = Condition(predicate);
+ Assert.AreEqual(operation, condition.operation, predicate.ToString());
+ Assert.AreEqual(caseSensitive, condition.caseSensitive, predicate.ToString());
+ Assert.AreEqual("ab", condition.value, predicate.ToString());
+ }
+ }
+
+ [TestMethod]
+ public void LengthComparisonMatrix_UsesLengthOperations()
+ {
+ (Expression> Predicate, string Operation)[] cases =
+ [
+ (record => record.Name.Length == 3, "LengthEqual"),
+ (record => record.Name.Length != 3, "NotLengthEqual"),
+ (record => record.Name.Length > 3, "LengthGreaterThan"),
+ (record => record.Name.Length >= 3, "LengthGreaterThanOrEqual"),
+ (record => record.Name.Length < 3, "LengthLessThan"),
+ (record => record.Name.Length <= 3, "LengthLessThanOrEqual")
+ ];
+
+ foreach (var (predicate, operation) in cases)
+ Assert.AreEqual(operation, Condition(predicate).operation, predicate.ToString());
+ }
+
+ [TestMethod]
+ public void DateComponentMatrix_UsesComponentOperations()
+ {
+ (Expression> Predicate, string Operation, object Value)[] cases =
+ [
+ (record => record.When.Year == 2030, "YearEqual", 2030),
+ (record => record.When.Month != 2, "NotMonthEqual", 2),
+ (record => record.When.Day > 10, "DayGreaterThan", 10),
+ (record => record.When.DayOfYear <= 100, "DayOfYearLessThanOrEqual", 100),
+ (record => record.When.DayOfWeek == DayOfWeek.Monday, "DayOfWeekEqual", 1)
+ ];
+
+ foreach (var (predicate, operation, value) in cases)
+ {
+ var condition = Condition(predicate);
+ Assert.AreEqual(operation, condition.operation, predicate.ToString());
+ Assert.AreEqual(value, condition.value, predicate.ToString());
+ }
+ }
+
+ [TestMethod]
+ public void CollectionMembership_PreservesEveryAlternative()
+ {
+ int[] values = [1, 3, 5];
+
+ var node = new UniversalExpressionBuilder(
+ record => values.Contains(record.Age)).Build();
+
+ Assert.AreEqual(FilterNodeType.Logical, node.NodeType);
+ Assert.AreEqual(FilterLogicalOperator.Or, node.Operator);
+ CollectionAssert.AreEqual(
+ values,
+ node.Children!.Select(child => (int)child.Condition!.Value.value!).ToArray());
+ Assert.IsTrue(node.Children!.All(child => child.Condition!.Value.operation == "Equal"));
+ }
+
+ [TestMethod]
+ public void NegatedLogicalExpression_AppliesDeMorgansLaw()
+ {
+ var node = new UniversalExpressionBuilder(
+ record => !(record.Age > 18 || record.Name == "admin")).Build();
+
+ Assert.AreEqual(FilterLogicalOperator.And, node.Operator);
+ CollectionAssert.AreEqual(
+ new[] { "LessThanOrEqual", "NotEquals" },
+ node.Children!.Select(child => child.Condition!.Value.operation).ToArray());
+ }
+
+ [TestMethod]
+ public void UnsupportedArithmeticExpression_FailsWithActionableContext()
+ {
+ var exception = Assert.ThrowsExactly(() =>
+ new UniversalExpressionBuilder(
+ record => record.Age + 1 > 20).Build());
+
+ StringAssert.Contains(exception.Message, "Unsupported binary expression");
+ }
+
+ private static Magic.IndexedDb.Models.UniversalOperations.FilterCondition Condition(
+ Expression> predicate)
+ {
+ var node = new UniversalExpressionBuilder(predicate).Build();
+ Assert.IsTrue(node.Condition.HasValue, predicate.ToString());
+ return node.Condition.Value;
+ }
+
+ private sealed class QueryRecord
+ {
+ [MagicName("persisted_age")]
+ public int Age { get; set; }
+ public string Name { get; set; } = string.Empty;
+ public bool Enabled { get; set; }
+ public DateTime When { get; set; }
+ }
+}
diff --git a/Magic.IndexedDb.UnitTests/Magic.IndexedDb.UnitTests.csproj b/Magic.IndexedDb.UnitTests/Magic.IndexedDb.UnitTests.csproj
index baab0d4..50786d8 100644
--- a/Magic.IndexedDb.UnitTests/Magic.IndexedDb.UnitTests.csproj
+++ b/Magic.IndexedDb.UnitTests/Magic.IndexedDb.UnitTests.csproj
@@ -13,6 +13,7 @@
+
diff --git a/Magic.IndexedDb.UnitTests/PublicApiBaseline.txt b/Magic.IndexedDb.UnitTests/PublicApiBaseline.txt
new file mode 100644
index 0000000..9340f60
--- /dev/null
+++ b/Magic.IndexedDb.UnitTests/PublicApiBaseline.txt
@@ -0,0 +1,308 @@
+enum Magic.IndexedDb.BlazorInteropMode : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable
+ field System.Int64 value__
+ field static Magic.IndexedDb.BlazorInteropMode SignalR = 31744
+ field static Magic.IndexedDb.BlazorInteropMode WASM = 15728640
+class Magic.IndexedDb.DbMigration
+ ctor Magic.IndexedDb.DbMigration()
+ property System.Collections.Generic.List Instructions { get; set; }
+ property System.String FromVersion { get; set; }
+ property System.String ToVersion { get; set; }
+class Magic.IndexedDb.DbMigrationInstruction
+ ctor Magic.IndexedDb.DbMigrationInstruction()
+ property System.String Action { get; set; }
+ property System.String Details { get; set; }
+ property System.String StoreName { get; set; }
+class Magic.IndexedDb.DbStore
+ ctor Magic.IndexedDb.DbStore()
+ property System.Collections.Generic.List DbMigrations { get; set; }
+ property System.Collections.Generic.List StoreSchemas { get; set; }
+ property System.Int32 Version { get; set; }
+ property System.String Name { get; set; }
+static class Magic.IndexedDb.Extensions.MagicJsChunkProcessor
+ method static System.String GetCompletedItem(System.String instanceId)
+ method static System.Void AddChunk(System.String instanceId, System.String chunkInstanceId, System.Int32 yieldOrderIndex, System.String chunk, System.Int32 chunkIndex, System.Int32 totalChunks)
+ method static System.Void RegisterInstance(System.String instanceId)
+ method static System.Void RemoveInstance(System.String instanceId)
+static class Magic.IndexedDb.Helpers.AttributeHelpers
+ method static System.Collections.Generic.List GetPrimaryKeys(T item)
+ method static System.Type[] GetPrimaryKeyTypes()
+ method static System.Void ValidatePrimaryKey(System.Object[] keys)
+static class Magic.IndexedDb.Helpers.ExpandoToTypeConverter
+static class Magic.IndexedDb.Helpers.ExpressionFlattener
+ method static System.Linq.Expressions.Expression> FlattenAndOptimize(System.Linq.Expressions.Expression> expr)
+static class Magic.IndexedDb.Helpers.MagicSerializationHelper
+ method static System.Object[] SerializeObjects(Magic.IndexedDb.Interfaces.ITypedArgument[] objs, Magic.IndexedDb.Models.MagicJsonSerializationSettings settings = null)
+ method static System.String SerializeObject(T value, Magic.IndexedDb.Models.MagicJsonSerializationSettings settings = null)
+ method static System.String[] SerializeObjectsToString(Magic.IndexedDb.Interfaces.ITypedArgument[] objs, Magic.IndexedDb.Models.MagicJsonSerializationSettings settings = null)
+ method static System.Text.Json.JsonElement SerializeObjectToJsonElement(T value, Magic.IndexedDb.Models.MagicJsonSerializationSettings settings = null)
+ method static System.Threading.Tasks.Task SerializeObjectToStreamAsync(System.IO.Stream stream, T value, Magic.IndexedDb.Models.MagicJsonSerializationSettings settings = null)
+ method static System.Threading.Tasks.Task SerializeObjectToStreamAsync(System.IO.StreamWriter writer, T value, Magic.IndexedDb.Models.MagicJsonSerializationSettings settings = null)
+ method static System.Void PopulateObject(T source, T target)
+ method static T DeserializeObject(System.String json, Magic.IndexedDb.Models.MagicJsonSerializationSettings settings = null)
+static class Magic.IndexedDb.Helpers.MagicValidator
+ method static System.Void ValidateTables(System.Collections.Generic.List magicTableClasses = null)
+class Magic.IndexedDb.Helpers.PrimaryKeys
+ ctor Magic.IndexedDb.Helpers.PrimaryKeys()
+ property System.Object Value { get; set; }
+ property System.String JsName { get; set; }
+static class Magic.IndexedDb.Helpers.PropertyMappingCache
+ method static Magic.IndexedDb.Helpers.SearchPropEntry GetTypeOfTProperties(System.Type type)
+ method static Magic.IndexedDb.Models.MagicPropertyEntry GetPropertyByCsharpName(Magic.IndexedDb.Helpers.SearchPropEntry propCachee, System.String csharpName)
+ method static Magic.IndexedDb.Models.MagicPropertyEntry GetPropertyEntry(System.Reflection.PropertyInfo property, System.Type type)
+ method static Magic.IndexedDb.Models.MagicPropertyEntry GetPropertyEntry(System.String propertyName, System.Type type)
+ method static Magic.IndexedDb.Models.MagicPropertyEntry GetPropertyEntry(System.Reflection.PropertyInfo property)
+ method static Magic.IndexedDb.Models.MagicPropertyEntry GetPropertyEntry(System.String propertyName)
+ method static System.Boolean IsComplexType(System.Type type)
+ method static System.Boolean IsSimpleType(System.Type type)
+ method static System.Collections.Generic.IEnumerable GetAllNestedComplexTypes(System.Collections.Generic.IEnumerable properties)
+ method static System.Collections.Generic.List GetPrimaryKeysOfType(System.Type type)
+ method static System.String GetCsharpPropertyName(Magic.IndexedDb.Helpers.SearchPropEntry propCachee, System.String jsPropertyName)
+ method static System.String GetCsharpPropertyName(System.String jsPropertyName, System.Type type)
+ method static System.String GetCsharpPropertyName(System.String jsPropertyName)
+ method static System.String GetJsPropertyName(System.Reflection.PropertyInfo prop, System.Type type)
+ method static System.String GetJsPropertyName(System.String csharpPropertyName, System.Type type)
+ method static System.String GetJsPropertyName(System.Reflection.PropertyInfo prop)
+ method static System.String GetJsPropertyName(System.String csharpPropertyName)
+static class Magic.IndexedDb.Helpers.SchemaHelper
+ method static Magic.IndexedDb.StoreSchema GetStoreSchema(System.Type type)
+ method static System.Boolean HasMagicTableInterface(System.Type type)
+ method static System.Boolean ImplementsIMagicRepository(System.Type type)
+ method static System.Boolean ImplementsIMagicTable(System.Type type)
+ method static System.Collections.Generic.List GetAllIndexedDbSets()
+ method static System.Collections.Generic.List GetAllSchemas(System.String databaseName = null)
+ method static System.Collections.Generic.List GetAllMagicRepositories()
+ method static System.Collections.Generic.List GetAllMagicTables()
+ method static System.String GetDefaultDatabaseName()
+ method static System.String GetTableName()
+struct Magic.IndexedDb.Helpers.SearchPropEntry
+ ctor Magic.IndexedDb.Helpers.SearchPropEntry(System.Type type, System.Collections.Generic.Dictionary _propertyEntries, System.Reflection.ConstructorInfo[] constructors)
+ property System.Boolean EnforcePascalCase { get; }
+ property System.Boolean HasConstructorParameters { get; }
+ property System.Collections.Generic.Dictionary propertyEntries { get; }
+ property System.Collections.Generic.Dictionary ConstructorParameterMappings { get; }
+ property System.Collections.Generic.Dictionary jsNameToCsName { get; }
+ property System.Func InstanceCreator { get; }
+ property System.Reflection.ConstructorInfo Constructor { get; }
+ property System.String EffectiveTableName { get; }
+interface Magic.IndexedDb.IMagicCompoundIndex
+ property System.Reflection.PropertyInfo[] PropertyInfos { get; }
+ property System.String[] ColumnNamesInCompoundIndex { get; }
+interface Magic.IndexedDb.IMagicCompoundKey
+ property System.Boolean AutoIncrement { get; }
+ property System.Reflection.PropertyInfo[] PropertyInfos { get; }
+ property System.String[] ColumnNamesInCompoundKey { get; }
+interface Magic.IndexedDb.IMagicCursor : Magic.IndexedDb.IMagicExecute
+ method Magic.IndexedDb.IMagicCursor Cursor(System.Linq.Expressions.Expression> predicate)
+ method Magic.IndexedDb.IMagicCursorPaginationTake Take(System.Int32 amount)
+ method Magic.IndexedDb.IMagicCursorPaginationTake TakeLast(System.Int32 amount)
+ method Magic.IndexedDb.IMagicCursorSkip Skip(System.Int32 amount)
+ method Magic.IndexedDb.IMagicCursorStage OrderBy(System.Linq.Expressions.Expression> predicate)
+ method Magic.IndexedDb.IMagicCursorStage OrderByDescending(System.Linq.Expressions.Expression> predicate)
+ method Magic.IndexedDb.IMagicCursorStage StableOrdering()
+ method System.Threading.Tasks.Task FirstOrDefaultAsync()
+ method System.Threading.Tasks.Task LastOrDefaultAsync()
+interface Magic.IndexedDb.IMagicCursorFinal : Magic.IndexedDb.IMagicExecute
+ method System.Threading.Tasks.Task FirstOrDefaultAsync()
+ method System.Threading.Tasks.Task LastOrDefaultAsync()
+interface Magic.IndexedDb.IMagicCursorPaginationTake : Magic.IndexedDb.IMagicExecute
+ method Magic.IndexedDb.IMagicCursorSkip Skip(System.Int32 amount)
+interface Magic.IndexedDb.IMagicCursorSkip : Magic.IndexedDb.IMagicExecute
+ method System.Threading.Tasks.Task FirstOrDefaultAsync()
+ method System.Threading.Tasks.Task LastOrDefaultAsync()
+interface Magic.IndexedDb.IMagicCursorStage : Magic.IndexedDb.IMagicExecute
+ method Magic.IndexedDb.IMagicCursorPaginationTake Take(System.Int32 amount)
+ method Magic.IndexedDb.IMagicCursorPaginationTake TakeLast(System.Int32 amount)
+ method Magic.IndexedDb.IMagicCursorSkip Skip(System.Int32 amount)
+ method System.Threading.Tasks.Task FirstOrDefaultAsync()
+ method System.Threading.Tasks.Task LastOrDefaultAsync()
+interface Magic.IndexedDb.IMagicExecute
+ method System.Collections.Generic.IAsyncEnumerable AsAsyncEnumerable(System.Threading.CancellationToken cancellationToken = null)
+ method System.Threading.Tasks.Task> ToListAsync()
+interface Magic.IndexedDb.IMagicIndexedDb
+ method System.Threading.Tasks.Task GetStorageEstimateAsync(System.Threading.CancellationToken cancellationToken = null)
+ method System.Threading.Tasks.ValueTask> Query()
+ method System.Threading.Tasks.ValueTask> Query(System.Func dbSetSelector)
+ method System.Threading.Tasks.ValueTask Database(Magic.IndexedDb.IndexedDbSet indexedDbSet)
+interface Magic.IndexedDb.IMagicQuery : Magic.IndexedDb.IMagicExecute
+ method Magic.IndexedDb.IMagicCursor Cursor(System.Linq.Expressions.Expression> predicate)
+ method Magic.IndexedDb.IMagicQueryFinal Skip(System.Int32 amount)
+ method Magic.IndexedDb.IMagicQueryFinal TakeLast(System.Int32 amount)
+ method Magic.IndexedDb.IMagicQueryOrderableTable OrderBy(System.Linq.Expressions.Expression> predicate)
+ method Magic.IndexedDb.IMagicQueryOrderableTable OrderByDescending(System.Linq.Expressions.Expression> predicate)
+ method Magic.IndexedDb.IMagicQueryPaginationTake Take(System.Int32 amount)
+ method Magic.IndexedDb.IMagicQueryStaging Where(System.Linq.Expressions.Expression> predicate)
+ method System.Threading.Tasks.Task AddAsync(T record, System.Threading.CancellationToken cancellationToken = null)
+ method System.Threading.Tasks.Task AddRangeAsync(System.Collections.Generic.IEnumerable records, System.Threading.CancellationToken cancellationToken = null)
+ method System.Threading.Tasks.Task ClearTable()
+ method System.Threading.Tasks.Task DeleteAsync(T item, System.Threading.CancellationToken cancellationToken = null)
+ method System.Threading.Tasks.Task CountAsync()
+ method System.Threading.Tasks.Task DeleteRangeAsync(System.Collections.Generic.IEnumerable items, System.Threading.CancellationToken cancellationToken = null)
+ method System.Threading.Tasks.Task UpdateAsync(T item, System.Threading.CancellationToken cancellationToken = null)
+ method System.Threading.Tasks.Task UpdateRangeAsync(System.Collections.Generic.IEnumerable items, System.Threading.CancellationToken cancellationToken = null)
+ method System.Threading.Tasks.Task FirstOrDefaultAsync()
+ method System.Threading.Tasks.Task FirstOrDefaultAsync(System.Linq.Expressions.Expression> predicate)
+ method System.Threading.Tasks.Task LastOrDefaultAsync()
+ method System.Threading.Tasks.Task LastOrDefaultAsync(System.Linq.Expressions.Expression> predicate)
+ property System.String DatabaseName { get; }
+ property System.String SchemaName { get; }
+interface Magic.IndexedDb.IMagicQueryFinal : Magic.IndexedDb.IMagicExecute
+ method System.Threading.Tasks.Task> WhereAsync(System.Linq.Expressions.Expression> predicate)
+interface Magic.IndexedDb.IMagicQueryOrderable : Magic.IndexedDb.IMagicExecute
+ method Magic.IndexedDb.IMagicQueryFinal Skip(System.Int32 amount)
+ method Magic.IndexedDb.IMagicQueryFinal TakeLast(System.Int32 amount)
+ method Magic.IndexedDb.IMagicQueryPaginationTake Take(System.Int32 amount)
+ method System.Threading.Tasks.Task> WhereAsync(System.Linq.Expressions.Expression> predicate)
+ method System.Threading.Tasks.Task FirstOrDefaultAsync()
+ method System.Threading.Tasks.Task LastOrDefaultAsync()
+interface Magic.IndexedDb.IMagicQueryOrderableTable : Magic.IndexedDb.IMagicExecute, Magic.IndexedDb.IMagicQueryOrderable
+ method System.Threading.Tasks.Task FirstOrDefaultAsync(System.Linq.Expressions.Expression> predicate)
+ method System.Threading.Tasks.Task LastOrDefaultAsync(System.Linq.Expressions.Expression> predicate)
+interface Magic.IndexedDb.IMagicQueryPaginationTake : Magic.IndexedDb.IMagicExecute
+ method Magic.IndexedDb.IMagicQueryFinal Skip(System.Int32 amount)
+ method System.Threading.Tasks.Task> WhereAsync(System.Linq.Expressions.Expression> predicate)
+interface Magic.IndexedDb.IMagicQueryStaging : Magic.IndexedDb.IMagicExecute
+ method Magic.IndexedDb.IMagicQueryFinal Skip(System.Int32 amount)
+ method Magic.IndexedDb.IMagicQueryFinal TakeLast(System.Int32 amount)
+ method Magic.IndexedDb.IMagicQueryPaginationTake Take(System.Int32 amount)
+ method Magic.IndexedDb.IMagicQueryStaging Where(System.Linq.Expressions.Expression> predicate)
+ method System.Threading.Tasks.Task FirstOrDefaultAsync()
+ method System.Threading.Tasks.Task LastOrDefaultAsync()
+interface Magic.IndexedDb.IMagicTable : Magic.IndexedDb.Interfaces.IMagicTableBase
+ property TDbSets Databases { get; }
+interface Magic.IndexedDb.IMagicUtilities
+class Magic.IndexedDb.IndexedDbSet
+ ctor Magic.IndexedDb.IndexedDbSet(System.String databaseName)
+ property System.String DatabaseName { get; }
+interface Magic.IndexedDb.Interfaces.IColumnNamed
+ property System.String ColumnName { get; }
+interface Magic.IndexedDb.Interfaces.IMagicRepository
+interface Magic.IndexedDb.Interfaces.IMagicTableBase
+ method Magic.IndexedDb.IMagicCompoundKey GetKeys()
+ method Magic.IndexedDb.IndexedDbSet GetDefaultDatabase()
+ method System.Collections.Generic.List GetCompoundIndexes()
+ method System.String GetTableName()
+interface Magic.IndexedDb.Interfaces.ITypedArgument
+ method System.String Serialize()
+ method System.String SerializeToJsonString(Magic.IndexedDb.Models.MagicJsonSerializationSettings settings = null)
+ method System.Text.Json.JsonElement SerializeToJsonElement(Magic.IndexedDb.Models.MagicJsonSerializationSettings settings = null)
+class Magic.IndexedDb.LinqTranslation.Extensions.UniversalExpressionBuilder
+ ctor Magic.IndexedDb.LinqTranslation.Extensions.UniversalExpressionBuilder(System.Linq.Expressions.Expression> predicate)
+ method Magic.IndexedDb.LinqTranslation.Models.FilterNode Build()
+interface Magic.IndexedDb.LinqTranslation.Interfaces.IMagicDatabaseScoped
+ method System.Threading.Tasks.Task CloseAsync()
+ method System.Threading.Tasks.Task DeleteAsync()
+ method System.Threading.Tasks.Task OpenAsync()
+ method System.Threading.Tasks.Task DoesExistAsync()
+ method System.Threading.Tasks.Task IsOpenAsync()
+enum Magic.IndexedDb.LinqTranslation.Models.FilterLogicalOperator : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable
+ field System.Int32 value__
+ field static Magic.IndexedDb.LinqTranslation.Models.FilterLogicalOperator And = 0
+ field static Magic.IndexedDb.LinqTranslation.Models.FilterLogicalOperator Or = 1
+class Magic.IndexedDb.LinqTranslation.Models.FilterNode
+ ctor Magic.IndexedDb.LinqTranslation.Models.FilterNode()
+ property Magic.IndexedDb.LinqTranslation.Models.FilterLogicalOperator Operator { get; set; }
+ property Magic.IndexedDb.LinqTranslation.Models.FilterNodeType NodeType { get; set; }
+ property System.Collections.Generic.List Children { get; set; }
+ property System.Nullable Condition { get; set; }
+enum Magic.IndexedDb.LinqTranslation.Models.FilterNodeType : System.IComparable, System.IConvertible, System.IFormattable, System.ISpanFormattable
+ field System.Int32 value__
+ field static Magic.IndexedDb.LinqTranslation.Models.FilterNodeType Condition = 1
+ field static Magic.IndexedDb.LinqTranslation.Models.FilterNodeType Logical = 0
+class Magic.IndexedDb.MagicTableTool
+ ctor Magic.IndexedDb.MagicTableTool()
+class Magic.IndexedDb.MagicUniqueIndexAttribute : Magic.IndexedDb.Interfaces.IColumnNamed, System.Attribute
+ ctor Magic.IndexedDb.MagicUniqueIndexAttribute(System.String columnName = null)
+ property System.String ColumnName { get; }
+class Magic.IndexedDb.Models.IndexFilterValue
+ ctor Magic.IndexedDb.Models.IndexFilterValue(System.String indexName, System.Object filterValue)
+ property System.Object FilterValue { get; set; }
+ property System.String IndexName { get; set; }
+class Magic.IndexedDb.Models.MagicConstructorException : System.InvalidOperationException, System.Runtime.Serialization.ISerializable
+ ctor Magic.IndexedDb.Models.MagicConstructorException(System.String message)
+class Magic.IndexedDb.Models.MagicException : System.Exception, System.Runtime.Serialization.ISerializable
+ ctor Magic.IndexedDb.Models.MagicException(System.String message, System.Exception inner = null)
+class Magic.IndexedDb.Models.MagicJsonSerializationSettings
+ ctor Magic.IndexedDb.Models.MagicJsonSerializationSettings()
+ method System.Text.Json.JsonSerializerOptions GetOptionsWithResolver()
+ property System.Boolean UseCamelCase { get; set; }
+ property System.Text.Json.JsonSerializerOptions Options { get; set; }
+struct Magic.IndexedDb.Models.MagicPropertyEntry
+ ctor Magic.IndexedDb.Models.MagicPropertyEntry(System.Reflection.PropertyInfo property, Magic.IndexedDb.Interfaces.IColumnNamed columnNamedAttribute, System.Boolean indexed, System.Boolean uniqueIndex, System.Boolean primaryKey, System.Boolean notMapped, System.Boolean overrideNeverCamel = false)
+ property System.Action Setter { get; }
+ property System.Boolean Indexed { get; set; }
+ property System.Boolean IsComplexType { get; }
+ property System.Boolean NeverCamelCase { get; }
+ property System.Boolean NotMapped { get; set; }
+ property System.Boolean OverrideNeverCamel { get; }
+ property System.Boolean PrimaryKey { get; set; }
+ property System.Boolean UniqueIndex { get; set; }
+ property System.Func Getter { get; }
+ property System.Object DefaultValue { get; }
+ property System.Reflection.PropertyInfo Property { get; set; }
+ property System.String CsharpPropertyName { get; }
+ property System.String JsPropertyName { get; }
+class Magic.IndexedDb.Models.PredicateVisitor : System.Linq.Expressions.ExpressionVisitor
+ ctor Magic.IndexedDb.Models.PredicateVisitor()
+class Magic.IndexedDb.Models.QuotaUsage : System.IEquatable
+ ctor Magic.IndexedDb.Models.QuotaUsage(System.Int64 Quota, System.Int64 Usage)
+ method Magic.IndexedDb.Models.QuotaUsage $()
+ method System.Boolean Equals(Magic.IndexedDb.Models.QuotaUsage other)
+ method System.Boolean Equals(System.Object obj)
+ method System.Int32 GetHashCode()
+ method System.String ToString()
+ method System.Void Deconstruct(out System.Int64 Quota, out System.Int64 Usage)
+ property System.Double QuotaInMegabytes { get; }
+ property System.Double UsageInMegabytes { get; }
+ property System.Int64 Quota { get; set; }
+ property System.Int64 Usage { get; set; }
+ property System.ValueTuple InMegabytes { get; }
+class Magic.IndexedDb.Models.StoredMagicQuery
+ ctor Magic.IndexedDb.Models.StoredMagicQuery()
+ property System.Int32 intValue { get; set; }
+ property System.String additionFunction { get; set; }
+ property System.String property { get; set; }
+class Magic.IndexedDb.Models.TypedArgument : Magic.IndexedDb.Interfaces.ITypedArgument
+ ctor Magic.IndexedDb.Models.TypedArgument(T value)
+ method System.String Serialize()
+ method System.String SerializeToJsonString(Magic.IndexedDb.Models.MagicJsonSerializationSettings settings = null)
+ method System.Text.Json.JsonElement SerializeToJsonElement(Magic.IndexedDb.Models.MagicJsonSerializationSettings settings = null)
+ property T Value { get; }
+struct Magic.IndexedDb.Models.UniversalOperations.FilterCondition
+ ctor Magic.IndexedDb.Models.UniversalOperations.FilterCondition(System.String _property, System.String _operation, System.Object _value, System.Boolean _isString = false, System.Boolean _caseSensitive = false)
+ property System.Boolean caseSensitive { get; set; }
+ property System.Boolean isString { get; set; }
+ property System.Object value { get; set; }
+ property System.String operation { get; set; }
+ property System.String property { get; set; }
+class Magic.IndexedDb.SchemaAnnotations.MagicConstructorAttribute : System.Attribute
+ ctor Magic.IndexedDb.SchemaAnnotations.MagicConstructorAttribute()
+class Magic.IndexedDb.SchemaAnnotations.MagicIndexAttribute : Magic.IndexedDb.Interfaces.IColumnNamed, System.Attribute
+ ctor Magic.IndexedDb.SchemaAnnotations.MagicIndexAttribute(System.String columnName = null)
+ property System.String ColumnName { get; }
+class Magic.IndexedDb.SchemaAnnotations.MagicNameAttribute : Magic.IndexedDb.Interfaces.IColumnNamed, System.Attribute
+ ctor Magic.IndexedDb.SchemaAnnotations.MagicNameAttribute(System.String columnName)
+ property System.String ColumnName { get; }
+class Magic.IndexedDb.SchemaAnnotations.MagicNotMappedAttribute : System.Attribute
+ ctor Magic.IndexedDb.SchemaAnnotations.MagicNotMappedAttribute()
+static class Magic.IndexedDb.ServiceCollectionExtensions
+ method static Microsoft.Extensions.DependencyInjection.IServiceCollection AddMagicBlazorDB(Microsoft.Extensions.DependencyInjection.IServiceCollection services, Magic.IndexedDb.BlazorInteropMode interoptMode, System.Boolean isDebug)
+ method static Microsoft.Extensions.DependencyInjection.IServiceCollection AddMagicBlazorDB(Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Int64 jsMessageSizeBytes, System.Boolean isDebug)
+class Magic.IndexedDb.StoreRecord