399 lines
16 KiB
C#
399 lines
16 KiB
C#
using System.Collections;
|
|
using System.Globalization;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Text.Json;
|
|
using AyCode.Core.Helpers;
|
|
using static AyCode.Core.Helpers.JsonUtilities;
|
|
|
|
namespace AyCode.Core.Serializers.Jsons;
|
|
|
|
public static partial class AcJsonDeserializer
|
|
{
|
|
#region With Reference Handling (JsonElement Path)
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static object? ReadValue(in JsonElement element, in Type targetType, DeserializationContext context, int depth)
|
|
{
|
|
var kind = element.ValueKind;
|
|
if (kind == JsonValueKind.Object) return ReadObject(element, targetType, context, depth);
|
|
if (kind == JsonValueKind.Array) return ReadArray(element, targetType, context, depth);
|
|
if (kind == JsonValueKind.Null || kind == JsonValueKind.Undefined) return null;
|
|
return ReadPrimitive(element, targetType, kind);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static object? ReadObject(in JsonElement element, in Type targetType, DeserializationContext context, int depth)
|
|
{
|
|
// Check for $ref first - support both string (Newtonsoft) and int formats
|
|
if (element.TryGetProperty(RefPropertyUtf8, out var refElement))
|
|
{
|
|
var refId = ParseRefId(refElement);
|
|
return context.TryGetReferencedObject(refId, out var refObj) ? refObj : new DeferredReference(refId, targetType);
|
|
}
|
|
|
|
if (depth > context.MaxDepth) return null;
|
|
|
|
if (IsDictionaryType(targetType, out var keyType, out var valueType))
|
|
return ReadDictionary(element, keyType!, valueType!, context, depth);
|
|
|
|
var metadata = GetTypeMetadata(targetType);
|
|
|
|
var instance = metadata.CompiledConstructor?.Invoke();
|
|
if (instance == null)
|
|
{
|
|
try { instance = Activator.CreateInstance(targetType); }
|
|
catch (MissingMethodException ex)
|
|
{
|
|
throw new AcJsonDeserializationException(
|
|
$"Cannot deserialize type '{targetType.FullName}' because it does not have a parameterless constructor.",
|
|
null, targetType, ex);
|
|
}
|
|
}
|
|
|
|
if (instance == null) return null;
|
|
|
|
// Check for $id and register - support both string (Newtonsoft) and int formats
|
|
if (element.TryGetProperty(IdPropertyUtf8, out var idElement))
|
|
context.RegisterObject(ParseRefId(idElement), instance);
|
|
|
|
PopulateObjectInternal(element, instance, metadata, context, depth);
|
|
|
|
// ChainMode: Use cached IId info from metadata (no runtime reflection!)
|
|
if (context.IsChainMode && metadata.IsIId && metadata.IdGetter != null && metadata.IdType != null)
|
|
{
|
|
var id = metadata.IdGetter(instance);
|
|
if (id != null && !IsDefaultValue(id, metadata.IdType))
|
|
{
|
|
// Check if we already have this object
|
|
if (context.ChainTracker!.TryGetObject(targetType, id, out var existingObj))
|
|
{
|
|
// Update existing object's properties and return it
|
|
CopyPropertiesJson(instance, existingObj!, metadata);
|
|
return existingObj;
|
|
}
|
|
|
|
// Register this new object
|
|
context.ChainTracker.TryRegisterIIdObject(instance);
|
|
}
|
|
}
|
|
|
|
return instance;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse $id/$ref value - supports both string (Newtonsoft format) and int formats.
|
|
/// Only numeric values are supported. Non-numeric string references (e.g., "tag1") are not supported.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static int ParseRefId(in JsonElement element)
|
|
{
|
|
if (element.ValueKind == JsonValueKind.Number)
|
|
return element.GetInt32();
|
|
|
|
if (element.ValueKind == JsonValueKind.String)
|
|
{
|
|
var str = element.GetString();
|
|
if (str == null) return 0;
|
|
|
|
if (int.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
|
|
return result;
|
|
|
|
throw new AcJsonDeserializationException(
|
|
$"Non-numeric $id/$ref value '{str}' is not supported. Only numeric reference IDs are supported. " +
|
|
$"If you need string-based references, please use a different serializer or convert the JSON to use numeric IDs.",
|
|
str, typeof(int));
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Copies properties from source to target using JSON metadata.
|
|
/// </summary>
|
|
private static void CopyPropertiesJson(object source, object target, JsonDeserializeTypeMetadata metadata)
|
|
{
|
|
var props = metadata.PropertiesArray;
|
|
for (var i = 0; i < props.Length; i++)
|
|
{
|
|
var prop = props[i];
|
|
var value = prop.GetValue(source);
|
|
if (value != null)
|
|
prop.SetValue(target, value);
|
|
}
|
|
}
|
|
|
|
private static void PopulateObjectInternal(in JsonElement element, object target, JsonDeserializeTypeMetadata metadata, DeserializationContext context, int depth)
|
|
{
|
|
var propsDict = metadata.PropertySettersFrozen;
|
|
var nextDepth = depth + 1;
|
|
|
|
foreach (var jsonProp in element.EnumerateObject())
|
|
{
|
|
var propName = jsonProp.Name;
|
|
|
|
// Skip $ properties
|
|
if (propName.Length > 0 && propName[0] == '$') continue;
|
|
|
|
if (!propsDict.TryGetValue(propName, out var propInfo)) continue;
|
|
|
|
var value = ReadValue(jsonProp.Value, propInfo.PropertyType, context, nextDepth);
|
|
|
|
if (value is DeferredReference deferred)
|
|
context.AddPropertyToResolve(target, propInfo, deferred.RefId);
|
|
else
|
|
propInfo.SetValue(target, value);
|
|
}
|
|
}
|
|
|
|
private static void PopulateObjectInternalMerge(in JsonElement element, object target, JsonDeserializeTypeMetadata metadata, DeserializationContext context, int depth)
|
|
{
|
|
var propsDict = metadata.PropertySettersFrozen;
|
|
var nextDepth = depth + 1;
|
|
var maxDepthReached = nextDepth > context.MaxDepth;
|
|
|
|
foreach (var jsonProp in element.EnumerateObject())
|
|
{
|
|
var propName = jsonProp.Name;
|
|
|
|
// Skip $ properties
|
|
if (propName.Length > 0 && propName[0] == '$') continue;
|
|
|
|
if (!propsDict.TryGetValue(propName, out var propInfo)) continue;
|
|
|
|
var propValue = jsonProp.Value;
|
|
var propValueKind = propValue.ValueKind;
|
|
|
|
if (maxDepthReached)
|
|
{
|
|
if (propValueKind != JsonValueKind.Object && propValueKind != JsonValueKind.Array)
|
|
{
|
|
var primitiveValue = ReadPrimitive(propValue, propInfo.PropertyType, propValueKind);
|
|
propInfo.SetValue(target, primitiveValue);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Handle IId collection merge
|
|
if (propInfo.IsIIdCollection && propValueKind == JsonValueKind.Array)
|
|
{
|
|
var existingCollection = propInfo.GetValue(target);
|
|
if (existingCollection != null)
|
|
{
|
|
MergeIIdCollection(propValue, existingCollection, propInfo, context, depth);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Handle nested objects
|
|
if (propValueKind == JsonValueKind.Object)
|
|
{
|
|
// Check for $ref
|
|
if (propValue.TryGetProperty(RefPropertyUtf8, out _))
|
|
{
|
|
var value = ReadValue(propValue, propInfo.PropertyType, context, nextDepth);
|
|
if (value is DeferredReference deferred)
|
|
context.AddPropertyToResolve(target, propInfo, deferred.RefId);
|
|
else
|
|
propInfo.SetValue(target, value);
|
|
continue;
|
|
}
|
|
|
|
// Merge into existing object
|
|
if (!propInfo.PropertyType.IsPrimitive && !ReferenceEquals(propInfo.PropertyType, StringType))
|
|
{
|
|
var existingObj = propInfo.GetValue(target);
|
|
if (existingObj != null)
|
|
{
|
|
var nestedMetadata = GetTypeMetadata(propInfo.PropertyType);
|
|
PopulateObjectInternalMerge(propValue, existingObj, nestedMetadata, context, nextDepth);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
var value2 = ReadValue(propValue, propInfo.PropertyType, context, nextDepth);
|
|
|
|
if (value2 is DeferredReference deferred2)
|
|
context.AddPropertyToResolve(target, propInfo, deferred2.RefId);
|
|
else
|
|
propInfo.SetValue(target, value2);
|
|
}
|
|
}
|
|
|
|
private static void PopulateList(in JsonElement arrayElement, IList targetList, in Type listType, DeserializationContext context, int depth)
|
|
{
|
|
if (depth > context.MaxDepth) return;
|
|
|
|
var elementType = GetCollectionElementType(listType);
|
|
if (elementType == null) return;
|
|
|
|
var acObservable = targetList as IAcObservableCollection;
|
|
acObservable?.BeginUpdate();
|
|
|
|
try
|
|
{
|
|
targetList.Clear();
|
|
var nextDepth = depth + 1;
|
|
|
|
// ChainMode: Use cached IId info from element type metadata (no runtime reflection!)
|
|
JsonDeserializeTypeMetadata? elementMetadata = null;
|
|
if (context.IsChainMode && !IsPrimitiveOrStringFast(elementType))
|
|
{
|
|
elementMetadata = GetTypeMetadata(elementType);
|
|
}
|
|
|
|
foreach (var item in arrayElement.EnumerateArray())
|
|
{
|
|
var value = ReadValue(item, elementType, context, nextDepth);
|
|
|
|
// ChainMode: Check if we already have this IId object using cached metadata
|
|
if (context.IsChainMode && value != null && elementMetadata != null &&
|
|
elementMetadata.IsIId && elementMetadata.IdGetter != null && elementMetadata.IdType != null)
|
|
{
|
|
var id = elementMetadata.IdGetter(value);
|
|
if (id != null && !IsDefaultValue(id, elementMetadata.IdType))
|
|
{
|
|
if (context.ChainTracker!.TryGetObject(elementType, id, out var existingObj))
|
|
{
|
|
// Use existing object instead of new one
|
|
targetList.Add(existingObj);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (value != null)
|
|
targetList.Add(value);
|
|
}
|
|
}
|
|
finally { acObservable?.EndUpdate(); }
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static object? ReadArray(in JsonElement element, in Type targetType, DeserializationContext context, int depth)
|
|
{
|
|
if (depth > context.MaxDepth) return null;
|
|
|
|
var elementType = GetCollectionElementType(targetType);
|
|
if (elementType == null) return null;
|
|
|
|
var nextDepth = depth + 1;
|
|
|
|
if (targetType.IsArray)
|
|
{
|
|
var list = GetOrCreateListFactory(elementType)();
|
|
foreach (var item in element.EnumerateArray())
|
|
list.Add(ReadValue(item, elementType, context, nextDepth));
|
|
|
|
var array = Array.CreateInstance(elementType, list.Count);
|
|
list.CopyTo(array, 0);
|
|
return array;
|
|
}
|
|
|
|
IList? targetList = null;
|
|
try
|
|
{
|
|
var instance = Activator.CreateInstance(targetType);
|
|
if (instance is IList list) targetList = list;
|
|
}
|
|
catch { /* Fallback to List<T> */ }
|
|
|
|
targetList ??= GetOrCreateListFactory(elementType)();
|
|
|
|
var acObservable = targetList as IAcObservableCollection;
|
|
acObservable?.BeginUpdate();
|
|
|
|
try
|
|
{
|
|
foreach (var item in element.EnumerateArray())
|
|
targetList.Add(ReadValue(item, elementType, context, nextDepth));
|
|
}
|
|
finally { acObservable?.EndUpdate(); }
|
|
|
|
return targetList;
|
|
}
|
|
|
|
private static void MergeIIdCollection(in JsonElement arrayElement, object existingCollection, PropertySetterInfo propInfo, DeserializationContext context, int depth)
|
|
{
|
|
var elementType = propInfo.ElementType!;
|
|
var idGetter = propInfo.ElementIdGetter!;
|
|
var idType = propInfo.ElementIdType!;
|
|
|
|
var existingList = (IList)existingCollection;
|
|
var count = existingList.Count;
|
|
|
|
var acObservable = existingList as IAcObservableCollection;
|
|
acObservable?.BeginUpdate();
|
|
|
|
try
|
|
{
|
|
Dictionary<object, object>? existingById = null;
|
|
if (count > 0)
|
|
{
|
|
existingById = new Dictionary<object, object>(count);
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
var item = existingList[i];
|
|
if (item != null)
|
|
{
|
|
var id = idGetter(item);
|
|
if (id != null && !IsDefaultValue(id, idType))
|
|
existingById[id] = item;
|
|
}
|
|
}
|
|
}
|
|
|
|
var nextDepth = depth + 1;
|
|
foreach (var jsonItem in arrayElement.EnumerateArray())
|
|
{
|
|
if (jsonItem.ValueKind != JsonValueKind.Object) continue;
|
|
|
|
object? itemId = null;
|
|
if (jsonItem.TryGetProperty("Id", out var idProp))
|
|
itemId = ReadPrimitive(idProp, idType, idProp.ValueKind);
|
|
|
|
if (itemId != null && !IsDefaultValue(itemId, idType) && existingById != null)
|
|
{
|
|
if (existingById.TryGetValue(itemId, out var existingItem))
|
|
{
|
|
var itemMetadata = GetTypeMetadata(elementType);
|
|
PopulateObjectInternalMerge(jsonItem, existingItem, itemMetadata, context, nextDepth);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
var newItem = ReadValue(jsonItem, elementType, context, nextDepth);
|
|
if (newItem != null) existingList.Add(newItem);
|
|
}
|
|
}
|
|
finally { acObservable?.EndUpdate(); }
|
|
}
|
|
|
|
private static object ReadDictionary(in JsonElement element, in Type keyType, in Type valueType, DeserializationContext context, int depth)
|
|
{
|
|
var dictType = DictionaryGenericType.MakeGenericType(keyType, valueType);
|
|
var dict = (IDictionary)Activator.CreateInstance(dictType)!;
|
|
var nextDepth = depth + 1;
|
|
|
|
foreach (var prop in element.EnumerateObject())
|
|
{
|
|
var name = prop.Name;
|
|
if (name.Length > 0 && name[0] == '$') continue;
|
|
|
|
object key;
|
|
if (ReferenceEquals(keyType, StringType)) key = name;
|
|
else if (ReferenceEquals(keyType, IntType)) key = int.Parse(name, CultureInfo.InvariantCulture);
|
|
else if (ReferenceEquals(keyType, LongType)) key = long.Parse(name, CultureInfo.InvariantCulture);
|
|
else if (ReferenceEquals(keyType, GuidType)) key = Guid.Parse(name);
|
|
else if (keyType.IsEnum) key = Enum.Parse(keyType, name);
|
|
else key = Convert.ChangeType(name, keyType, CultureInfo.InvariantCulture);
|
|
|
|
dict.Add(key, ReadValue(prop.Value, valueType, context, nextDepth));
|
|
}
|
|
|
|
return dict;
|
|
}
|
|
|
|
#endregion
|
|
}
|