Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
7adb00b
Replace JsonConstructor by MagicConstructor
Ard2025 Aug 24, 2025
a46fc71
SingleOrDefault rewrite MagicConstructor
Ard2025 Aug 24, 2025
dfe321a
improvement from yueyinqiu
Ard2025 Aug 24, 2025
29c1b8f
Proper constructor work when deserializing T. properties that are not…
Ard2025 Aug 25, 2025
c3483b0
Small styling improvement
Ard2025 Aug 25, 2025
e6f14b4
Use debuglog method more consistantly
Ard2025 Aug 28, 2025
7a548e9
Reduce memory useage on serializing
Ard2025 Oct 5, 2025
399c1d3
update server test to test memory 45MB
Ard2025 Oct 5, 2025
a6c0ddb
Andisposing the responseStream correctly
Ard2025 Oct 5, 2025
2d9550a
Improve internal serializeing of List<List<T>> and enum
Ard2025 Nov 14, 2025
74c63d7
Merge pull request #1 from magiccodingman/ard2025/MagicConstructor
Ard2025 Dec 28, 2025
5dc7569
Merge pull request #2 from magiccodingman/ard2025/use-debuglog-consis…
Ard2025 Dec 28, 2025
0c7d505
Merge pull request #3 from magiccodingman/Ard/serializer-memory-usage…
Ard2025 Dec 28, 2025
a786bea
Clean testwasm
Ard2025 Dec 28, 2025
ff95ac7
Merge pull request #4 from Ard2025/clean-testwasm
Ard2025 Dec 28, 2025
dce3835
upgrade to dotnet 10
Ard2025 Dec 28, 2025
4c02f2e
update playright unit tests
Ard2025 Dec 28, 2025
f01a5f9
.
Ard2025 Dec 28, 2025
13c5d4d
Merge pull request #5 from Ard2025/upgrade-dotnet-10
Ard2025 Dec 28, 2025
3a42cf2
Merge pull request #1 from Ard2025/master
Paul-ChCh Mar 23, 2026
78de43b
Upgrade to .Net 9 and provide a longer js timeout to allow for large …
Paul-ChCh Mar 31, 2026
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
10 changes: 5 additions & 5 deletions E2eTest/E2eTest.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>latest</LangVersion>
<LangVersion>13</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.2" />
<PackageReference Include="Microsoft.Playwright.MSTest" Version="1.49.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="MSTest" Version="3.6.4" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.10" />
<PackageReference Include="Microsoft.Playwright.MSTest" Version="1.57.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="MSTest" Version="3.11.1" />
</ItemGroup>

<ItemGroup>
Expand Down
1 change: 1 addition & 0 deletions E2eTestWebApp/E2eTestWebApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>13</LangVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
3 changes: 3 additions & 0 deletions Magic.IndexedDb/Exceptions/MagicConstructorException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace Magic.IndexedDb.Exceptions;

public class MagicConstructorException(string message) : Exception(message);
5 changes: 3 additions & 2 deletions Magic.IndexedDb/Extensions/MagicJsInvoke.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,17 +122,18 @@ internal async Task CallInvokeVoidDefaultJsAsync(string modulePath, string funct

stream.Position = 0;

var streamRef = new DotNetStreamReference(stream);
using var streamRef = new DotNetStreamReference(stream);

// Send to JS
var responseStreamRef = await _jsModule.InvokeAsync<IJSStreamReference>("streamedJsHandler",
var responseStreamRef = await _jsModule.InvokeAsync<IJSStreamReference>("streamedJsHandler", TimeSpan.FromSeconds(120),
streamRef, instanceId, DotNetObjectReference.Create(this), _jsMessageSizeBytes);

// 🚀 Convert the stream reference back to JSON in C#
await using var responseStream = await responseStreamRef.OpenReadStreamAsync(long.MaxValue, token);
using var reader = new StreamReader(responseStream);

string jsonResponse = await reader.ReadToEndAsync();
await responseStreamRef.DisposeAsync();
return MagicSerializationHelper.DeserializeObject<T>(jsonResponse, settings);
}

Expand Down
14 changes: 10 additions & 4 deletions Magic.IndexedDb/Helpers/PropertyMappingCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Concurrent;
using System.Reflection;
using System.Text.Json.Serialization;
using Magic.IndexedDb.Exceptions;

namespace Magic.IndexedDb.Helpers;

Expand Down Expand Up @@ -37,12 +38,17 @@ public SearchPropEntry(Type type, Dictionary<string, MagicPropertyEntry> _proper
EnforcePascalCase = false;
}

// 🔥 Pick the best constructor: Prefer JsonConstructor, then fall back to a parameterized one, else fallback to parameterless
var jsonConstructor = constructors.FirstOrDefault(c => c.GetCustomAttribute<JsonConstructorAttribute>() != null);
if (jsonConstructor == null)
// 🔥 Pick the best constructor: Prefer MagicConstructor, then fall back to a parameterized one, else fallback to parameterless
try
{
Constructor = constructors.SingleOrDefault(c => c.GetCustomAttribute<MagicConstructorAttribute>() is not null);
}
catch (InvalidOperationException)
{
Constructor = constructors.OrderByDescending(c => c.GetParameters().Length).FirstOrDefault();
throw new MagicConstructorException("Only one magic constructor is allowed");
}
Constructor ??= constructors.OrderByDescending(c => c.GetParameters().Length).FirstOrDefault();

HasConstructorParameters = Constructor != null && Constructor.GetParameters().Length > 0;

// 🔥 Cache constructor parameter mappings
Expand Down
26 changes: 20 additions & 6 deletions Magic.IndexedDb/Helpers/SchemaHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,24 @@ internal static void EnsureSchemaIsCached(Type type)
});
}

private static List<Type> getAllLoadableTypes()
{
return AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(a =>
{
try
{
return a.GetTypes();
}
catch (ReflectionTypeLoadException ex)
{
return ex.Types.Where(t => t != null)!;
}
})
.ToList();
}


public static bool ImplementsIMagicTable(Type type)
{
return type.GetInterfaces().Any(i => i.IsGenericType
Expand All @@ -28,18 +46,14 @@ public static bool ImplementsIMagicTable(Type type)

public static List<Type>? GetAllMagicTables()
{
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
return assemblies
.SelectMany(a => a.GetTypes())
return getAllLoadableTypes()
.Where(t => t.IsClass && !t.IsAbstract && SchemaHelper.ImplementsIMagicTable(t))
.ToList();
}

public static List<Type>? GetAllMagicRepositories()
{
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
return assemblies
.SelectMany(a => a.GetTypes())
return getAllLoadableTypes()
.Where(t => t.IsClass && !t.IsAbstract && ImplementsIMagicRepository(t))
.ToList();
}
Expand Down
1 change: 0 additions & 1 deletion Magic.IndexedDb/Interfaces/ITypedArgument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ namespace Magic.IndexedDb.Interfaces;

public interface ITypedArgument
{
string Serialize(); // Still needed for some cases
JsonElement SerializeToJsonElement(MagicJsonSerializationSettings? settings = null); // Ensures proper object passing
string SerializeToJsonString(MagicJsonSerializationSettings? settings = null);
}
Original file line number Diff line number Diff line change
Expand Up @@ -744,16 +744,23 @@ private static ConstantExpression ToConst(Expression expr)
{
expr = StripConvert(expr); // <-- handle Convert wrappers

return expr switch
try
{
ConstantExpression c => c,
return expr switch
{
ConstantExpression c => c,

// e.g., new DateTime(...) or anything not marked constant but compile-safe
NewExpression or MemberExpression or MethodCallExpression =>
Expression.Constant(Expression.Lambda(expr).Compile().DynamicInvoke()),
// e.g., new DateTime(...) or anything not marked constant but compile-safe
NewExpression or MemberExpression or MethodCallExpression =>
Expression.Constant(Expression.Lambda(expr).Compile().DynamicInvoke()),

_ => throw new InvalidOperationException($"Unsupported or non-constant expression: {expr}")
};
_ => throw new InvalidOperationException($"Unsupported or non-constant expression: {expr}")
};
}
catch (Exception ex)
{
throw ex;
}
}

private static Expression StripConvert(Expression expr)
Expand Down
9 changes: 5 additions & 4 deletions Magic.IndexedDb/Magic.IndexedDb.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<Title>Magic.IndexedDb</Title>
Expand All @@ -17,7 +17,8 @@
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageLicenseFile>LICENSE.txt</PackageLicenseFile>
<FileVersion>1.01</FileVersion>
<Version>2.0.2</Version>
<Version>2.9.2</Version>
<LangVersion>13</LangVersion>
</PropertyGroup>

<ItemGroup>
Expand All @@ -42,7 +43,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="8.0.11" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="9.0.10" />
<PackageReference Include="System.Linq.Async" Version="7.0.0" />
</ItemGroup>
</Project>
43 changes: 35 additions & 8 deletions Magic.IndexedDb/Models/MagicContractResolver.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Collections;
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using Magic.IndexedDb.Helpers;
Expand Down Expand Up @@ -62,25 +63,41 @@ internal class MagicContractResolver<T> : JsonConverter<T>

private object CreateObjectFromDictionary(Type type, Dictionary<string, object?> propertyValues, SearchPropEntry search)
{
// 🚀 If there's a constructor with parameters, use it
if (search.ConstructorParameterMappings.Count > 0)
object? obj = null;
List<string> unInitializedProperties = new List<string>();
// If the constructor set in the SearchPropEntry contains parameters, fill them
if (search.HasConstructorParameters)
{
var constructorArgs = new object?[search.ConstructorParameterMappings.Count];
foreach (var (paramName, index) in search.ConstructorParameterMappings)
{
if (propertyValues.TryGetValue(paramName, out var value))
{
constructorArgs[index] = value;
propertyValues.Remove(paramName);
}
else
constructorArgs[index] = GetDefaultValue(type.GetProperty(paramName)?.PropertyType ?? typeof(object));
{
constructorArgs[index] =
GetDefaultValue(type.GetProperty(paramName)?.PropertyType ?? typeof(object));
}
}

return search.InstanceCreator(constructorArgs) ?? throw new InvalidOperationException($"Failed to create instance of type {type.Name}.");
obj = search.InstanceCreator(constructorArgs);
}
else
{
// 🚀 Use parameterless constructor
obj = search.InstanceCreator([]);
}

if (obj is null)
{
throw new InvalidOperationException($"Failed to create instance of type {type.Name}.");
}

// 🚀 Use parameterless constructor
var obj = search.InstanceCreator(Array.Empty<object?>()) ?? throw new InvalidOperationException($"Failed to create instance of type {type.Name}.");

// 🚀 Assign property values
// 🚀 Assign property values (to properties not passed to constructor)
foreach (var (propName, value) in propertyValues)
{
if (search.propertyEntries.TryGetValue(propName, out var propEntry))
Expand Down Expand Up @@ -342,6 +359,12 @@ private bool SerializeIEnumerable(Utf8JsonWriter writer, object? value, JsonSeri
continue;
}

if (item is IEnumerable nestedEnumerable && item.GetType() != typeof(string))
{
SerializeIEnumerable(writer, nestedEnumerable, options);
continue;
}

if (item != null)
{
Type itemType = item.GetType();
Expand Down Expand Up @@ -374,7 +397,8 @@ private void WriteSimpleType(Utf8JsonWriter writer, object value)
switch (value)
{
case string str:
writer.WriteStringValue(str);
str = str.Replace("\"", "\\\"");
writer.WriteRawValue("\"" + str + "\"", true);
break;
case bool b:
writer.WriteBooleanValue(b);
Expand All @@ -397,6 +421,9 @@ private void WriteSimpleType(Utf8JsonWriter writer, object value)
case Guid guid:
writer.WriteStringValue(guid.ToString());
break;
case Enum e:
writer.WriteNumberValue(((IConvertible)e).ToInt32(null));
break;
default:
JsonSerializer.Serialize(writer, value);
break;
Expand Down
5 changes: 0 additions & 5 deletions Magic.IndexedDb/Models/TypedArgument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,6 @@ public TypedArgument(T? value)
Value = value;
}

public string Serialize()
{
return MagicSerializationHelper.SerializeObject(Value);
}

public JsonElement SerializeToJsonElement(MagicJsonSerializationSettings? settings = null)
{
return MagicSerializationHelper.SerializeObjectToJsonElement(Value, settings);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Magic.IndexedDb.SchemaAnnotations;

/// <summary>
/// Sets the preferred constructor for serialization for MagicDB
/// </summary>
public class MagicConstructorAttribute : Attribute;
13 changes: 7 additions & 6 deletions Magic.IndexedDb/wwwroot/magicDB.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
/// <reference types="./dexie/dexie.d.ts" />
import Dexie from "./dexie/dexie.js";
import { magicQueryAsync, magicQueryYield } from "./magicLinqToIndexedDb.js";
import {debugLog} from "./utilities/utilityHelpers.js";
/**
* @typedef {Object} DatabasesItem
* @property {string} name
Expand Down Expand Up @@ -77,7 +78,7 @@ export function listOpenDatabases() {


export function createDb(dbStore) {
console.log("Debug: Received dbStore in createDb", dbStore);
debugLog("Received dbStore in createDb", dbStore);

if (!dbStore || !dbStore.name) {
console.error("Blazor.IndexedDB.Framework - Invalid dbStore provided");
Expand Down Expand Up @@ -144,7 +145,7 @@ export function createDb(dbStore) {
stores[schema.tableName] = def;
}

console.log("Dexie Store Definition:", stores);
debugLog("Dexie Store Definition:", stores);

db.version(dbStore.version).stores(stores);

Expand Down Expand Up @@ -178,7 +179,7 @@ export function closeAll() {
databases.forEach((entry, dbName) => {
entry.db.close();
entry.isOpen = false;
console.log(`Database ${dbName} closed.`);
debugLog(`Database ${dbName} closed.`);
});
}

Expand All @@ -205,7 +206,7 @@ export async function deleteDb(dbName) {
}

await Dexie.delete(dbName);
console.log(`Database '${dbName}' deleted.`);
debugLog(`Database '${dbName}' deleted.`);
} catch (deleteErr) {
console.error(`deleteDb: Failed to delete DB '${dbName}'`, deleteErr);
}
Expand Down Expand Up @@ -297,7 +298,7 @@ export async function deleteAllDatabases() {
for (const dbName of databases.keys()) {
await deleteDb(dbName);
}
console.log("All databases deleted.");
debugLog("All databases deleted.");
}


Expand Down Expand Up @@ -457,7 +458,7 @@ export async function bulkDelete(dbName, storeName, items) {
items.map(item => getKeyArrayForDelete(dbName, storeName, item))
);

console.log('Keys to delete:', formattedKeys);
debugLog('Keys to delete:', formattedKeys);
await table.bulkDelete(formattedKeys);
return items.length;
} catch (e) {
Expand Down
4 changes: 3 additions & 1 deletion Magic.IndexedDb/wwwroot/magicMigration.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import {debugLog} from "./utilities/utilityHelpers.js";

export class MagicMigration {
constructor(db) {
this.db = db;
}

Initialize() {
console.log("Using Dexie from MagicDB:", this.db);
debugLog("Using Dexie from MagicDB:", this.db);
// You can now do any Dexie operations here
}
}
2 changes: 1 addition & 1 deletion TestBase/Data/PersonData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ public static class PersonData
{
public static Person[] persons =
[
new Person { _Id = 1, Name = "Zack", DateOfBirth = null, TestInt = 9, _Age = 45, GUIY = Guid.NewGuid(), DoNotMapTest = "I buried treasure behind my house", Access=Person.Permissions.CanRead},
new Person { _Id = 1, Name = "Zack", DateOfBirth = null, TestInt = 9, _Age = 45, GUIY = Guid.NewGuid(), DoNotMapTest = "I buried treasure behind my house", Access=Person.Permissions.CanRead, Secret = new String('a', 45)},
new Person { _Id = 2, Name = "Luna", TestInt = 9, DateOfBirth = new DateTime(1980, 1, 1), _Age = 35, GUIY = Guid.NewGuid(), DoNotMapTest = "Jerry is my husband and I had an affair with Bob.", Access = Person.Permissions.CanRead|Person.Permissions.CanWrite},
new Person { _Id = 3, Name = "Jerry", TestInt = 9, DateOfBirth = new DateTime(1981, 1, 1), _Age = 35, GUIY = Guid.NewGuid(), DoNotMapTest = "My wife is amazing", Access = Person.Permissions.CanRead|Person.Permissions.CanWrite|Person.Permissions.CanCreate},
new Person { _Id = 4, Name = "Jamie", TestInt = 9, DateOfBirth = new DateTime(1982, 1, 1), _Age = 35, GUIY = Guid.NewGuid(), DoNotMapTest = "My wife is amazing", Access = Person.Permissions.CanRead|Person.Permissions.CanWrite|Person.Permissions.CanCreate},
Expand Down
Loading