AyCode.Core/AyCode.Core/Extensions/AcJsonDeserializer.cs

1773 lines
70 KiB
C#

using System.Buffers;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.Globalization;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using AyCode.Core.Helpers;
using AyCode.Core.Interfaces;
using Newtonsoft.Json;
using static AyCode.Core.Extensions.JsonUtilities;
namespace AyCode.Core.Extensions;
/// <summary>
/// Exception thrown when JSON deserialization fails.
/// </summary>
public class AcJsonDeserializationException : Exception
{
public string? Json { get; }
public Type? TargetType { get; }
public AcJsonDeserializationException(string message, string? json = null, Type? targetType = null, Exception? innerException = null)
: base(message, innerException)
{
Json = json?.Length > 500 ? json[..500] + "..." : json;
TargetType = targetType;
}
}
/// <summary>
/// High-performance custom JSON deserializer optimized for IId&lt;T&gt; reference handling.
/// Uses Utf8JsonReader for streaming deserialization when possible (STJ approach).
/// </summary>
public static class AcJsonDeserializer
{
private static readonly ConcurrentDictionary<Type, DeserializeTypeMetadata> TypeMetadataCache = new();
// Pre-computed JSON property names for fast lookup (UTF-8 bytes)
private static readonly byte[] RefPropertyUtf8 = "$ref"u8.ToArray();
private static readonly byte[] IdPropertyUtf8 = "$id"u8.ToArray();
#region Public API
/// <summary>
/// Deserialize JSON string to a new object of type T with default options.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T? Deserialize<T>(string json) => Deserialize<T>(json, AcJsonSerializerOptions.Default);
/// <summary>
/// Deserialize JSON string to a new object of type T with specified options.
/// </summary>
public static T? Deserialize<T>(string json, in AcJsonSerializerOptions options)
{
if (string.IsNullOrEmpty(json) || json == "null") return default;
var targetType = typeof(T);
try
{
if (TryDeserializePrimitiveFast(json, targetType, out var primitiveResult))
return (T?)primitiveResult;
ValidateJson(json, targetType);
// Fast path for no reference handling - use Utf8JsonReader (streaming, no DOM)
if (!options.UseReferenceHandling)
{
return DeserializeWithUtf8Reader<T>(json, options.MaxDepth);
}
// Reference handling requires DOM for forward references
using var doc = JsonDocument.Parse(json);
var context = DeserializationContextPool.Get(options);
try
{
var result = ReadValue(doc.RootElement, targetType, context, 0);
context.ResolveReferences();
return (T?)result;
}
finally
{
DeserializationContextPool.Return(context);
}
}
catch (AcJsonDeserializationException) { throw; }
catch (System.Text.Json.JsonException ex)
{
throw new AcJsonDeserializationException($"Failed to parse JSON for type '{targetType.Name}': {ex.Message}", json, targetType, ex);
}
catch (Exception ex) when (ex is FormatException or InvalidCastException or OverflowException)
{
throw new AcJsonDeserializationException($"Failed to convert JSON value for type '{targetType.Name}': {ex.Message}", json, targetType, ex);
}
}
/// <summary>
/// Deserialize JSON string to specified type with default options.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static object? Deserialize(string json, Type targetType) => Deserialize(json, targetType, AcJsonSerializerOptions.Default);
/// <summary>
/// Deserialize JSON string to specified type with specified options.
/// </summary>
public static object? Deserialize(string json, in Type targetType, in AcJsonSerializerOptions options)
{
if (string.IsNullOrEmpty(json) || json == "null") return null;
try
{
var firstChar = json[0];
var isArrayJson = firstChar == '[';
var isObjectJson = firstChar == '{';
if (!isArrayJson && !isObjectJson)
{
if (TryDeserializePrimitiveFast(json, targetType, out var primitiveResult))
return primitiveResult;
}
ValidateJson(json, targetType);
// Fast path for no reference handling
if (!options.UseReferenceHandling)
{
return DeserializeWithUtf8ReaderNonGeneric(json, targetType, options.MaxDepth);
}
using var doc = JsonDocument.Parse(json);
var context = DeserializationContextPool.Get(options);
try
{
var result = ReadValue(doc.RootElement, targetType, context, 0);
context.ResolveReferences();
return result;
}
finally
{
DeserializationContextPool.Return(context);
}
}
catch (AcJsonDeserializationException) { throw; }
catch (System.Text.Json.JsonException ex)
{
throw new AcJsonDeserializationException($"Failed to parse JSON for type '{targetType.Name}': {ex.Message}", json, targetType, ex);
}
catch (Exception ex) when (ex is FormatException or InvalidCastException or OverflowException)
{
throw new AcJsonDeserializationException($"Failed to convert JSON value for type '{targetType.Name}': {ex.Message}", json, targetType, ex);
}
}
/// <summary>
/// Populate existing object with JSON data (merge mode) with default options.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Populate<T>(string json, T target) where T : class
=> Populate(json, target, AcJsonSerializerOptions.Default);
/// <summary>
/// Populate existing object with JSON data (merge mode) with specified options.
/// </summary>
public static void Populate<T>(string json, T target, in AcJsonSerializerOptions options) where T : class
{
ArgumentNullException.ThrowIfNull(target);
if (string.IsNullOrEmpty(json) || json == "null") return;
PopulateInternal(json, target, target.GetType(), options);
}
/// <summary>
/// Populate existing object with JSON data with default options.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Populate(string json, object target)
=> Populate(json, target, target.GetType(), AcJsonSerializerOptions.Default);
/// <summary>
/// Populate existing object with JSON data with specified options.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Populate(string json, object target, in AcJsonSerializerOptions options)
=> Populate(json, target, target.GetType(), options);
/// <summary>
/// Populate existing object with JSON data with default options.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Populate(string json, object target, Type targetType)
=> Populate(json, target, targetType, AcJsonSerializerOptions.Default);
/// <summary>
/// Populate existing object with JSON data with specified options.
/// </summary>
public static void Populate(string json, object target, in Type targetType, in AcJsonSerializerOptions options)
{
ArgumentNullException.ThrowIfNull(target);
if (string.IsNullOrEmpty(json) || json == "null") return;
PopulateInternal(json, target, targetType, options);
}
private static void PopulateInternal(string json, object target, in Type targetType, in AcJsonSerializerOptions options)
{
try
{
ValidateJson(json, targetType);
// Fast path for no reference handling - use Utf8JsonReader streaming
if (!options.UseReferenceHandling)
{
var firstChar = json[0];
if (firstChar == '[')
{
if (target is IList targetList)
PopulateListWithUtf8Reader(json, targetList, targetType, options.MaxDepth);
else
throw new AcJsonDeserializationException($"Cannot populate non-list target '{targetType.Name}' with JSON array", json, targetType);
return;
}
if (firstChar == '{')
{
var metadata = GetTypeMetadata(targetType);
PopulateObjectWithUtf8Reader(json, target, metadata, options.MaxDepth);
return;
}
throw new AcJsonDeserializationException($"Cannot populate object with JSON starting with '{firstChar}'", json, targetType);
}
// Reference handling requires DOM for forward references
using var doc = JsonDocument.Parse(json);
var rootElement = doc.RootElement;
var context = DeserializationContextPool.Get(options);
context.IsMergeMode = true;
try
{
if (rootElement.ValueKind == JsonValueKind.Array)
{
if (target is IList targetList)
PopulateList(rootElement, targetList, targetType, context, 0);
else
throw new AcJsonDeserializationException($"Cannot populate non-list target '{targetType.Name}' with JSON array", json, targetType);
context.ResolveReferences();
return;
}
if (rootElement.ValueKind == JsonValueKind.Object)
{
var metadata = GetTypeMetadata(targetType);
PopulateObjectInternalMerge(rootElement, target, metadata, context, 0);
}
else
throw new AcJsonDeserializationException($"Cannot populate object with JSON value of kind '{rootElement.ValueKind}'", json, targetType);
context.ResolveReferences();
}
finally
{
DeserializationContextPool.Return(context);
}
}
catch (AcJsonDeserializationException) { throw; }
catch (System.Text.Json.JsonException ex)
{
throw new AcJsonDeserializationException($"Failed to parse JSON for population of type '{targetType.Name}': {ex.Message}", json, targetType, ex);
}
catch (Exception ex) when (ex is FormatException or InvalidCastException or OverflowException)
{
throw new AcJsonDeserializationException($"Failed to convert JSON value during population of type '{targetType.Name}': {ex.Message}", json, targetType, ex);
}
}
#endregion
#region Utf8JsonReader Fast Path (STJ-style streaming)
/// <summary>
/// Deserialize using Utf8JsonReader - streaming without DOM allocation.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static T? DeserializeWithUtf8Reader<T>(string json, byte maxDepth)
{
var (buffer, length) = GetUtf8Bytes(json);
try
{
var reader = new Utf8JsonReader(buffer.AsSpan(0, length), new JsonReaderOptions { MaxDepth = maxDepth });
if (!reader.Read()) return default;
return (T?)ReadValueFromReader(ref reader, typeof(T), maxDepth, 0);
}
finally
{
ReturnUtf8Buffer(buffer);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? DeserializeWithUtf8ReaderNonGeneric(string json, Type targetType, byte maxDepth)
{
var (buffer, length) = GetUtf8Bytes(json);
try
{
var reader = new Utf8JsonReader(buffer.AsSpan(0, length), new JsonReaderOptions { MaxDepth = maxDepth });
if (!reader.Read()) return null;
return ReadValueFromReader(ref reader, targetType, maxDepth, 0);
}
finally
{
ReturnUtf8Buffer(buffer);
}
}
private static object? ReadValueFromReader(ref Utf8JsonReader reader, Type targetType, byte maxDepth, int depth)
{
switch (reader.TokenType)
{
case JsonTokenType.Null:
return null;
case JsonTokenType.True:
return true;
case JsonTokenType.False:
return false;
case JsonTokenType.Number:
return ReadNumberFromReader(ref reader, targetType);
case JsonTokenType.String:
return ReadStringFromReader(ref reader, targetType);
case JsonTokenType.StartObject:
return ReadObjectFromReader(ref reader, targetType, maxDepth, depth);
case JsonTokenType.StartArray:
return ReadArrayFromReader(ref reader, targetType, maxDepth, depth);
default:
return null;
}
}
/// <summary>
/// Read number value using cached PropertySetterInfo (avoids Nullable.GetUnderlyingType + Type.GetTypeCode calls).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? ReadNumberFromReaderCached(ref Utf8JsonReader reader, PropertySetterInfo propInfo)
{
return propInfo.PropertyTypeCode switch
{
TypeCode.Int32 => reader.GetInt32(),
TypeCode.Int64 => reader.GetInt64(),
TypeCode.Double => reader.GetDouble(),
TypeCode.Decimal => reader.GetDecimal(),
TypeCode.Single => reader.GetSingle(),
TypeCode.Byte => reader.GetByte(),
TypeCode.Int16 => reader.GetInt16(),
TypeCode.UInt16 => reader.GetUInt16(),
TypeCode.UInt32 => reader.GetUInt32(),
TypeCode.UInt64 => reader.GetUInt64(),
TypeCode.SByte => reader.GetSByte(),
_ => propInfo.UnderlyingType.IsEnum ? Enum.ToObject(propInfo.UnderlyingType, reader.GetInt32()) : reader.GetDouble()
};
}
/// <summary>
/// Read string value using cached PropertySetterInfo (avoids Nullable.GetUnderlyingType calls).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? ReadStringFromReaderCached(ref Utf8JsonReader reader, PropertySetterInfo propInfo)
{
var type = propInfo.UnderlyingType;
if (ReferenceEquals(type, StringType)) return reader.GetString();
if (ReferenceEquals(type, DateTimeType)) return reader.GetDateTime();
if (ReferenceEquals(type, GuidType)) return reader.GetGuid();
if (ReferenceEquals(type, DateTimeOffsetType)) return reader.GetDateTimeOffset();
if (ReferenceEquals(type, TimeSpanType))
{
var str = reader.GetString();
return str != null ? TimeSpan.Parse(str, CultureInfo.InvariantCulture) : default(TimeSpan);
}
if (type.IsEnum)
{
var str = reader.GetString();
return str != null ? Enum.Parse(type, str) : null;
}
return reader.GetString();
}
/// <summary>
/// Read value from reader using cached PropertySetterInfo for faster type resolution.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? ReadValueFromReaderCached(ref Utf8JsonReader reader, PropertySetterInfo propInfo, byte maxDepth, int depth)
{
switch (reader.TokenType)
{
case JsonTokenType.Null:
return null;
case JsonTokenType.True:
return true;
case JsonTokenType.False:
return false;
case JsonTokenType.Number:
return ReadNumberFromReaderCached(ref reader, propInfo);
case JsonTokenType.String:
return ReadStringFromReaderCached(ref reader, propInfo);
case JsonTokenType.StartObject:
return ReadObjectFromReader(ref reader, propInfo.PropertyType, maxDepth, depth);
case JsonTokenType.StartArray:
return ReadArrayFromReader(ref reader, propInfo.PropertyType, maxDepth, depth);
default:
return null;
}
}
private static object? ReadNumberFromReader(ref Utf8JsonReader reader, Type targetType)
{
var type = Nullable.GetUnderlyingType(targetType) ?? targetType;
var typeCode = Type.GetTypeCode(type);
return typeCode switch
{
TypeCode.Int32 => reader.GetInt32(),
TypeCode.Int64 => reader.GetInt64(),
TypeCode.Double => reader.GetDouble(),
TypeCode.Decimal => reader.GetDecimal(),
TypeCode.Single => reader.GetSingle(),
TypeCode.Byte => reader.GetByte(),
TypeCode.Int16 => reader.GetInt16(),
TypeCode.UInt16 => reader.GetUInt16(),
TypeCode.UInt32 => reader.GetUInt32(),
TypeCode.UInt64 => reader.GetUInt64(),
TypeCode.SByte => reader.GetSByte(),
_ => type.IsEnum ? Enum.ToObject(type, reader.GetInt32()) : reader.GetDouble()
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? ReadStringFromReader(ref Utf8JsonReader reader, Type targetType)
{
var type = Nullable.GetUnderlyingType(targetType) ?? targetType;
if (ReferenceEquals(type, StringType)) return reader.GetString();
if (ReferenceEquals(type, DateTimeType)) return reader.GetDateTime();
if (ReferenceEquals(type, GuidType)) return reader.GetGuid();
if (ReferenceEquals(type, DateTimeOffsetType)) return reader.GetDateTimeOffset();
if (ReferenceEquals(type, TimeSpanType))
{
var str = reader.GetString();
return str != null ? TimeSpan.Parse(str, CultureInfo.InvariantCulture) : default(TimeSpan);
}
if (type.IsEnum)
{
var str = reader.GetString();
return str != null ? Enum.Parse(type, str) : null;
}
return reader.GetString();
}
private static object? ReadObjectFromReader(ref Utf8JsonReader reader, Type targetType, byte maxDepth, int depth)
{
if (depth > maxDepth)
{
reader.Skip();
return null;
}
if (IsDictionaryType(targetType, out var keyType, out var valueType))
return ReadDictionaryFromReader(ref reader, keyType!, valueType!, maxDepth, 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;
var nextDepth = depth + 1;
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
break;
if (reader.TokenType != JsonTokenType.PropertyName)
continue;
// Use UTF8 lookup to avoid string allocation
if (!metadata.TryGetPropertyUtf8(ref reader, out var propInfo) || propInfo == null)
{
if (!reader.Read()) break;
reader.Skip();
continue;
}
if (!reader.Read())
break;
// Try direct set for primitives (no boxing)
if (propInfo.TrySetValueDirect(instance, ref reader))
continue;
// Fallback to boxed path for complex types
var value = ReadValueFromReaderCached(ref reader, propInfo, maxDepth, nextDepth);
propInfo.SetValue(instance, value);
}
return instance;
}
/// <summary>
/// Read object from reader when we're already at StartObject.
/// </summary>
private static object? ReadObjectFromReaderAtStart(ref Utf8JsonReader reader, Type targetType, DeserializeTypeMetadata metadata, byte maxDepth, int depth)
{
if (depth > maxDepth)
{
reader.Skip();
return null;
}
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;
var nextDepth = depth + 1;
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
break;
if (reader.TokenType != JsonTokenType.PropertyName)
continue;
// Use UTF8 lookup to avoid string allocation
if (!metadata.TryGetPropertyUtf8(ref reader, out var propInfo) || propInfo == null)
{
if (!reader.Read()) break;
reader.Skip();
continue;
}
if (!reader.Read())
break;
// Try direct set for primitives (no boxing)
if (propInfo.TrySetValueDirect(instance, ref reader))
continue;
// Fallback to boxed path for complex types
var value = ReadValueFromReaderCached(ref reader, propInfo, maxDepth, nextDepth);
propInfo.SetValue(instance, value);
}
return instance;
}
private static object? ReadArrayFromReader(ref Utf8JsonReader reader, Type targetType, byte maxDepth, int depth)
{
if (depth > maxDepth)
{
reader.Skip();
return null;
}
var elementType = GetCollectionElementType(targetType);
if (elementType == null) return null;
var nextDepth = depth + 1;
var list = GetOrCreateListFactory(elementType)();
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndArray)
break;
var value = ReadValueFromReader(ref reader, elementType, maxDepth, nextDepth);
if (value != null)
list.Add(value);
}
if (targetType.IsArray)
{
var array = Array.CreateInstance(elementType, list.Count);
list.CopyTo(array, 0);
return array;
}
return list;
}
private static object ReadDictionaryFromReader(ref Utf8JsonReader reader, Type keyType, Type valueType, byte maxDepth, int depth)
{
var dictType = DictionaryGenericType.MakeGenericType(keyType, valueType);
var dict = (IDictionary)Activator.CreateInstance(dictType)!;
var nextDepth = depth + 1;
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
break;
if (reader.TokenType != JsonTokenType.PropertyName)
continue;
var keyStr = reader.GetString();
if (keyStr == null || !reader.Read())
continue;
object key;
if (ReferenceEquals(keyType, StringType)) key = keyStr;
else if (ReferenceEquals(keyType, IntType)) key = int.Parse(keyStr, CultureInfo.InvariantCulture);
else if (ReferenceEquals(keyType, LongType)) key = long.Parse(keyStr, CultureInfo.InvariantCulture);
else if (ReferenceEquals(keyType, GuidType)) key = Guid.Parse(keyStr);
else if (keyType.IsEnum) key = Enum.Parse(keyType, keyStr);
else key = Convert.ChangeType(keyStr, keyType, CultureInfo.InvariantCulture);
var value = ReadValueFromReader(ref reader, valueType, maxDepth, nextDepth);
dict.Add(key, value);
}
return dict;
}
/// <summary>
/// Populate object using Utf8JsonReader streaming (no DOM allocation).
/// </summary>
private static void PopulateObjectWithUtf8Reader(string json, object target, DeserializeTypeMetadata metadata, byte maxDepth)
{
var (buffer, length) = GetUtf8Bytes(json);
try
{
var reader = new Utf8JsonReader(buffer.AsSpan(0, length), new JsonReaderOptions { MaxDepth = maxDepth });
if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
return;
PopulateObjectMergeFromReader(ref reader, target, metadata, maxDepth, 0);
}
finally
{
ReturnUtf8Buffer(buffer);
}
}
/// <summary>
/// Populate list using Utf8JsonReader streaming (no DOM allocation).
/// </summary>
private static void PopulateListWithUtf8Reader(string json, IList targetList, in Type listType, byte maxDepth)
{
var (buffer, length) = GetUtf8Bytes(json);
try
{
var reader = new Utf8JsonReader(buffer.AsSpan(0, length), new JsonReaderOptions { MaxDepth = maxDepth });
if (!reader.Read() || reader.TokenType != JsonTokenType.StartArray)
return;
var elementType = GetCollectionElementType(listType);
if (elementType == null) return;
var acObservable = targetList as IAcObservableCollection;
acObservable?.BeginUpdate();
try
{
targetList.Clear();
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndArray)
break;
var value = ReadValueFromReader(ref reader, elementType, maxDepth, 1);
if (value != null)
targetList.Add(value);
}
}
finally { acObservable?.EndUpdate(); }
}
finally
{
ReturnUtf8Buffer(buffer);
}
}
/// <summary>
/// Populate object with merge semantics from Utf8JsonReader.
/// </summary>
private static void PopulateObjectMergeFromReader(ref Utf8JsonReader reader, object target, DeserializeTypeMetadata metadata, byte maxDepth, int depth)
{
var nextDepth = depth + 1;
var maxDepthReached = nextDepth > maxDepth;
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
break;
if (reader.TokenType != JsonTokenType.PropertyName)
continue;
// Use UTF8 lookup to avoid string allocation
if (!metadata.TryGetPropertyUtf8(ref reader, out var propInfo) || propInfo == null)
{
if (!reader.Read()) break;
reader.Skip();
continue;
}
if (!reader.Read())
break;
var tokenType = reader.TokenType;
if (maxDepthReached)
{
if (tokenType != JsonTokenType.StartObject && tokenType != JsonTokenType.StartArray)
{
// Try direct set for primitives (no boxing)
if (!propInfo.TrySetValueDirect(target, ref reader))
{
var primitiveValue = ReadPrimitiveFromReaderCached(ref reader, propInfo);
propInfo.SetValue(target, primitiveValue);
}
}
else
{
reader.Skip();
}
continue;
}
// Handle IId collection merge
if (propInfo.IsIIdCollection && tokenType == JsonTokenType.StartArray)
{
var existingCollection = propInfo.GetValue(target);
if (existingCollection != null)
{
MergeIIdCollectionFromReader(ref reader, existingCollection, propInfo, maxDepth, depth);
continue;
}
}
// Handle nested objects - merge into existing
if (tokenType == JsonTokenType.StartObject && !propInfo.PropertyType.IsPrimitive && !ReferenceEquals(propInfo.PropertyType, StringType))
{
var existingObj = propInfo.GetValue(target);
if (existingObj != null)
{
var nestedMetadata = GetTypeMetadata(propInfo.PropertyType);
PopulateObjectMergeFromReader(ref reader, existingObj, nestedMetadata, maxDepth, nextDepth);
continue;
}
}
// Try direct set for primitives (no boxing)
if (propInfo.TrySetValueDirect(target, ref reader))
continue;
// Fallback to boxed path for complex types
var value = ReadValueFromReaderCached(ref reader, propInfo, maxDepth, nextDepth);
propInfo.SetValue(target, value);
}
}
/// <summary>
/// Read primitive value from Utf8JsonReader using cached PropertySetterInfo.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? ReadPrimitiveFromReaderCached(ref Utf8JsonReader reader, PropertySetterInfo propInfo)
{
var tokenType = reader.TokenType;
if (tokenType == JsonTokenType.Null) return null;
if (tokenType == JsonTokenType.True) return true;
if (tokenType == JsonTokenType.False) return false;
if (tokenType == JsonTokenType.Number) return ReadNumberFromReaderCached(ref reader, propInfo);
if (tokenType == JsonTokenType.String) return ReadStringFromReaderCached(ref reader, propInfo);
return null;
}
/// <summary>
/// Merge IId collection from Utf8JsonReader streaming.
/// </summary>
private static void MergeIIdCollectionFromReader(ref Utf8JsonReader reader, object existingCollection, PropertySetterInfo propInfo, byte maxDepth, 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;
var elementMetadata = GetTypeMetadata(elementType);
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndArray)
break;
if (reader.TokenType != JsonTokenType.StartObject)
{
var primitiveValue = ReadValueFromReader(ref reader, elementType, maxDepth, nextDepth);
if (primitiveValue != null) existingList.Add(primitiveValue);
continue;
}
// For objects, we need to read the full object to check the Id
var newItem = ReadObjectFromReaderAtStart(ref reader, elementType, elementMetadata, maxDepth, nextDepth);
if (newItem == null) continue;
// Check if this item already exists by Id
var itemId = propInfo.ElementIdGetter?.Invoke(newItem);
if (itemId != null && !IsDefaultValue(itemId, idType) && existingById != null)
{
if (existingById.TryGetValue(itemId, out var existingItem))
{
// Copy properties from newItem to existingItem
CopyProperties(newItem, existingItem, elementMetadata);
continue;
}
}
existingList.Add(newItem);
}
}
finally { acObservable?.EndUpdate(); }
}
/// <summary>
/// Copy properties from source to target using metadata.
/// </summary>
private static void CopyProperties(object source, object target, DeserializeTypeMetadata metadata)
{
foreach (var prop in metadata.PropertySettersFrozen.Values)
{
var value = prop.GetValue(source);
if (value != null)
prop.SetValue(target, value);
}
}
/// <summary>
/// Read primitive value from Utf8JsonReader.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? ReadPrimitiveFromReader(ref Utf8JsonReader reader, Type targetType)
{
var tokenType = reader.TokenType;
if (tokenType == JsonTokenType.Null) return null;
if (tokenType == JsonTokenType.True) return true;
if (tokenType == JsonTokenType.False) return false;
if (tokenType == JsonTokenType.Number) return ReadNumberFromReader(ref reader, targetType);
if (tokenType == JsonTokenType.String) return ReadStringFromReader(ref reader, targetType);
return null;
}
#endregion
#region Validation
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ValidateJson(string json, in Type targetType)
{
if (json.Length < 2) return;
var firstChar = json[0];
// Quick check for double-serialized JSON
if (firstChar == '"' && json[^1] == '"')
{
var inner = json.AsSpan(1, json.Length - 2);
if (inner.Contains("\\\"", StringComparison.Ordinal) &&
(inner.Contains("{", StringComparison.Ordinal) || inner.Contains("[", StringComparison.Ordinal)))
throw new AcJsonDeserializationException(
$"Detected double-serialized JSON string. Target type: {targetType.Name}.",
json, targetType);
}
var isArrayJson = firstChar == '[';
var isObjectJson = firstChar == '{';
if (isArrayJson)
{
var isCollectionType = targetType.IsArray || IsGenericCollectionType(targetType);
var isDictType = IsDictionaryType(targetType, out _, out _);
if (!isCollectionType && !isDictType && targetType != typeof(object))
throw new AcJsonDeserializationException($"JSON is an array but target type '{targetType.Name}' is not a collection type.", json, targetType);
}
else if (isObjectJson)
{
var isCollectionType = targetType.IsArray || IsGenericCollectionType(targetType);
var isDictType = IsDictionaryType(targetType, out _, out _);
if (isCollectionType && !isDictType)
throw new AcJsonDeserializationException($"JSON is an object but target type '{targetType.Name}' is a collection type (not dictionary).", json, targetType);
}
}
#endregion
#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
if (element.TryGetProperty(RefPropertyUtf8, out var refElement))
{
var refId = refElement.GetString()!;
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
if (element.TryGetProperty(IdPropertyUtf8, out var idElement))
context.RegisterObject(idElement.GetString()!, instance);
PopulateObjectInternal(element, instance, metadata, context, depth);
return instance;
}
private static void PopulateObjectInternal(in JsonElement element, object target, DeserializeTypeMetadata 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, DeserializeTypeMetadata 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;
foreach (var item in arrayElement.EnumerateArray())
{
var value = ReadValue(item, elementType, context, nextDepth);
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
#region Primitive Reading
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? ReadPrimitive(in JsonElement element, in Type targetType, JsonValueKind valueKind)
{
var type = Nullable.GetUnderlyingType(targetType) ?? targetType;
if (valueKind == JsonValueKind.Number)
{
var typeCode = Type.GetTypeCode(type);
return typeCode switch
{
TypeCode.Int32 => element.GetInt32(),
TypeCode.Int64 => element.GetInt64(),
TypeCode.Double => element.GetDouble(),
TypeCode.Decimal => element.GetDecimal(),
TypeCode.Single => element.GetSingle(),
TypeCode.Byte => element.GetByte(),
TypeCode.Int16 => element.GetInt16(),
TypeCode.UInt16 => element.GetUInt16(),
TypeCode.UInt32 => element.GetUInt32(),
TypeCode.UInt64 => element.GetUInt64(),
TypeCode.SByte => element.GetSByte(),
_ => type.IsEnum ? Enum.ToObject(type, element.GetInt32()) : null
};
}
if (valueKind == JsonValueKind.String)
{
if (ReferenceEquals(type, StringType)) return element.GetString();
if (ReferenceEquals(type, DateTimeType)) return element.GetDateTime();
if (ReferenceEquals(type, GuidType)) return element.GetGuid();
if (ReferenceEquals(type, DateTimeOffsetType)) return element.GetDateTimeOffset();
if (ReferenceEquals(type, TimeSpanType)) return TimeSpan.Parse(element.GetString()!, CultureInfo.InvariantCulture);
if (type.IsEnum) return Enum.Parse(type, element.GetString()!);
return null;
}
if (valueKind == JsonValueKind.True) return true;
if (valueKind == JsonValueKind.False) return false;
return null;
}
#endregion
#region Primitive Deserialization
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool TryDeserializePrimitiveFast(string json, in Type targetType, out object? result)
{
var type = Nullable.GetUnderlyingType(targetType) ?? targetType;
// Handle enums first
if (type.IsEnum)
{
if (json.Length > 0 && json[0] == '"')
{
using var doc = JsonDocument.Parse(json);
result = Enum.Parse(type, doc.RootElement.GetString()!);
}
else result = Enum.ToObject(type, int.Parse(json, CultureInfo.InvariantCulture));
return true;
}
var typeCode = Type.GetTypeCode(type);
switch (typeCode)
{
case TypeCode.Int32: result = int.Parse(json, CultureInfo.InvariantCulture); return true;
case TypeCode.Int64: result = long.Parse(json, CultureInfo.InvariantCulture); return true;
case TypeCode.Boolean: result = json == "true"; return true;
case TypeCode.Double: result = double.Parse(json, CultureInfo.InvariantCulture); return true;
case TypeCode.Decimal: result = decimal.Parse(json, CultureInfo.InvariantCulture); return true;
case TypeCode.Single: result = float.Parse(json, CultureInfo.InvariantCulture); return true;
case TypeCode.String:
// If already unwrapped (no quotes), return as-is; otherwise parse JSON
if (json.Length == 0 || json[0] != '"')
{
result = json;
return true;
}
using (var doc = JsonDocument.Parse(json))
{
result = doc.RootElement.GetString();
return true;
}
case TypeCode.DateTime:
// If already unwrapped (no quotes), parse directly; otherwise use JSON parser
if (json.Length == 0 || json[0] != '"')
{
result = DateTime.Parse(json, CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.RoundtripKind);
return true;
}
using (var doc = JsonDocument.Parse(json))
{
result = doc.RootElement.GetDateTime();
return true;
}
case TypeCode.Byte: result = byte.Parse(json, CultureInfo.InvariantCulture); return true;
case TypeCode.Int16: result = short.Parse(json, CultureInfo.InvariantCulture); return true;
case TypeCode.UInt16: result = ushort.Parse(json, CultureInfo.InvariantCulture); return true;
case TypeCode.UInt32: result = uint.Parse(json, CultureInfo.InvariantCulture); return true;
case TypeCode.UInt64: result = ulong.Parse(json, CultureInfo.InvariantCulture); return true;
case TypeCode.SByte: result = sbyte.Parse(json, CultureInfo.InvariantCulture); return true;
case TypeCode.Char:
if (json.Length == 0 || json[0] != '"')
{
result = json.Length > 0 ? json[0] : '\0';
return true;
}
using (var doc = JsonDocument.Parse(json))
{
var s = doc.RootElement.GetString();
result = s?.Length > 0 ? s[0] : '\0';
return true;
}
}
if (ReferenceEquals(type, GuidType))
{
// If already unwrapped (no quotes), parse directly
if (json.Length == 0 || json[0] != '"')
{
result = Guid.Parse(json);
return true;
}
using var doc = JsonDocument.Parse(json);
result = doc.RootElement.GetGuid();
return true;
}
if (ReferenceEquals(type, DateTimeOffsetType))
{
// If already unwrapped (no quotes), parse directly
if (json.Length == 0 || json[0] != '"')
{
result = DateTimeOffset.Parse(json, CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.RoundtripKind);
return true;
}
using var doc = JsonDocument.Parse(json);
result = doc.RootElement.GetDateTimeOffset();
return true;
}
if (ReferenceEquals(type, TimeSpanType))
{
// If already unwrapped (no quotes), parse directly
if (json.Length == 0 || json[0] != '"')
{
result = TimeSpan.Parse(json, CultureInfo.InvariantCulture);
return true;
}
using var doc = JsonDocument.Parse(json);
result = TimeSpan.Parse(doc.RootElement.GetString()!, CultureInfo.InvariantCulture);
return true;
}
result = null;
return false;
}
#endregion
#region Context Pool
private static class DeserializationContextPool
{
private static readonly ConcurrentQueue<DeserializationContext> Pool = new();
private const int MaxPoolSize = 16;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static DeserializationContext Get(in AcJsonSerializerOptions options)
{
if (Pool.TryDequeue(out var context))
{
context.Reset(options);
return context;
}
return new DeserializationContext(options);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Return(DeserializationContext context)
{
if (Pool.Count < MaxPoolSize)
{
context.Clear();
Pool.Enqueue(context);
}
}
}
#endregion
#region Type Metadata
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static DeserializeTypeMetadata GetTypeMetadata(in Type type)
=> TypeMetadataCache.GetOrAdd(type, static t => new DeserializeTypeMetadata(t));
private sealed class DeserializeTypeMetadata
{
public FrozenDictionary<string, PropertySetterInfo> PropertySettersFrozen { get; }
public PropertySetterInfo[] PropertiesArray { get; } // Array for fast UTF8 linear scan (small objects)
public Func<object>? CompiledConstructor { get; }
public DeserializeTypeMetadata(Type type)
{
var ctor = type.GetConstructor(Type.EmptyTypes);
if (ctor != null)
{
var newExpr = Expression.New(type);
var boxed = Expression.Convert(newExpr, typeof(object));
CompiledConstructor = Expression.Lambda<Func<object>>(boxed).Compile();
}
var allProps = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
var propsList = new List<PropertyInfo>(allProps.Length);
foreach (var p in allProps)
{
if (!p.CanWrite || !p.CanRead || p.GetIndexParameters().Length != 0) continue;
if (HasJsonIgnoreAttribute(p)) continue;
propsList.Add(p);
}
var propertySetters = new Dictionary<string, PropertySetterInfo>(propsList.Count, StringComparer.OrdinalIgnoreCase);
var propsArray = new PropertySetterInfo[propsList.Count];
var index = 0;
foreach (var prop in propsList)
{
var propInfo = new PropertySetterInfo(prop, type);
propertySetters[prop.Name] = propInfo;
propsArray[index++] = propInfo;
}
PropertiesArray = propsArray;
// Create frozen dictionary for faster lookup in hot paths
PropertySettersFrozen = propertySetters.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
}
/// <summary>
/// Try to find property by UTF-8 name using ValueTextEquals (avoids string allocation).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetPropertyUtf8(ref Utf8JsonReader reader, out PropertySetterInfo? propInfo)
{
var props = PropertiesArray;
for (var i = 0; i < props.Length; i++)
{
if (reader.ValueTextEquals(props[i].NameUtf8))
{
propInfo = props[i];
return true;
}
}
propInfo = null;
return false;
}
}
private sealed class PropertySetterInfo
{
public readonly Type PropertyType;
public readonly Type UnderlyingType; // Cached: Nullable.GetUnderlyingType(PropertyType) ?? PropertyType
public readonly TypeCode PropertyTypeCode; // Cached TypeCode for fast primitive reading
public readonly bool IsNullable;
public readonly bool IsIIdCollection;
public readonly Type? ElementType;
public readonly Type? ElementIdType;
public readonly Func<object, object?>? ElementIdGetter;
public readonly byte[] NameUtf8; // Pre-computed UTF-8 bytes of property name for fast matching
// Typed setters to avoid boxing for primitives
private readonly Action<object, object?> _setter;
private readonly Func<object, object?> _getter;
// Typed setters for common primitive types (avoid boxing)
internal readonly Action<object, int>? _setInt32;
internal readonly Action<object, long>? _setInt64;
internal readonly Action<object, double>? _setDouble;
internal readonly Action<object, bool>? _setBool;
internal readonly Action<object, decimal>? _setDecimal;
internal readonly Action<object, float>? _setSingle;
internal readonly Action<object, DateTime>? _setDateTime;
internal readonly Action<object, Guid>? _setGuid;
public PropertySetterInfo(PropertyInfo prop, Type declaringType)
{
PropertyType = prop.PropertyType;
var underlying = Nullable.GetUnderlyingType(PropertyType);
IsNullable = underlying != null;
UnderlyingType = underlying ?? PropertyType;
PropertyTypeCode = Type.GetTypeCode(UnderlyingType);
NameUtf8 = Encoding.UTF8.GetBytes(prop.Name);
_setter = CreateCompiledSetter(declaringType, prop);
_getter = CreateCompiledGetter(declaringType, prop);
// Create typed setters for common primitives to avoid boxing
if (!IsNullable)
{
if (ReferenceEquals(PropertyType, IntType))
_setInt32 = CreateTypedSetter<int>(declaringType, prop);
else if (ReferenceEquals(PropertyType, LongType))
_setInt64 = CreateTypedSetter<long>(declaringType, prop);
else if (ReferenceEquals(PropertyType, DoubleType))
_setDouble = CreateTypedSetter<double>(declaringType, prop);
else if (ReferenceEquals(PropertyType, BoolType))
_setBool = CreateTypedSetter<bool>(declaringType, prop);
else if (ReferenceEquals(PropertyType, DecimalType))
_setDecimal = CreateTypedSetter<decimal>(declaringType, prop);
else if (ReferenceEquals(PropertyType, FloatType))
_setSingle = CreateTypedSetter<float>(declaringType, prop);
else if (ReferenceEquals(PropertyType, DateTimeType))
_setDateTime = CreateTypedSetter<DateTime>(declaringType, prop);
else if (ReferenceEquals(PropertyType, GuidType))
_setGuid = CreateTypedSetter<Guid>(declaringType, prop);
}
ElementType = GetCollectionElementType(PropertyType);
var isCollection = ElementType != null && ElementType != typeof(object) &&
typeof(IEnumerable).IsAssignableFrom(PropertyType) &&
!ReferenceEquals(PropertyType, StringType);
if (isCollection && ElementType != null)
{
var idInfo = GetIdInfo(ElementType);
if (idInfo.IsId)
{
IsIIdCollection = true;
ElementIdType = idInfo.IdType;
var idProp = ElementType.GetProperty("Id");
if (idProp != null)
ElementIdGetter = CreateCompiledGetter(ElementType, idProp);
}
}
}
private static Action<object, object?> CreateCompiledSetter(Type declaringType, PropertyInfo prop)
{
var objParam = Expression.Parameter(typeof(object), "obj");
var valueParam = Expression.Parameter(typeof(object), "value");
var castObj = Expression.Convert(objParam, declaringType);
var castValue = Expression.Convert(valueParam, prop.PropertyType);
var propAccess = Expression.Property(castObj, prop);
var assign = Expression.Assign(propAccess, castValue);
return Expression.Lambda<Action<object, object?>>(assign, objParam, valueParam).Compile();
}
private static Func<object, object?> CreateCompiledGetter(Type declaringType, PropertyInfo prop)
{
var objParam = Expression.Parameter(typeof(object), "obj");
var castExpr = Expression.Convert(objParam, declaringType);
var propAccess = Expression.Property(castExpr, prop);
var boxed = Expression.Convert(propAccess, typeof(object));
return Expression.Lambda<Func<object, object?>>(boxed, objParam).Compile();
}
private static Action<object, T> CreateTypedSetter<T>(Type declaringType, PropertyInfo prop)
{
var objParam = Expression.Parameter(typeof(object), "obj");
var valueParam = Expression.Parameter(typeof(T), "value");
var castObj = Expression.Convert(objParam, declaringType);
var propAccess = Expression.Property(castObj, prop);
var assign = Expression.Assign(propAccess, valueParam);
return Expression.Lambda<Action<object, T>>(assign, objParam, valueParam).Compile();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetValue(object target, object? value) => _setter(target, value);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public object? GetValue(object target) => _getter(target);
/// <summary>
/// Read and set value directly from Utf8JsonReader, avoiding boxing for primitives.
/// Returns true if value was set, false if it needs fallback to SetValue.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TrySetValueDirect(object target, ref Utf8JsonReader reader)
{
var tokenType = reader.TokenType;
// Handle null
if (tokenType == JsonTokenType.Null)
{
if (IsNullable || !PropertyType.IsValueType)
{
_setter(target, null);
return true;
}
return true; // Skip null for non-nullable value types
}
// Fast path for booleans - no boxing needed with typed setter
if (tokenType == JsonTokenType.True)
{
if (_setBool != null) { _setBool(target, true); return true; }
_setter(target, BoxedTrue);
return true;
}
if (tokenType == JsonTokenType.False)
{
if (_setBool != null) { _setBool(target, false); return true; }
_setter(target, BoxedFalse);
return true;
}
// Fast path for numbers - use typed setters when available
if (tokenType == JsonTokenType.Number)
{
if (_setInt32 != null) { _setInt32(target, reader.GetInt32()); return true; }
if (_setInt64 != null) { _setInt64(target, reader.GetInt64()); return true; }
if (_setDouble != null) { _setDouble(target, reader.GetDouble()); return true; }
if (_setDecimal != null) { _setDecimal(target, reader.GetDecimal()); return true; }
if (_setSingle != null) { _setSingle(target, reader.GetSingle()); return true; }
return false; // Fallback to boxed path
}
// Fast path for strings - common types
if (tokenType == JsonTokenType.String)
{
if (ReferenceEquals(UnderlyingType, StringType))
{
_setter(target, reader.GetString());
return true;
}
if (_setDateTime != null) { _setDateTime(target, reader.GetDateTime()); return true; }
if (_setGuid != null) { _setGuid(target, reader.GetGuid()); return true; }
return false; // Fallback to boxed path
}
return false; // Complex types need standard handling
}
// Pre-boxed boolean values to avoid repeated boxing
private static readonly object BoxedTrue = true;
private static readonly object BoxedFalse = false;
}
#endregion
#region Reference Resolution
private sealed class DeferredReference(string refId, Type targetType)
{
public string RefId { get; } = refId;
public Type TargetType { get; } = targetType;
}
private readonly struct PropertyToResolve(object target, PropertySetterInfo property, string refId)
{
public readonly object Target = target;
public readonly PropertySetterInfo Property = property;
public readonly string RefId = refId;
}
private sealed class DeserializationContext
{
private Dictionary<string, object>? _idToObject;
private List<PropertyToResolve>? _propertiesToResolve;
public bool IsMergeMode { get; set; }
public bool UseReferenceHandling { get; private set; }
public byte MaxDepth { get; private set; }
public DeserializationContext(in AcJsonSerializerOptions options)
{
Reset(options);
}
public void Reset(in AcJsonSerializerOptions options)
{
UseReferenceHandling = options.UseReferenceHandling;
MaxDepth = options.MaxDepth;
IsMergeMode = false;
}
public void Clear()
{
_idToObject?.Clear();
_propertiesToResolve?.Clear();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RegisterObject(string id, object obj)
{
if (!UseReferenceHandling) return;
_idToObject ??= new Dictionary<string, object>(8, StringComparer.Ordinal);
_idToObject[id] = obj;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetReferencedObject(string id, out object? obj)
{
if (_idToObject != null) return _idToObject.TryGetValue(id, out obj);
obj = null;
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddPropertyToResolve(object target, PropertySetterInfo property, string refId)
{
_propertiesToResolve ??= new List<PropertyToResolve>(4);
_propertiesToResolve.Add(new PropertyToResolve(target, property, refId));
}
public void ResolveReferences()
{
if (_propertiesToResolve == null || _idToObject == null) return;
foreach (ref readonly var ptr in System.Runtime.InteropServices.CollectionsMarshal.AsSpan(_propertiesToResolve))
{
if (_idToObject.TryGetValue(ptr.RefId, out var refObj))
ptr.Property.SetValue(ptr.Target, refObj);
}
}
}
#endregion
}