Skip to content

Commit d4171b3

Browse files
A long journey this has been
1 parent 29a759e commit d4171b3

File tree

3 files changed

+174
-34
lines changed

3 files changed

+174
-34
lines changed

Magic.IndexedDb/Helpers/PropertyMappingCache.cs

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,43 @@
1212

1313
namespace Magic.IndexedDb.Helpers
1414
{
15+
public struct SearchPropEntry
16+
{
17+
public SearchPropEntry(Dictionary<string, MagicPropertyEntry> _propertyEntries)
18+
{
19+
propertyEntries = _propertyEntries;
20+
jsNameToCsName = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
21+
22+
foreach (var entry in propertyEntries)
23+
{
24+
jsNameToCsName[entry.Value.JsPropertyName] = entry.Value.CsharpPropertyName;
25+
}
26+
}
27+
28+
public Dictionary<string, MagicPropertyEntry> propertyEntries { get; }
29+
public Dictionary<string, string> jsNameToCsName { get; }
30+
}
31+
32+
33+
1534
public static class PropertyMappingCache
1635
{
17-
internal static readonly ConcurrentDictionary<string, Dictionary<string, MagicPropertyEntry>> _propertyCache = new();
36+
internal static readonly ConcurrentDictionary<string, SearchPropEntry> _propertyCache = new();
37+
38+
39+
public static MagicPropertyEntry GetPrimaryKeyOfType(Type type)
40+
{
41+
var properties = GetTypeOfTProperties(type);
42+
foreach (var prop in properties.propertyEntries)
43+
{
44+
if (prop.Value.PrimaryKey)
45+
return prop.Value;
46+
}
1847

48+
throw new Exception($"The provided type doesn't have a primary key: {type.FullName}");
49+
}
1950

20-
public static Dictionary<string, MagicPropertyEntry> GetTypeOfTProperties(Type type)
51+
public static SearchPropEntry GetTypeOfTProperties(Type type)
2152
{
2253
EnsureTypeIsCached(type);
2354
if (_propertyCache.TryGetValue(type.FullName!, out var properties))
@@ -132,10 +163,9 @@ public static string GetCsharpPropertyName(string jsPropertyName, Type type)
132163

133164
try
134165
{
135-
if (_propertyCache.TryGetValue(typeKey, out var properties) &&
136-
properties.TryGetValue(jsPropertyName, out var entry))
166+
if (_propertyCache.TryGetValue(typeKey, out var search))
137167
{
138-
return entry.CsharpPropertyName;
168+
return search.GetCsharpPropertyName(jsPropertyName);
139169
}
140170
}
141171
catch (Exception ex)
@@ -146,6 +176,25 @@ public static string GetCsharpPropertyName(string jsPropertyName, Type type)
146176
return jsPropertyName; // Fallback to original name if not found
147177
}
148178

179+
public static string GetCsharpPropertyName(this SearchPropEntry propCachee, string jsPropertyName)
180+
{
181+
try
182+
{
183+
if (propCachee.jsNameToCsName.TryGetValue(jsPropertyName, out var csName)
184+
&& propCachee.propertyEntries.TryGetValue(csName, out var entry))
185+
{
186+
return entry.CsharpPropertyName;
187+
}
188+
}
189+
catch (Exception ex)
190+
{
191+
throw new Exception($"Error retrieving C# property name for JS property '{jsPropertyName}'.", ex);
192+
}
193+
194+
return jsPropertyName; // Fallback to original name if not found
195+
}
196+
197+
149198
/// <summary>
150199
/// Gets the JavaScript property name (ColumnName) given a C# property name.
151200
/// </summary>
@@ -175,7 +224,7 @@ public static string GetJsPropertyName(string csharpPropertyName, Type type)
175224
try
176225
{
177226
if (_propertyCache.TryGetValue(typeKey, out var properties) &&
178-
properties.TryGetValue(csharpPropertyName, out var entry))
227+
properties.propertyEntries.TryGetValue(csharpPropertyName, out var entry))
179228
{
180229
return entry.JsPropertyName;
181230
}
@@ -207,7 +256,7 @@ public static MagicPropertyEntry GetPropertyEntry(string propertyName, Type type
207256
try
208257
{
209258
if (_propertyCache.TryGetValue(typeKey, out var properties) &&
210-
properties.TryGetValue(propertyName, out var entry))
259+
properties.propertyEntries.TryGetValue(propertyName, out var entry))
211260
{
212261
return entry;
213262
}
@@ -257,7 +306,7 @@ internal static void EnsureTypeIsCached(Type type)
257306
SchemaHelper.EnsureSchemaIsCached(type);
258307

259308
// Initialize the dictionary for this type
260-
var propertyEntries = new Dictionary<string, MagicPropertyEntry>();
309+
var propertyEntries = new Dictionary<string, MagicPropertyEntry>(StringComparer.OrdinalIgnoreCase);
261310

262311
List<MagicPropertyEntry> newMagicPropertyEntry = new List<MagicPropertyEntry>();
263312
foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy))
@@ -288,7 +337,7 @@ internal static void EnsureTypeIsCached(Type type)
288337
}
289338

290339
// Cache the properties for this type
291-
_propertyCache[typeKey] = propertyEntries;
340+
_propertyCache[typeKey] = new SearchPropEntry(propertyEntries);
292341

293342
var complexTypes = GetAllNestedComplexTypes(newMagicPropertyEntry.Select(x => x.Property));
294343
if (complexTypes != null && complexTypes.Any())

Magic.IndexedDb/Models/MagicContractResolver.cs

Lines changed: 109 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,28 @@ internal class MagicContractResolver<T> : JsonConverter<T>
1919
if (reader.TokenType == JsonTokenType.Null)
2020
return default; // ✅ Return default(T) if null is encountered
2121

22+
// ✅ Handle primitive types before assuming it's complex
23+
if (PropertyMappingCache.IsSimpleType(typeToConvert))
24+
{
25+
return (T?)ReadSimpleType(ref reader, typeToConvert);
26+
}
27+
28+
// ✅ Explicitly check if the type is `JsonElement`
29+
if (typeToConvert == typeof(JsonElement))
30+
{
31+
JsonElement element = JsonSerializer.Deserialize<JsonElement>(ref reader); // Extract JsonElement
32+
33+
// ✅ Re-run null check for JsonElement
34+
if (IsSimpleJsonNull(element))
35+
return default;
36+
37+
// ✅ Re-run primitive check for JsonElement
38+
if (IsSimpleJsonElement(element))
39+
return (T?)(object)element; // 🚀 Directly cast JsonElement to T
40+
41+
return (T?)(object)JsonSerializer.Deserialize<JsonElement>(ref reader);
42+
}
43+
2244
// ✅ Handle root-level arrays correctly
2345
if (reader.TokenType == JsonTokenType.StartArray)
2446
{
@@ -28,7 +50,66 @@ internal class MagicContractResolver<T> : JsonConverter<T>
2850
return (T?)ReadIEnumerable(ref reader, typeToConvert, options);
2951
}
3052

31-
return (T?)ReadComplexObject(ref reader, typeToConvert, options);
53+
// ✅ If it's neither a primitive nor an array, assume it's a complex object
54+
if (reader.TokenType == JsonTokenType.StartObject)
55+
{
56+
return (T?)ReadComplexObject(ref reader, typeToConvert, options);
57+
}
58+
59+
throw new JsonException($"Unexpected JSON token: {reader.TokenType} when deserializing {typeToConvert.Name}.");
60+
}
61+
62+
private object CreateObjectFromDictionary(Type type, Dictionary<string, object?> propertyValues)
63+
{
64+
var constructor = type.GetConstructors().FirstOrDefault();
65+
66+
// 🔥 If there's a constructor AND it has parameters, we use it (for records like QuotaUsage)
67+
if (constructor != null && constructor.GetParameters().Length > 0)
68+
{
69+
var parameters = constructor.GetParameters()
70+
.Select(p =>
71+
{
72+
if (propertyValues.TryGetValue(p.Name!, out var value))
73+
return value;
74+
75+
var matchedKey = propertyValues.Keys
76+
.FirstOrDefault(k => string.Equals(k, p.Name, StringComparison.OrdinalIgnoreCase));
77+
78+
return matchedKey != null ? propertyValues[matchedKey] : GetDefaultValue(p.ParameterType);
79+
})
80+
.ToArray();
81+
82+
return constructor.Invoke(parameters);
83+
}
84+
85+
// 🔥 Handle non-constructor classes (e.g., Person class)
86+
var instance = Activator.CreateInstance(type);
87+
if (instance == null)
88+
throw new InvalidOperationException($"Failed to create instance of type {type.Name}.");
89+
90+
foreach (var (propName, value) in propertyValues)
91+
{
92+
var property = type.GetProperty(propName, BindingFlags.Public | BindingFlags.Instance);
93+
if (property != null && property.CanWrite)
94+
{
95+
property.SetValue(instance, value);
96+
}
97+
}
98+
99+
return instance;
100+
}
101+
102+
103+
private bool IsSimpleJsonElement(JsonElement element)
104+
{
105+
return element.ValueKind == JsonValueKind.String ||
106+
element.ValueKind == JsonValueKind.Number ||
107+
element.ValueKind == JsonValueKind.True ||
108+
element.ValueKind == JsonValueKind.False;
109+
}
110+
private bool IsSimpleJsonNull(JsonElement element)
111+
{
112+
return element.ValueKind == JsonValueKind.Null;
32113
}
33114

34115
/// <summary>
@@ -42,13 +123,18 @@ internal class MagicContractResolver<T> : JsonConverter<T>
42123
if (reader.TokenType != JsonTokenType.StartObject)
43124
throw new JsonException($"Expected StartObject token for type {type.Name}.");
44125

45-
var instance = Activator.CreateInstance(type);
46-
var properties = PropertyMappingCache.GetTypeOfTProperties(type);
126+
// 🔥 Step 1: Create a dictionary to store extracted values
127+
var propertyValues = new Dictionary<string, object?>();
47128

129+
var properties = PropertyMappingCache.GetTypeOfTProperties(type);
48130
while (reader.Read())
49131
{
50132
if (reader.TokenType == JsonTokenType.EndObject)
51-
return instance;
133+
{
134+
// 🔥 Step 3: Convert the dictionary into the final object
135+
var result = CreateObjectFromDictionary(type, propertyValues);
136+
return result;
137+
}
52138

53139
if (reader.TokenType != JsonTokenType.PropertyName)
54140
throw new JsonException("Expected PropertyName token.");
@@ -57,39 +143,40 @@ internal class MagicContractResolver<T> : JsonConverter<T>
57143
if (!reader.Read())
58144
throw new JsonException("Unexpected end of JSON.");
59145

60-
// 🔥 Resolve correct C# property name dynamically
61-
string csharpPropertyName = PropertyMappingCache.GetCsharpPropertyName(jsonPropertyName, type);
62146

63-
if (properties.TryGetValue(csharpPropertyName, out var mpe))
147+
string csharpPropertyName = properties.GetCsharpPropertyName(jsonPropertyName);
148+
149+
//string csharpPropertyName = PropertyMappingCache.GetCsharpPropertyName(jsonPropertyName, type);
150+
151+
if (properties.propertyEntries.TryGetValue(csharpPropertyName, out var mpe))
64152
{
65153
if (mpe.NotMapped)
66154
{
67155
reader.Skip();
68156
continue;
69157
}
70158

159+
// 🔥 Step 2: Extract values and store them in the dictionary
71160
object? value = ReadPropertyValue(ref reader, mpe, options);
72-
if (value != null)
73-
{
74-
try
75-
{
76-
mpe.Setter(instance, value);
77-
}
78-
catch
79-
{
80-
// Prevent hard crash but log if needed
81-
}
82-
}
161+
propertyValues[csharpPropertyName] = value;
83162
}
84163
else
85164
{
86165
reader.Skip();
87166
}
88167
}
89168

90-
return instance;
169+
throw new JsonException("Unexpected end of JSON while reading an object.");
170+
}
171+
172+
private object? GetDefaultValue(Type type)
173+
{
174+
return type.IsValueType ? Activator.CreateInstance(type) : null;
91175
}
92176

177+
178+
179+
93180
/// <summary>
94181
/// Reads and assigns a property value, detecting collections, simple types, and complex objects.
95182
/// </summary>
@@ -174,8 +261,6 @@ internal class MagicContractResolver<T> : JsonConverter<T>
174261
return list;
175262
}
176263

177-
178-
179264
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
180265
{
181266
if (value == null)
@@ -202,7 +287,7 @@ public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions
202287

203288
// 🔥 Handle complex objects recursively
204289
writer.WriteStartObject();
205-
SerializeComplexProperties(writer, value, properties, options);
290+
SerializeComplexProperties(writer, value, properties.propertyEntries, options);
206291
writer.WriteEndObject();
207292
}
208293

@@ -260,7 +345,7 @@ private bool SerializeIEnumerable(Utf8JsonWriter writer, object value, JsonSeria
260345
{
261346
var nestedProperties = PropertyMappingCache.GetTypeOfTProperties(itemType);
262347
writer.WriteStartObject();
263-
SerializeComplexProperties(writer, item, nestedProperties, options);
348+
SerializeComplexProperties(writer, item, nestedProperties.propertyEntries, options);
264349
writer.WriteEndObject();
265350
}
266351
else
@@ -356,7 +441,7 @@ private void SerializeComplexProperties(Utf8JsonWriter writer, object value, Dic
356441
{
357442
var nestedProps = PropertyMappingCache.GetTypeOfTProperties(propValue.GetType());
358443
writer.WriteStartObject();
359-
SerializeComplexProperties(writer, propValue, nestedProps, options);
444+
SerializeComplexProperties(writer, propValue, nestedProps.propertyEntries, options);
360445
writer.WriteEndObject();
361446
}
362447
}

Magic.IndexedDb/Models/Structs/MagicPropertyEntry.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ public MagicPropertyEntry(PropertyInfo property, IColumnNamed? columnNamedAttrib
2828

2929
IsComplexType = PropertyMappingCache.IsComplexType(property.PropertyType);
3030

31+
// 🔥 Identify if this property is a constructor parameter
32+
var declaringType = property.DeclaringType!;
33+
var constructor = declaringType.GetConstructors().FirstOrDefault();
34+
3135
// 🔥 Create delegates for performance 🔥
3236
if (property.CanRead)
3337
{
@@ -53,7 +57,9 @@ public MagicPropertyEntry(PropertyInfo property, IColumnNamed? columnNamedAttrib
5357
/// camel case if that's the current json setting. These must stay
5458
/// as they were initially designated.
5559
/// </summary>
56-
public bool NeverCamelCase { get
60+
public bool NeverCamelCase
61+
{
62+
get
5763
{
5864
if (PrimaryKey || UniqueIndex || Indexed)
5965
return true;

0 commit comments

Comments
 (0)