AyCode.Core/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.JsonElem...

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
}