1773 lines
70 KiB
C#
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<T> 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
|
|
}
|