From 7adb00b51f5634578fa6130e082764d56e9823cb Mon Sep 17 00:00:00 2001 From: Ard van der Marel <109339598+Ard2025@users.noreply.github.com> Date: Sun, 24 Aug 2025 09:15:53 +0200 Subject: [PATCH 1/5] Replace JsonConstructor by MagicConstructor --- .../Exceptions/MagicConstructorException.cs | 3 +++ Magic.IndexedDb/Helpers/PropertyMappingCache.cs | 12 +++++++++--- .../SchemaAnnotations/MagicConstructorAttribute.cs | 6 ++++++ TestBase/Models/Person.cs | 2 +- 4 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 Magic.IndexedDb/Exceptions/MagicConstructorException.cs create mode 100644 Magic.IndexedDb/SchemaAnnotations/MagicConstructorAttribute.cs diff --git a/Magic.IndexedDb/Exceptions/MagicConstructorException.cs b/Magic.IndexedDb/Exceptions/MagicConstructorException.cs new file mode 100644 index 0000000..b6da8b0 --- /dev/null +++ b/Magic.IndexedDb/Exceptions/MagicConstructorException.cs @@ -0,0 +1,3 @@ +namespace Magic.IndexedDb.Exceptions; + +public class MagicConstructorException(string message) : Exception(message); \ No newline at end of file diff --git a/Magic.IndexedDb/Helpers/PropertyMappingCache.cs b/Magic.IndexedDb/Helpers/PropertyMappingCache.cs index cc9750d..fe61d0c 100644 --- a/Magic.IndexedDb/Helpers/PropertyMappingCache.cs +++ b/Magic.IndexedDb/Helpers/PropertyMappingCache.cs @@ -5,6 +5,7 @@ using System.Collections.Concurrent; using System.Reflection; using System.Text.Json.Serialization; +using Magic.IndexedDb.Exceptions; namespace Magic.IndexedDb.Helpers; @@ -37,9 +38,14 @@ public SearchPropEntry(Type type, Dictionary _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() != null); - if (jsonConstructor == null) + // 🔥 Pick the best constructor: Prefer MagicConstructor, then fall back to a parameterized one, else fallback to parameterless + if (constructors.Count(c => c.GetCustomAttribute() != null) > 1) + { + throw new MagicConstructorException("Only one magic constructor is allowed"); + } + + var magicConstructor = constructors.FirstOrDefault(c => c.GetCustomAttribute() != null); + if (magicConstructor == null) { Constructor = constructors.OrderByDescending(c => c.GetParameters().Length).FirstOrDefault(); } diff --git a/Magic.IndexedDb/SchemaAnnotations/MagicConstructorAttribute.cs b/Magic.IndexedDb/SchemaAnnotations/MagicConstructorAttribute.cs new file mode 100644 index 0000000..9dd5430 --- /dev/null +++ b/Magic.IndexedDb/SchemaAnnotations/MagicConstructorAttribute.cs @@ -0,0 +1,6 @@ +namespace Magic.IndexedDb.SchemaAnnotations; + +/// +/// Sets the preferred constructor for serialization for MagicDB +/// +public class MagicConstructorAttribute : Attribute; \ No newline at end of file diff --git a/TestBase/Models/Person.cs b/TestBase/Models/Person.cs index 8e5afd3..9fc4738 100644 --- a/TestBase/Models/Person.cs +++ b/TestBase/Models/Person.cs @@ -14,7 +14,7 @@ public class Nested public class Person : MagicTableTool, IMagicTable { - [JsonConstructor] + [MagicConstructor] public Person() { DoNotMapTest2 = "Test"; From a46fc71d551506ecc9c198094a80716d6211c980 Mon Sep 17 00:00:00 2001 From: Ard van der Marel <109339598+Ard2025@users.noreply.github.com> Date: Sun, 24 Aug 2025 11:33:30 +0200 Subject: [PATCH 2/5] SingleOrDefault rewrite MagicConstructor --- Magic.IndexedDb/Helpers/PropertyMappingCache.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Magic.IndexedDb/Helpers/PropertyMappingCache.cs b/Magic.IndexedDb/Helpers/PropertyMappingCache.cs index fe61d0c..615b655 100644 --- a/Magic.IndexedDb/Helpers/PropertyMappingCache.cs +++ b/Magic.IndexedDb/Helpers/PropertyMappingCache.cs @@ -39,16 +39,19 @@ public SearchPropEntry(Type type, Dictionary _proper } // 🔥 Pick the best constructor: Prefer MagicConstructor, then fall back to a parameterized one, else fallback to parameterless - if (constructors.Count(c => c.GetCustomAttribute() != null) > 1) + try { + var magicConstructor = constructors.SingleOrDefault(c => c.GetCustomAttribute() != null); + if (magicConstructor == null) + { + Constructor = constructors.OrderByDescending(c => c.GetParameters().Length).FirstOrDefault(); + } + } + + catch (InvalidOperationException){ throw new MagicConstructorException("Only one magic constructor is allowed"); } - var magicConstructor = constructors.FirstOrDefault(c => c.GetCustomAttribute() != null); - if (magicConstructor == null) - { - Constructor = constructors.OrderByDescending(c => c.GetParameters().Length).FirstOrDefault(); - } HasConstructorParameters = Constructor != null && Constructor.GetParameters().Length > 0; // 🔥 Cache constructor parameter mappings From dfe321adbb488823f3c7217ca9a65217e23cf107 Mon Sep 17 00:00:00 2001 From: Ard van der Marel <109339598+Ard2025@users.noreply.github.com> Date: Sun, 24 Aug 2025 13:29:19 +0200 Subject: [PATCH 3/5] improvement from yueyinqiu --- Magic.IndexedDb/Helpers/PropertyMappingCache.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Magic.IndexedDb/Helpers/PropertyMappingCache.cs b/Magic.IndexedDb/Helpers/PropertyMappingCache.cs index 615b655..efec81d 100644 --- a/Magic.IndexedDb/Helpers/PropertyMappingCache.cs +++ b/Magic.IndexedDb/Helpers/PropertyMappingCache.cs @@ -41,16 +41,13 @@ public SearchPropEntry(Type type, Dictionary _proper // 🔥 Pick the best constructor: Prefer MagicConstructor, then fall back to a parameterized one, else fallback to parameterless try { - var magicConstructor = constructors.SingleOrDefault(c => c.GetCustomAttribute() != null); - if (magicConstructor == null) - { - Constructor = constructors.OrderByDescending(c => c.GetParameters().Length).FirstOrDefault(); - } + Constructor = constructors.SingleOrDefault(c => c.GetCustomAttribute() is not null); } - - catch (InvalidOperationException){ + catch (InvalidOperationException) + { throw new MagicConstructorException("Only one magic constructor is allowed"); } + Constructor ??= constructors.OrderByDescending(c => c.GetParameters().Length).FirstOrDefault(); HasConstructorParameters = Constructor != null && Constructor.GetParameters().Length > 0; From 29c1b8fb34d5b62a7ef9c9fe2884454c5d8f9e60 Mon Sep 17 00:00:00 2001 From: Ard van der Marel <109339598+Ard2025@users.noreply.github.com> Date: Mon, 25 Aug 2025 22:07:00 +0200 Subject: [PATCH 4/5] Proper constructor work when deserializing T. properties that are not passed to the constructor will get initialized --- .../Models/MagicContractResolver.cs | 26 ++++++++++++++----- TestBase/Models/Person.cs | 3 ++- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/Magic.IndexedDb/Models/MagicContractResolver.cs b/Magic.IndexedDb/Models/MagicContractResolver.cs index 38de04b..6359fcd 100644 --- a/Magic.IndexedDb/Models/MagicContractResolver.cs +++ b/Magic.IndexedDb/Models/MagicContractResolver.cs @@ -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; @@ -62,25 +63,38 @@ internal class MagicContractResolver : JsonConverter private object CreateObjectFromDictionary(Type type, Dictionary propertyValues, SearchPropEntry search) { - // 🚀 If there's a constructor with parameters, use it - if (search.ConstructorParameterMappings.Count > 0) + object? obj = null; + List unInitializedProperties = new List(); + // 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)); } - 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()) ?? 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)) diff --git a/TestBase/Models/Person.cs b/TestBase/Models/Person.cs index 9fc4738..1c7c5f9 100644 --- a/TestBase/Models/Person.cs +++ b/TestBase/Models/Person.cs @@ -14,6 +14,7 @@ public class Nested public class Person : MagicTableTool, IMagicTable { + [MagicConstructor] public Person() { @@ -22,7 +23,7 @@ public Person() public Person(int _Id) { - DoNotMapTest2 = _Id.ToString(); + this._Id = _Id; } public List GetCompoundIndexes() => From c3483b094bb38019a3501bcca9e621cc19fc343e Mon Sep 17 00:00:00 2001 From: Ard van der Marel <109339598+Ard2025@users.noreply.github.com> Date: Mon, 25 Aug 2025 22:15:14 +0200 Subject: [PATCH 5/5] Small styling improvement --- Magic.IndexedDb/Models/MagicContractResolver.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Magic.IndexedDb/Models/MagicContractResolver.cs b/Magic.IndexedDb/Models/MagicContractResolver.cs index 6359fcd..6c16707 100644 --- a/Magic.IndexedDb/Models/MagicContractResolver.cs +++ b/Magic.IndexedDb/Models/MagicContractResolver.cs @@ -77,7 +77,10 @@ private object CreateObjectFromDictionary(Type type, Dictionary propertyValues.Remove(paramName); } else - constructorArgs[index] = GetDefaultValue(type.GetProperty(paramName)?.PropertyType ?? typeof(object)); + { + constructorArgs[index] = + GetDefaultValue(type.GetProperty(paramName)?.PropertyType ?? typeof(object)); + } } obj = search.InstanceCreator(constructorArgs);