Skip to content

Commit 083aacb

Browse files
authored
Merge pull request #5 from magiccodingman/magiccodingman/jsonfix
Magiccodingman/jsonfix
2 parents 788e6b5 + 13bf413 commit 083aacb

File tree

7 files changed

+302
-46
lines changed

7 files changed

+302
-46
lines changed
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
using System.Dynamic;
2+
using System.Linq.Expressions;
3+
4+
namespace Magic.IndexedDb.Helpers
5+
{
6+
public static class ExpandoToTypeConverter<T>
7+
{
8+
private static readonly Dictionary<string, Action<T, object?>> PropertySetters = new();
9+
private static readonly Dictionary<Type, bool> NonConcreteTypeCache = new();
10+
11+
private static readonly bool IsConcrete;
12+
private static readonly bool HasParameterlessConstructor;
13+
14+
static ExpandoToTypeConverter()
15+
{
16+
Type type = typeof(T);
17+
IsConcrete = !(type.IsAbstract || type.IsInterface);
18+
HasParameterlessConstructor = type.GetConstructor(Type.EmptyTypes) != null;
19+
20+
if (IsConcrete && HasParameterlessConstructor)
21+
{
22+
PrecomputePropertySetters(type);
23+
}
24+
}
25+
26+
private static void PrecomputePropertySetters(Type type)
27+
{
28+
foreach (var prop in type.GetProperties().Where(p => p.CanWrite))
29+
{
30+
var targetExp = Expression.Parameter(type);
31+
var valueExp = Expression.Parameter(typeof(object));
32+
33+
var convertedValueExp = Expression.Convert(valueExp, prop.PropertyType);
34+
35+
var propertySetterExp = Expression.Lambda<Action<T, object?>>(
36+
Expression.Assign(Expression.Property(targetExp, prop), convertedValueExp),
37+
targetExp, valueExp
38+
);
39+
40+
PropertySetters[prop.Name] = propertySetterExp.Compile();
41+
}
42+
}
43+
44+
public static T ConvertExpando(ExpandoObject expando)
45+
{
46+
Type type = typeof(T);
47+
48+
if (IsConcrete && HasParameterlessConstructor)
49+
{
50+
// Use the fastest method: Precomputed property setters
51+
var instance = Activator.CreateInstance<T>();
52+
var expandoDict = (IDictionary<string, object?>)expando;
53+
54+
foreach (var kvp in expandoDict)
55+
{
56+
if (PropertySetters.TryGetValue(kvp.Key, out var setter))
57+
{
58+
setter(instance, kvp.Value);
59+
}
60+
}
61+
62+
return instance;
63+
}
64+
else if (IsConcrete) // Concrete class without a parameterless constructor
65+
{
66+
var instance = Activator.CreateInstance(type);
67+
MagicSerializationHelper.PopulateObject(MagicSerializationHelper.SerializeObject(expando), instance);
68+
return (T)instance!;
69+
}
70+
else
71+
{
72+
// Last resort: If `T` is an interface or abstract class, fall back to full JSON deserialization
73+
var instance = MagicSerializationHelper.DeserializeObject<T>(MagicSerializationHelper.SerializeObject(expando))!;
74+
75+
// Check if we can cache this as a known type
76+
if (!NonConcreteTypeCache.ContainsKey(type))
77+
{
78+
NonConcreteTypeCache[type] = true;
79+
80+
// Dynamically compute property setters for this type
81+
PrecomputePropertySetters(type);
82+
}
83+
84+
return instance;
85+
}
86+
}
87+
}
88+
89+
90+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
using Magic.IndexedDb.Models;
2+
using System.Text.Json;
3+
4+
namespace Magic.IndexedDb.Helpers
5+
{
6+
/// <summary>
7+
/// Helper to serialize between the Magic Library content to the JS. To communicate with Dexie.JS -
8+
/// Note I left this public only to allow it to be targeted by external projects for testing.
9+
/// </summary>
10+
public static class MagicSerializationHelper
11+
{
12+
public static string SerializeObject<T>(T value, MagicJsonSerializationSettings? settings = null)
13+
{
14+
if (settings == null)
15+
settings = new MagicJsonSerializationSettings();
16+
17+
if (value == null)
18+
throw new ArgumentNullException(nameof(value), "Object cannot be null");
19+
20+
var options = settings.GetOptionsWithResolver<T>(); // Ensure the correct resolver is applied
21+
22+
return JsonSerializer.Serialize(value, options);
23+
}
24+
25+
public static T? DeserializeObject<T>(string json, MagicJsonSerializationSettings? settings = null)
26+
{
27+
if (string.IsNullOrWhiteSpace(json))
28+
throw new ArgumentException("JSON cannot be null or empty.", nameof(json));
29+
30+
if (settings == null)
31+
settings = new MagicJsonSerializationSettings();
32+
33+
var options = settings.GetOptionsWithResolver<T>(); // Ensure correct resolver for deserialization
34+
35+
return JsonSerializer.Deserialize<T>(json, options);
36+
}
37+
38+
public static void PopulateObject<T>(T source, T target)
39+
{
40+
if (source == null || target == null)
41+
throw new ArgumentNullException("Source and target cannot be null");
42+
43+
var json = JsonSerializer.Serialize(source);
44+
var deserialized = JsonSerializer.Deserialize<T>(json);
45+
46+
foreach (var prop in typeof(T).GetProperties())
47+
{
48+
var value = prop.GetValue(deserialized);
49+
prop.SetValue(target, value);
50+
}
51+
}
52+
}
53+
54+
55+
}

Magic.IndexedDb/IndexDbManager.cs

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@
77
using Magic.IndexedDb.Models;
88
using Magic.IndexedDb.SchemaAnnotations;
99
using Microsoft.JSInterop;
10-
using Newtonsoft.Json;
11-
using Newtonsoft.Json.Linq;
12-
using Newtonsoft.Json.Serialization;
10+
using System.Text.Json.Nodes;
1311

1412
namespace Magic.IndexedDb
1513
{
@@ -233,9 +231,9 @@ public async Task AddRangeAsync<T>(
233231
T? myClass = null;
234232

235233
object? processedRecord = await ProcessRecordAsync(record, cancellationToken);
236-
if (processedRecord is ExpandoObject)
234+
if (processedRecord is ExpandoObject expando)
237235
{
238-
myClass = JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(processedRecord));
236+
myClass = ExpandoToTypeConverter<T>.ConvertExpando(expando);
239237
IsExpando = true;
240238
}
241239
else
@@ -376,7 +374,6 @@ public MagicQuery<T> Where<T>(Expression<Func<T, bool>> predicate) where T : cla
376374

377375
// Preprocess the predicate to break down Any and All expressions
378376
var preprocessedPredicate = PreprocessPredicate(predicate);
379-
var asdf = preprocessedPredicate.ToString();
380377
CollectBinaryExpressions(preprocessedPredicate.Body, preprocessedPredicate, query.JsonQueries);
381378

382379
return query;
@@ -397,7 +394,7 @@ private Expression<Func<T, bool>> PreprocessPredicate<T>(Expression<Func<T, bool
397394
string? jsonQueryAdditions = null;
398395
if (query != null && query.storedMagicQueries != null && query.storedMagicQueries.Count > 0)
399396
{
400-
jsonQueryAdditions = Newtonsoft.Json.JsonConvert.SerializeObject(query.storedMagicQueries.ToArray());
397+
jsonQueryAdditions = MagicSerializationHelper.SerializeObject(query.storedMagicQueries.ToArray());
401398
}
402399
var propertyMappings = ManagerHelper.GeneratePropertyMapping<T>();
403400
IList<Dictionary<string, object>>? ListToConvert =
@@ -510,9 +507,13 @@ private TRecord ConvertIndexedDbRecordToCRecord<TRecord>(Dictionary<string, obje
510507

511508
private string GetJsonQueryFromExpression<T>(Expression<Func<T, bool>> predicate) where T : class
512509
{
513-
var serializerSettings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() };
514-
var conditions = new List<JObject>();
515-
var orConditions = new List<List<JObject>>();
510+
var serializerSettings = new MagicJsonSerializationSettings
511+
{
512+
UseCamelCase = true // Equivalent to setting CamelCasePropertyNamesContractResolver
513+
};
514+
515+
var conditions = new List<JsonObject>();
516+
var orConditions = new List<List<JsonObject>>();
516517

517518
void TraverseExpression(Expression expression, bool inOrBranch = false)
518519
{
@@ -656,14 +657,14 @@ void AddConditionInternal(MemberExpression? left, ConstantExpression? right, str
656657
columnName = propertyInfo.GetPropertyColumnName<MagicPrimaryKeyAttribute>();
657658

658659
bool _isString = false;
659-
JToken? valSend = null;
660+
JsonNode? valSend = null;
660661
if (right != null && right.Value != null)
661662
{
662-
valSend = JToken.FromObject(right.Value);
663+
valSend = JsonValue.Create(right.Value);
663664
_isString = right.Value is string;
664665
}
665666

666-
var jsonCondition = new JObject
667+
var jsonCondition = new JsonObject
667668
{
668669
{ "property", columnName },
669670
{ "operation", operation },
@@ -677,7 +678,7 @@ void AddConditionInternal(MemberExpression? left, ConstantExpression? right, str
677678
var currentOrConditions = orConditions.LastOrDefault();
678679
if (currentOrConditions == null)
679680
{
680-
currentOrConditions = new List<JObject>();
681+
currentOrConditions = new List<JsonObject>();
681682
orConditions.Add(currentOrConditions);
682683
}
683684
currentOrConditions.Add(jsonCondition);
@@ -697,7 +698,7 @@ void AddConditionInternal(MemberExpression? left, ConstantExpression? right, str
697698
orConditions.Add(conditions);
698699
}
699700

700-
return JsonConvert.SerializeObject(orConditions, serializerSettings);
701+
return MagicSerializationHelper.SerializeObject(orConditions, serializerSettings);
701702
}
702703

703704
/// <summary>

Magic.IndexedDb/Magic.IndexedDb.csproj

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk.Razor">
1+
<Project Sdk="Microsoft.NET.Sdk.Razor">
22

33
<PropertyGroup>
44
<TargetFramework>net8.0</TargetFramework>
@@ -43,7 +43,6 @@
4343

4444
<ItemGroup>
4545
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="8.0.11" />
46-
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
4746
</ItemGroup>
4847

4948
</Project>

Magic.IndexedDb/Models/CustomContractResolver.cs

Lines changed: 0 additions & 29 deletions
This file was deleted.
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
using Magic.IndexedDb.SchemaAnnotations;
2+
using System.Collections.Concurrent;
3+
using System.Reflection;
4+
using System.Text.Json.Serialization;
5+
using System.Text.Json;
6+
using System.Collections;
7+
8+
namespace Magic.IndexedDb.Models
9+
{
10+
internal class MagicContractResolver<T> : JsonConverter<T>
11+
{
12+
private static readonly ConcurrentDictionary<MemberInfo, bool> _cachedIgnoredProperties = new();
13+
14+
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
15+
{
16+
// Defer deserialization back to built-in handling to ensure correctness
17+
return JsonSerializer.Deserialize<T>(ref reader, options);
18+
}
19+
20+
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
21+
{
22+
if (value == null)
23+
{
24+
writer.WriteNullValue();
25+
return;
26+
}
27+
28+
// Handle simple or primitive types directly
29+
if (IsSimpleType(typeof(T)))
30+
{
31+
JsonSerializer.Serialize(writer, value, options);
32+
return;
33+
}
34+
35+
// Handle collections
36+
if (value is IEnumerable enumerable && typeof(T) != typeof(string))
37+
{
38+
writer.WriteStartArray();
39+
foreach (var item in enumerable)
40+
{
41+
if (item == null)
42+
writer.WriteNullValue();
43+
else
44+
JsonSerializer.Serialize(writer, item, item.GetType(), options);
45+
}
46+
writer.WriteEndArray();
47+
return;
48+
}
49+
50+
// Handle complex objects
51+
writer.WriteStartObject();
52+
foreach (var property in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance))
53+
{
54+
if (ShouldIgnoreProperty(property))
55+
continue;
56+
57+
var propName = options.PropertyNamingPolicy?.ConvertName(property.Name) ?? property.Name;
58+
59+
if (property.GetIndexParameters().Length > 0)
60+
continue; // Skip indexers entirely
61+
62+
object? propValue;
63+
try
64+
{
65+
propValue = property.GetValue(value);
66+
}
67+
catch
68+
{
69+
// Fallback: write null if getting property fails
70+
propValue = null;
71+
}
72+
73+
writer.WritePropertyName(propName);
74+
if (propValue == null)
75+
writer.WriteNullValue();
76+
else
77+
JsonSerializer.Serialize(writer, propValue, propValue.GetType(), options);
78+
}
79+
writer.WriteEndObject();
80+
}
81+
82+
private static bool ShouldIgnoreProperty(PropertyInfo property)
83+
{
84+
return _cachedIgnoredProperties.GetOrAdd(property, prop =>
85+
prop.GetCustomAttributes(inherit: true)
86+
.Any(a => a.GetType().FullName == typeof(MagicNotMappedAttribute).FullName));
87+
}
88+
89+
private static bool IsSimpleType(Type type)
90+
{
91+
return type.IsPrimitive ||
92+
type.IsEnum ||
93+
type == typeof(string) ||
94+
type == typeof(decimal) ||
95+
type == typeof(DateTime) ||
96+
type == typeof(DateTimeOffset) ||
97+
type == typeof(Guid) ||
98+
type == typeof(Uri) ||
99+
type == typeof(TimeSpan);
100+
}
101+
}
102+
103+
}

0 commit comments

Comments
 (0)