High-perf streaming JSON (de)serialization, refactor
Major upgrade to AcJsonSerializer/AcJsonDeserializer: - Add Utf8JsonReader/Writer fast-paths for streaming, allocation-free (de)serialization when reference handling is not needed, matching STJ performance. - Populate/merge now uses streaming for in-place updates. - Type metadata caches TypeCode, UnderlyingType, and uses frozen dictionaries for hot property lookup. - Context pooling reduces allocations and GC pressure. - Primitive (de)serialization uses TypeCode-based fast paths; improved enum, Guid, DateTime, etc. handling. - Shared UTF8 buffer pool for efficient encoding/decoding. - Pre-encoded property names and STJ writer for output. - Improved validation, error handling, and double-serialization detection. - Expanded benchmarks: small/medium/large, with/without refs, AyCode vs STJ vs Newtonsoft, grouped by scenario. - General code modernization: aggressive inlining, ref params, ReferenceEquals, improved naming, and comments. - Unified IId<T> and collection element detection; consistent $id/$ref handling. Brings AyCode JSON (de)serializer to near-parity with STJ for non-ref scenarios, while retaining advanced features and improving maintainability and performance.
This commit is contained in:
parent
ad426feba4
commit
a945db9b09
|
|
@ -1,9 +1,12 @@
|
|||
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;
|
||||
|
|
@ -30,23 +33,28 @@ public class AcJsonDeserializationException : Exception
|
|||
|
||||
/// <summary>
|
||||
/// High-performance custom JSON deserializer optimized for IId<T> reference handling.
|
||||
/// Supports MaxDepth and UseReferenceHandling options.
|
||||
/// 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, AcJsonSerializerOptions options)
|
||||
public static T? Deserialize<T>(string json, in AcJsonSerializerOptions options)
|
||||
{
|
||||
if (string.IsNullOrEmpty(json) || json == "null") return default;
|
||||
|
||||
|
|
@ -54,18 +62,30 @@ public static class AcJsonDeserializer
|
|||
|
||||
try
|
||||
{
|
||||
ValidateJson(json, targetType);
|
||||
|
||||
if (TryDeserializePrimitive(json, targetType, out var primitiveResult))
|
||||
if (TryDeserializePrimitiveFast(json, targetType, out var primitiveResult))
|
||||
return (T?)primitiveResult;
|
||||
|
||||
var context = new DeserializationContext(options);
|
||||
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 result = ReadValue(doc.RootElement, targetType, context, 0);
|
||||
context.ResolveReferences();
|
||||
|
||||
return (T?)result;
|
||||
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)
|
||||
|
|
@ -81,37 +101,48 @@ public static class AcJsonDeserializer
|
|||
/// <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, Type targetType, AcJsonSerializerOptions options)
|
||||
public static object? Deserialize(string json, in Type targetType, in AcJsonSerializerOptions options)
|
||||
{
|
||||
if (string.IsNullOrEmpty(json) || json == "null") return null;
|
||||
|
||||
try
|
||||
{
|
||||
ValidateJson(json, targetType);
|
||||
var firstChar = json[0];
|
||||
var isArrayJson = firstChar == '[';
|
||||
var isObjectJson = firstChar == '{';
|
||||
|
||||
var isArrayJson = json.Length > 0 && json[0] == '[';
|
||||
var isObjectJson = json.Length > 0 && json[0] == '{';
|
||||
var isCollectionType = targetType.IsArray || IsGenericCollectionType(targetType);
|
||||
var isDictType = IsDictionaryType(targetType, out _, out _);
|
||||
|
||||
if (!isArrayJson && !isCollectionType && !(isObjectJson && isDictType))
|
||||
if (!isArrayJson && !isObjectJson)
|
||||
{
|
||||
if (TryDeserializePrimitive(json, targetType, out var primitiveResult))
|
||||
if (TryDeserializePrimitiveFast(json, targetType, out var primitiveResult))
|
||||
return primitiveResult;
|
||||
}
|
||||
|
||||
var context = new DeserializationContext(options);
|
||||
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 result = ReadValue(doc.RootElement, targetType, context, 0);
|
||||
context.ResolveReferences();
|
||||
|
||||
return result;
|
||||
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)
|
||||
|
|
@ -127,74 +158,115 @@ public static class AcJsonDeserializer
|
|||
/// <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, AcJsonSerializerOptions options) where T : class
|
||||
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;
|
||||
Populate(json, (object)target, target.GetType(), options);
|
||||
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>
|
||||
public static void Populate(string json, object target, AcJsonSerializerOptions options)
|
||||
[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, Type targetType, AcJsonSerializerOptions options)
|
||||
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);
|
||||
|
||||
var context = new DeserializationContext(options) { IsMergeMode = true };
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
// 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;
|
||||
|
||||
if (rootElement.ValueKind == JsonValueKind.Array)
|
||||
var context = DeserializationContextPool.Get(options);
|
||||
context.IsMergeMode = true;
|
||||
|
||||
try
|
||||
{
|
||||
if (target is IList targetList)
|
||||
PopulateList(rootElement, targetList, targetType, context, 0);
|
||||
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 non-list target '{targetType.Name}' with JSON array", json, targetType);
|
||||
throw new AcJsonDeserializationException($"Cannot populate object with JSON value of kind '{rootElement.ValueKind}'", json, targetType);
|
||||
|
||||
context.ResolveReferences();
|
||||
return;
|
||||
}
|
||||
|
||||
if (rootElement.ValueKind == JsonValueKind.Object)
|
||||
finally
|
||||
{
|
||||
var metadata = GetTypeMetadata(targetType);
|
||||
PopulateObjectInternalMerge(rootElement, target, metadata, context, 0);
|
||||
DeserializationContextPool.Return(context);
|
||||
}
|
||||
else
|
||||
throw new AcJsonDeserializationException($"Cannot populate object with JSON value of kind '{rootElement.ValueKind}'", json, targetType);
|
||||
|
||||
context.ResolveReferences();
|
||||
}
|
||||
catch (AcJsonDeserializationException) { throw; }
|
||||
catch (System.Text.Json.JsonException ex)
|
||||
|
|
@ -209,71 +281,202 @@ public static class AcJsonDeserializer
|
|||
|
||||
#endregion
|
||||
|
||||
#region Validation
|
||||
#region Utf8JsonReader Fast Path (STJ-style streaming)
|
||||
|
||||
private static void ValidateJson(string json, Type targetType)
|
||||
/// <summary>
|
||||
/// Deserialize using Utf8JsonReader - streaming without DOM allocation.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static T? DeserializeWithUtf8Reader<T>(string json, byte maxDepth)
|
||||
{
|
||||
if (string.IsNullOrEmpty(json)) return;
|
||||
|
||||
if (json.Length > 2 && json[0] == '"' && json[^1] == '"')
|
||||
var (buffer, length) = GetUtf8Bytes(json);
|
||||
try
|
||||
{
|
||||
var inner = json[1..^1];
|
||||
if (inner.Contains("\\\"") && (inner.Contains("{") || inner.Contains("[")))
|
||||
throw new AcJsonDeserializationException(
|
||||
$"Detected double-serialized JSON string. Target type: {targetType.Name}.",
|
||||
json, targetType);
|
||||
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);
|
||||
}
|
||||
|
||||
var isArrayJson = json.Length > 0 && json[0] == '[';
|
||||
var isObjectJson = json.Length > 0 && json[0] == '{';
|
||||
var isCollectionType = targetType.IsArray || IsGenericCollectionType(targetType);
|
||||
var isDictType = IsDictionaryType(targetType, out _, out _);
|
||||
|
||||
if (isArrayJson && !isCollectionType && !isDictType && targetType != typeof(object))
|
||||
throw new AcJsonDeserializationException($"JSON is an array but target type '{targetType.Name}' is not a collection type.", json, targetType);
|
||||
|
||||
if (isObjectJson && isCollectionType && !isDictType)
|
||||
throw new AcJsonDeserializationException($"JSON is an object but target type '{targetType.Name}' is a collection type (not dictionary).", json, targetType);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Core Reading Methods
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static object? ReadValue(JsonElement element, Type targetType, DeserializationContext context, int depth)
|
||||
private static object? DeserializeWithUtf8ReaderNonGeneric(string json, Type targetType, byte maxDepth)
|
||||
{
|
||||
return element.ValueKind switch
|
||||
var (buffer, length) = GetUtf8Bytes(json);
|
||||
try
|
||||
{
|
||||
JsonValueKind.Object => ReadObject(element, targetType, context, depth),
|
||||
JsonValueKind.Array => ReadArray(element, targetType, context, depth),
|
||||
JsonValueKind.Null => null,
|
||||
_ => ReadPrimitive(element, targetType, element.ValueKind)
|
||||
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? ReadObject(JsonElement element, Type targetType, DeserializationContext context, int depth)
|
||||
private static object? ReadStringFromReader(ref Utf8JsonReader reader, Type targetType)
|
||||
{
|
||||
// Handle $ref even if reference handling is disabled (for compatibility)
|
||||
if (context.UseReferenceHandling && element.TryGetProperty("$ref", out var refElement))
|
||||
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 refId = refElement.GetString()!;
|
||||
return context.TryGetReferencedObject(refId, out var refObj) ? refObj : new DeferredReference(refId, targetType);
|
||||
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;
|
||||
}
|
||||
|
||||
// Check depth limit
|
||||
if (depth > context.MaxDepth) return null;
|
||||
|
||||
if (IsDictionaryType(targetType, out var keyType, out var valueType))
|
||||
return ReadDictionary(element, keyType!, valueType!, context, depth);
|
||||
return ReadDictionaryFromReader(ref reader, keyType!, valueType!, maxDepth, depth);
|
||||
|
||||
var metadata = GetTypeMetadata(targetType);
|
||||
|
||||
object? instance;
|
||||
if (metadata.CompiledConstructor != null)
|
||||
instance = metadata.CompiledConstructor.Invoke();
|
||||
else
|
||||
var instance = metadata.CompiledConstructor?.Invoke();
|
||||
if (instance == null)
|
||||
{
|
||||
try { instance = Activator.CreateInstance(targetType); }
|
||||
catch (MissingMethodException ex)
|
||||
|
|
@ -286,172 +489,310 @@ public static class AcJsonDeserializer
|
|||
|
||||
if (instance == null) return null;
|
||||
|
||||
if (context.UseReferenceHandling && element.TryGetProperty("$id", out var idElement))
|
||||
context.RegisterObject(idElement.GetString()!, instance);
|
||||
|
||||
PopulateObjectInternal(element, instance, metadata, context, depth);
|
||||
var propsDict = metadata.PropertySettersFrozen;
|
||||
var nextDepth = depth + 1;
|
||||
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.EndObject)
|
||||
break;
|
||||
|
||||
if (reader.TokenType != JsonTokenType.PropertyName)
|
||||
continue;
|
||||
|
||||
var propName = reader.GetString();
|
||||
if (propName == null || !reader.Read())
|
||||
continue;
|
||||
|
||||
if (!propsDict.TryGetValue(propName, out var propInfo))
|
||||
{
|
||||
reader.Skip();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use cached version for faster type resolution
|
||||
var value = ReadValueFromReaderCached(ref reader, propInfo, maxDepth, nextDepth);
|
||||
propInfo.SetValue(instance, value);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
private static void PopulateObjectInternal(JsonElement element, object target, DeserializeTypeMetadata metadata, DeserializationContext context, int depth)
|
||||
/// <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)
|
||||
{
|
||||
foreach (var jsonProp in element.EnumerateObject())
|
||||
if (depth > maxDepth)
|
||||
{
|
||||
var propName = jsonProp.Name;
|
||||
|
||||
if (propName.Length > 0 && propName[0] == '$' && (propName == "$id" || propName == "$ref"))
|
||||
continue;
|
||||
|
||||
if (!metadata.PropertySetters.TryGetValue(propName, out var propInfo)) continue;
|
||||
|
||||
var value = ReadValue(jsonProp.Value, propInfo.PropertyType, context, depth + 1);
|
||||
|
||||
if (value is DeferredReference deferred)
|
||||
context.AddPropertyToResolve(target, propInfo, deferred.RefId);
|
||||
else
|
||||
propInfo.SetValue(target, value);
|
||||
reader.Skip();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void PopulateObjectInternalMerge(JsonElement element, object target, DeserializeTypeMetadata metadata, DeserializationContext context, int depth)
|
||||
{
|
||||
foreach (var jsonProp in element.EnumerateObject())
|
||||
var instance = metadata.CompiledConstructor?.Invoke();
|
||||
if (instance == null)
|
||||
{
|
||||
var propName = jsonProp.Name;
|
||||
|
||||
if (propName.Length > 0 && propName[0] == '$' && (propName == "$id" || propName == "$ref"))
|
||||
continue;
|
||||
|
||||
if (!metadata.PropertySetters.TryGetValue(propName, out var propInfo)) continue;
|
||||
|
||||
var propValue = jsonProp.Value;
|
||||
var propValueKind = propValue.ValueKind;
|
||||
|
||||
// Check depth limit for nested objects/collections
|
||||
if (depth + 1 > context.MaxDepth)
|
||||
try { instance = Activator.CreateInstance(targetType); }
|
||||
catch (MissingMethodException ex)
|
||||
{
|
||||
// At max depth, only set primitives
|
||||
if (propValueKind != JsonValueKind.Object && propValueKind != JsonValueKind.Array)
|
||||
{
|
||||
var primitiveValue = ReadPrimitive(propValue, propInfo.PropertyType, propValueKind);
|
||||
propInfo.SetValue(target, primitiveValue);
|
||||
}
|
||||
continue;
|
||||
throw new AcJsonDeserializationException(
|
||||
$"Cannot deserialize type '{targetType.FullName}' because it does not have a parameterless constructor.",
|
||||
null, targetType, ex);
|
||||
}
|
||||
|
||||
if (propInfo is { IsCollection: true, ElementIsIId: true } && propValueKind == JsonValueKind.Array)
|
||||
{
|
||||
var existingCollection = propInfo.GetValue(target);
|
||||
if (existingCollection != null)
|
||||
{
|
||||
MergeIIdCollection(propValue, existingCollection, propInfo, context, depth);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (propValueKind == JsonValueKind.Object)
|
||||
{
|
||||
if (context.UseReferenceHandling && propValue.TryGetProperty("$ref", out _))
|
||||
{
|
||||
var value = ReadValue(propValue, propInfo.PropertyType, context, depth + 1);
|
||||
if (value is DeferredReference deferred)
|
||||
context.AddPropertyToResolve(target, propInfo, deferred.RefId);
|
||||
else
|
||||
propInfo.SetValue(target, value);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!propInfo.PropertyType.IsPrimitive && propInfo.PropertyType != StringType)
|
||||
{
|
||||
var existingObj = propInfo.GetValue(target);
|
||||
if (existingObj != null)
|
||||
{
|
||||
var nestedMetadata = GetTypeMetadata(propInfo.PropertyType);
|
||||
PopulateObjectInternalMerge(propValue, existingObj, nestedMetadata, context, depth + 1);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var value2 = ReadValue(propValue, propInfo.PropertyType, context, depth + 1);
|
||||
|
||||
if (value2 is DeferredReference deferred2)
|
||||
context.AddPropertyToResolve(target, propInfo, deferred2.RefId);
|
||||
else
|
||||
propInfo.SetValue(target, value2);
|
||||
}
|
||||
}
|
||||
|
||||
private static void PopulateList(JsonElement arrayElement, IList targetList, Type listType, DeserializationContext context, int depth)
|
||||
{
|
||||
// Check depth limit
|
||||
if (depth > context.MaxDepth) return;
|
||||
|
||||
var elementType = GetCollectionElementType(listType);
|
||||
if (elementType == null) return;
|
||||
|
||||
var acObservable = targetList as IAcObservableCollection;
|
||||
acObservable?.BeginUpdate();
|
||||
if (instance == null) return null;
|
||||
|
||||
try
|
||||
var propsDict = metadata.PropertySettersFrozen;
|
||||
var nextDepth = depth + 1;
|
||||
|
||||
while (reader.Read())
|
||||
{
|
||||
targetList.Clear();
|
||||
if (reader.TokenType == JsonTokenType.EndObject)
|
||||
break;
|
||||
|
||||
foreach (var item in arrayElement.EnumerateArray())
|
||||
if (reader.TokenType != JsonTokenType.PropertyName)
|
||||
continue;
|
||||
|
||||
var propName = reader.GetString();
|
||||
if (propName == null || !reader.Read())
|
||||
continue;
|
||||
|
||||
if (!propsDict.TryGetValue(propName, out var propInfo))
|
||||
{
|
||||
var value = ReadValue(item, elementType, context, depth + 1);
|
||||
if (value != null)
|
||||
targetList.Add(value);
|
||||
reader.Skip();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use cached version for faster type resolution
|
||||
var value = ReadValueFromReaderCached(ref reader, propInfo, maxDepth, nextDepth);
|
||||
propInfo.SetValue(instance, value);
|
||||
}
|
||||
finally { acObservable?.EndUpdate(); }
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static object? ReadArray(JsonElement element, Type targetType, DeserializationContext context, int depth)
|
||||
private static object? ReadArrayFromReader(ref Utf8JsonReader reader, Type targetType, byte maxDepth, int depth)
|
||||
{
|
||||
// Check depth limit
|
||||
if (depth > context.MaxDepth) return null;
|
||||
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 list = GetOrCreateListFactory(elementType)();
|
||||
foreach (var item in element.EnumerateArray())
|
||||
list.Add(ReadValue(item, elementType, context, depth + 1));
|
||||
|
||||
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, depth + 1));
|
||||
}
|
||||
finally { acObservable?.EndUpdate(); }
|
||||
|
||||
return targetList;
|
||||
return list;
|
||||
}
|
||||
|
||||
private static void MergeIIdCollection(JsonElement arrayElement, object existingCollection, PropertySetterInfo propInfo, DeserializationContext context, int depth)
|
||||
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 propsDict = metadata.PropertySettersFrozen;
|
||||
var nextDepth = depth + 1;
|
||||
var maxDepthReached = nextDepth > maxDepth;
|
||||
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.EndObject)
|
||||
break;
|
||||
|
||||
if (reader.TokenType != JsonTokenType.PropertyName)
|
||||
continue;
|
||||
|
||||
var propName = reader.GetString();
|
||||
if (propName == null || !reader.Read())
|
||||
continue;
|
||||
|
||||
if (!propsDict.TryGetValue(propName, out var propInfo))
|
||||
{
|
||||
reader.Skip();
|
||||
continue;
|
||||
}
|
||||
|
||||
var tokenType = reader.TokenType;
|
||||
|
||||
if (maxDepthReached)
|
||||
{
|
||||
if (tokenType != JsonTokenType.StartObject && tokenType != JsonTokenType.StartArray)
|
||||
{
|
||||
// Use cached version for faster primitive reading
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Use cached version for faster type resolution
|
||||
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!;
|
||||
|
|
@ -481,6 +822,364 @@ public static class AcJsonDeserializer
|
|||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
@ -494,38 +1193,70 @@ public static class AcJsonDeserializer
|
|||
if (existingById.TryGetValue(itemId, out var existingItem))
|
||||
{
|
||||
var itemMetadata = GetTypeMetadata(elementType);
|
||||
PopulateObjectInternalMerge(jsonItem, existingItem, itemMetadata, context, depth + 1);
|
||||
PopulateObjectInternalMerge(jsonItem, existingItem, itemMetadata, context, nextDepth);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var newItem = ReadValue(jsonItem, elementType, context, depth + 1);
|
||||
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(JsonElement element, Type targetType, JsonValueKind valueKind)
|
||||
private static object? ReadPrimitive(in JsonElement element, in Type targetType, JsonValueKind valueKind)
|
||||
{
|
||||
var type = Nullable.GetUnderlyingType(targetType) ?? targetType;
|
||||
|
||||
if (valueKind == JsonValueKind.Number)
|
||||
{
|
||||
if (ReferenceEquals(type, IntType)) return element.GetInt32();
|
||||
if (ReferenceEquals(type, LongType)) return element.GetInt64();
|
||||
if (ReferenceEquals(type, DoubleType)) return element.GetDouble();
|
||||
if (ReferenceEquals(type, DecimalType)) return element.GetDecimal();
|
||||
if (ReferenceEquals(type, FloatType)) return element.GetSingle();
|
||||
if (type == ByteType) return element.GetByte();
|
||||
if (type == ShortType) return element.GetInt16();
|
||||
if (type == UShortType) return element.GetUInt16();
|
||||
if (type == UIntType) return element.GetUInt32();
|
||||
if (type == ULongType) return element.GetUInt64();
|
||||
if (type == SByteType) return element.GetSByte();
|
||||
if (type.IsEnum) return Enum.ToObject(type, element.GetInt32());
|
||||
return null;
|
||||
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)
|
||||
|
|
@ -533,8 +1264,8 @@ public static class AcJsonDeserializer
|
|||
if (ReferenceEquals(type, StringType)) return element.GetString();
|
||||
if (ReferenceEquals(type, DateTimeType)) return element.GetDateTime();
|
||||
if (ReferenceEquals(type, GuidType)) return element.GetGuid();
|
||||
if (type == DateTimeOffsetType) return element.GetDateTimeOffset();
|
||||
if (type == TimeSpanType) return TimeSpan.Parse(element.GetString()!, CultureInfo.InvariantCulture);
|
||||
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;
|
||||
}
|
||||
|
|
@ -545,56 +1276,62 @@ public static class AcJsonDeserializer
|
|||
return null;
|
||||
}
|
||||
|
||||
private static object ReadDictionary(JsonElement element, Type keyType, Type valueType, DeserializationContext context, int depth)
|
||||
{
|
||||
var dictType = DictionaryGenericType.MakeGenericType(keyType, valueType);
|
||||
var dict = (IDictionary)Activator.CreateInstance(dictType)!;
|
||||
|
||||
foreach (var prop in element.EnumerateObject())
|
||||
{
|
||||
if (prop.Name.StartsWith('$')) continue;
|
||||
|
||||
object key;
|
||||
if (keyType == StringType) key = prop.Name;
|
||||
else if (keyType == IntType) key = int.Parse(prop.Name, CultureInfo.InvariantCulture);
|
||||
else if (keyType == LongType) key = long.Parse(prop.Name, CultureInfo.InvariantCulture);
|
||||
else if (keyType == GuidType) key = Guid.Parse(prop.Name);
|
||||
else if (keyType.IsEnum) key = Enum.Parse(keyType, prop.Name);
|
||||
else key = Convert.ChangeType(prop.Name, keyType, CultureInfo.InvariantCulture);
|
||||
|
||||
dict.Add(key, ReadValue(prop.Value, valueType, context, depth + 1));
|
||||
}
|
||||
|
||||
return dict;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Primitive Deserialization
|
||||
|
||||
private static bool TryDeserializePrimitive(string json, Type targetType, out object? result)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool TryDeserializePrimitiveFast(string json, in Type targetType, out object? result)
|
||||
{
|
||||
var type = Nullable.GetUnderlyingType(targetType) ?? targetType;
|
||||
|
||||
if (ReferenceEquals(type, StringType))
|
||||
// Handle enums first
|
||||
if (type.IsEnum)
|
||||
{
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
result = doc.RootElement.GetString();
|
||||
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;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(type, IntType)) { result = int.Parse(json, CultureInfo.InvariantCulture); return true; }
|
||||
if (ReferenceEquals(type, LongType)) { result = long.Parse(json, CultureInfo.InvariantCulture); return true; }
|
||||
if (ReferenceEquals(type, BoolType)) { result = json == "true"; return true; }
|
||||
if (ReferenceEquals(type, DoubleType)) { result = double.Parse(json, CultureInfo.InvariantCulture); return true; }
|
||||
if (ReferenceEquals(type, DecimalType)) { result = decimal.Parse(json, CultureInfo.InvariantCulture); return true; }
|
||||
if (ReferenceEquals(type, FloatType)) { result = float.Parse(json, CultureInfo.InvariantCulture); return true; }
|
||||
var typeCode = Type.GetTypeCode(type);
|
||||
|
||||
if (ReferenceEquals(type, DateTimeType))
|
||||
switch (typeCode)
|
||||
{
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
result = doc.RootElement.GetDateTime();
|
||||
return true;
|
||||
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:
|
||||
using (var doc = JsonDocument.Parse(json))
|
||||
{
|
||||
result = doc.RootElement.GetString();
|
||||
return true;
|
||||
}
|
||||
case TypeCode.DateTime:
|
||||
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:
|
||||
using (var doc = JsonDocument.Parse(json))
|
||||
{
|
||||
var s = doc.RootElement.GetString();
|
||||
result = s?.Length > 0 ? s[0] : '\0';
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (ReferenceEquals(type, GuidType))
|
||||
|
|
@ -604,32 +1341,17 @@ public static class AcJsonDeserializer
|
|||
return true;
|
||||
}
|
||||
|
||||
if (type == DateTimeOffsetType) { using var doc = JsonDocument.Parse(json); result = doc.RootElement.GetDateTimeOffset(); return true; }
|
||||
if (type == TimeSpanType) { using var doc = JsonDocument.Parse(json); result = TimeSpan.Parse(doc.RootElement.GetString()!, CultureInfo.InvariantCulture); return true; }
|
||||
|
||||
if (type.IsEnum)
|
||||
if (ReferenceEquals(type, DateTimeOffsetType))
|
||||
{
|
||||
if (json.StartsWith('"'))
|
||||
{
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
result = Enum.Parse(type, doc.RootElement.GetString()!);
|
||||
}
|
||||
else result = Enum.ToObject(type, int.Parse(json, CultureInfo.InvariantCulture));
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
result = doc.RootElement.GetDateTimeOffset();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (type == ByteType) { result = byte.Parse(json, CultureInfo.InvariantCulture); return true; }
|
||||
if (type == ShortType) { result = short.Parse(json, CultureInfo.InvariantCulture); return true; }
|
||||
if (type == UShortType) { result = ushort.Parse(json, CultureInfo.InvariantCulture); return true; }
|
||||
if (type == UIntType) { result = uint.Parse(json, CultureInfo.InvariantCulture); return true; }
|
||||
if (type == ULongType) { result = ulong.Parse(json, CultureInfo.InvariantCulture); return true; }
|
||||
if (type == SByteType) { result = sbyte.Parse(json, CultureInfo.InvariantCulture); return true; }
|
||||
|
||||
if (type == CharType)
|
||||
if (ReferenceEquals(type, TimeSpanType))
|
||||
{
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
var s = doc.RootElement.GetString();
|
||||
result = s?.Length > 0 ? s[0] : '\0';
|
||||
result = TimeSpan.Parse(doc.RootElement.GetString()!, CultureInfo.InvariantCulture);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -639,15 +1361,46 @@ public static class AcJsonDeserializer
|
|||
|
||||
#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(Type type)
|
||||
private static DeserializeTypeMetadata GetTypeMetadata(in Type type)
|
||||
=> TypeMetadataCache.GetOrAdd(type, static t => new DeserializeTypeMetadata(t));
|
||||
|
||||
private sealed class DeserializeTypeMetadata
|
||||
{
|
||||
public Dictionary<string, PropertySetterInfo> PropertySetters { get; }
|
||||
public FrozenDictionary<string, PropertySetterInfo> PropertySettersFrozen { get; }
|
||||
public Func<object>? CompiledConstructor { get; }
|
||||
|
||||
public DeserializeTypeMetadata(Type type)
|
||||
|
|
@ -670,20 +1423,28 @@ public static class AcJsonDeserializer
|
|||
propsList.Add(p);
|
||||
}
|
||||
|
||||
PropertySetters = new Dictionary<string, PropertySetterInfo>(propsList.Count, StringComparer.OrdinalIgnoreCase);
|
||||
var propertySetters = new Dictionary<string, PropertySetterInfo>(propsList.Count, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var prop in propsList)
|
||||
PropertySetters[prop.Name] = new PropertySetterInfo(prop, type);
|
||||
{
|
||||
propertySetters[prop.Name] = new PropertySetterInfo(prop, type);
|
||||
}
|
||||
|
||||
// Create frozen dictionary for faster lookup in hot paths
|
||||
PropertySettersFrozen = propertySetters.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class PropertySetterInfo
|
||||
{
|
||||
public Type PropertyType { get; }
|
||||
public bool IsCollection { get; }
|
||||
public bool ElementIsIId { get; }
|
||||
public Type? ElementType { get; }
|
||||
public Type? ElementIdType { get; }
|
||||
public Func<object, object?>? ElementIdGetter { get; }
|
||||
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;
|
||||
|
||||
private readonly Action<object, object?> _setter;
|
||||
private readonly Func<object, object?> _getter;
|
||||
|
|
@ -691,21 +1452,26 @@ public static class AcJsonDeserializer
|
|||
public PropertySetterInfo(PropertyInfo prop, Type declaringType)
|
||||
{
|
||||
PropertyType = prop.PropertyType;
|
||||
var underlying = Nullable.GetUnderlyingType(PropertyType);
|
||||
IsNullable = underlying != null;
|
||||
UnderlyingType = underlying ?? PropertyType;
|
||||
PropertyTypeCode = Type.GetTypeCode(UnderlyingType);
|
||||
|
||||
_setter = CreateCompiledSetter(declaringType, prop);
|
||||
_getter = CreateCompiledGetter(declaringType, prop);
|
||||
|
||||
ElementType = GetCollectionElementType(PropertyType);
|
||||
IsCollection = ElementType != null && ElementType != typeof(object) &&
|
||||
var isCollection = ElementType != null && ElementType != typeof(object) &&
|
||||
typeof(IEnumerable).IsAssignableFrom(PropertyType) &&
|
||||
PropertyType != StringType;
|
||||
!ReferenceEquals(PropertyType, StringType);
|
||||
|
||||
if (IsCollection && ElementType != null)
|
||||
if (isCollection && ElementType != null)
|
||||
{
|
||||
var (isId, idType) = GetIdInfo(ElementType);
|
||||
if (isId)
|
||||
var idInfo = GetIdInfo(ElementType);
|
||||
if (idInfo.IsId)
|
||||
{
|
||||
ElementIsIId = true;
|
||||
ElementIdType = idType;
|
||||
IsIIdCollection = true;
|
||||
ElementIdType = idInfo.IdType;
|
||||
var idProp = ElementType.GetProperty("Id");
|
||||
if (idProp != null)
|
||||
ElementIdGetter = CreateCompiledGetter(ElementType, idProp);
|
||||
|
|
@ -762,16 +1528,26 @@ public static class AcJsonDeserializer
|
|||
private Dictionary<string, object>? _idToObject;
|
||||
private List<PropertyToResolve>? _propertiesToResolve;
|
||||
|
||||
public bool IsMergeMode { get; init; }
|
||||
public bool UseReferenceHandling { get; }
|
||||
public byte MaxDepth { get; }
|
||||
public bool IsMergeMode { get; set; }
|
||||
public bool UseReferenceHandling { get; private set; }
|
||||
public byte MaxDepth { get; private set; }
|
||||
|
||||
public DeserializationContext() : this(AcJsonSerializerOptions.Default) { }
|
||||
public DeserializationContext(in AcJsonSerializerOptions options)
|
||||
{
|
||||
Reset(options);
|
||||
}
|
||||
|
||||
public DeserializationContext(AcJsonSerializerOptions 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)]
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using System.Linq.Expressions;
|
|||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using AyCode.Core.Interfaces;
|
||||
using Newtonsoft.Json;
|
||||
using static AyCode.Core.Extensions.JsonUtilities;
|
||||
|
|
@ -14,92 +15,95 @@ namespace AyCode.Core.Extensions;
|
|||
|
||||
/// <summary>
|
||||
/// High-performance custom JSON serializer optimized for IId<T> reference handling.
|
||||
/// Features:
|
||||
/// - Single-pass serialization with inline $id/$ref emission
|
||||
/// - StringBuilder-based output (no intermediate string allocations)
|
||||
/// - Compiled expression tree property accessors
|
||||
/// - Smart reference tracking: only emits $id when object is actually referenced later
|
||||
/// - MaxDepth support for controlling serialization depth
|
||||
/// Uses Utf8JsonWriter for high-performance UTF-8 output (STJ approach).
|
||||
/// </summary>
|
||||
public static class AcJsonSerializer
|
||||
{
|
||||
private static readonly ConcurrentDictionary<Type, TypeMetadata> TypeMetadataCache = new();
|
||||
|
||||
// Pre-encoded property names for $id/$ref (STJ optimization)
|
||||
private static readonly JsonEncodedText IdPropertyEncoded = JsonEncodedText.Encode("$id");
|
||||
private static readonly JsonEncodedText RefPropertyEncoded = JsonEncodedText.Encode("$ref");
|
||||
|
||||
/// <summary>
|
||||
/// Serialize object to JSON string with default options.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static string Serialize<T>(T value) => Serialize(value, AcJsonSerializerOptions.Default);
|
||||
|
||||
/// <summary>
|
||||
/// Serialize object to JSON string with specified options.
|
||||
/// </summary>
|
||||
public static string Serialize<T>(T value, AcJsonSerializerOptions options)
|
||||
public static string Serialize<T>(T value, in AcJsonSerializerOptions options)
|
||||
{
|
||||
if (value == null) return "null";
|
||||
|
||||
var type = typeof(T);
|
||||
var type = value.GetType();
|
||||
|
||||
if (TrySerializePrimitive(value, type, out var primitiveJson))
|
||||
if (TrySerializePrimitiveRuntime(value, type, out var primitiveJson))
|
||||
return primitiveJson;
|
||||
|
||||
var context = new SerializationContext(options);
|
||||
|
||||
if (options.UseReferenceHandling)
|
||||
ScanReferences(value, context, 0);
|
||||
|
||||
context.StartWriting();
|
||||
WriteValue(value, context, 0);
|
||||
|
||||
return context.GetResult();
|
||||
var context = SerializationContextPool.Get(options);
|
||||
try
|
||||
{
|
||||
if (options.UseReferenceHandling)
|
||||
ScanReferences(value, context, 0);
|
||||
|
||||
WriteValue(value, context, 0);
|
||||
return context.GetResult();
|
||||
}
|
||||
finally
|
||||
{
|
||||
SerializationContextPool.Return(context);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TrySerializePrimitive<T>(T value, Type type, out string json)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool TrySerializePrimitiveRuntime(object value, in Type type, out string json)
|
||||
{
|
||||
var underlyingType = Nullable.GetUnderlyingType(type) ?? type;
|
||||
var typeCode = Type.GetTypeCode(underlyingType);
|
||||
|
||||
if (underlyingType == StringType) { json = SerializeString((string)(object)value!); return true; }
|
||||
if (underlyingType == IntType) { json = ((int)(object)value!).ToString(CultureInfo.InvariantCulture); return true; }
|
||||
if (underlyingType == LongType) { json = ((long)(object)value!).ToString(CultureInfo.InvariantCulture); return true; }
|
||||
if (underlyingType == BoolType) { json = (bool)(object)value! ? "true" : "false"; return true; }
|
||||
|
||||
if (underlyingType == DoubleType)
|
||||
switch (typeCode)
|
||||
{
|
||||
var d = (double)(object)value!;
|
||||
json = double.IsNaN(d) || double.IsInfinity(d) ? "null" : d.ToString("G17", CultureInfo.InvariantCulture);
|
||||
return true;
|
||||
case TypeCode.String: json = SerializeString((string)value); return true;
|
||||
case TypeCode.Int32: json = ((int)value).ToString(CultureInfo.InvariantCulture); return true;
|
||||
case TypeCode.Int64: json = ((long)value).ToString(CultureInfo.InvariantCulture); return true;
|
||||
case TypeCode.Boolean: json = (bool)value ? "true" : "false"; return true;
|
||||
case TypeCode.Double:
|
||||
var d = (double)value;
|
||||
json = double.IsNaN(d) || double.IsInfinity(d) ? "null" : d.ToString("G17", CultureInfo.InvariantCulture);
|
||||
return true;
|
||||
case TypeCode.Decimal: json = ((decimal)value).ToString(CultureInfo.InvariantCulture); return true;
|
||||
case TypeCode.Single:
|
||||
var f = (float)value;
|
||||
json = float.IsNaN(f) || float.IsInfinity(f) ? "null" : f.ToString("G9", CultureInfo.InvariantCulture);
|
||||
return true;
|
||||
case TypeCode.DateTime: json = $"\"{((DateTime)value).ToString("O", CultureInfo.InvariantCulture)}\""; return true;
|
||||
case TypeCode.Byte: json = ((byte)value).ToString(CultureInfo.InvariantCulture); return true;
|
||||
case TypeCode.Int16: json = ((short)value).ToString(CultureInfo.InvariantCulture); return true;
|
||||
case TypeCode.UInt16: json = ((ushort)value).ToString(CultureInfo.InvariantCulture); return true;
|
||||
case TypeCode.UInt32: json = ((uint)value).ToString(CultureInfo.InvariantCulture); return true;
|
||||
case TypeCode.UInt64: json = ((ulong)value).ToString(CultureInfo.InvariantCulture); return true;
|
||||
case TypeCode.SByte: json = ((sbyte)value).ToString(CultureInfo.InvariantCulture); return true;
|
||||
case TypeCode.Char: json = SerializeString(value.ToString()!); return true;
|
||||
}
|
||||
|
||||
if (underlyingType == DecimalType) { json = ((decimal)(object)value!).ToString(CultureInfo.InvariantCulture); return true; }
|
||||
|
||||
if (underlyingType == FloatType)
|
||||
{
|
||||
var f = (float)(object)value!;
|
||||
json = float.IsNaN(f) || float.IsInfinity(f) ? "null" : f.ToString("G9", CultureInfo.InvariantCulture);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (underlyingType == DateTimeType) { json = $"\"{((DateTime)(object)value!).ToString("O", CultureInfo.InvariantCulture)}\""; return true; }
|
||||
if (underlyingType == DateTimeOffsetType) { json = $"\"{((DateTimeOffset)(object)value!).ToString("O", CultureInfo.InvariantCulture)}\""; return true; }
|
||||
if (underlyingType == GuidType) { json = $"\"{((Guid)(object)value!).ToString("D")}\""; return true; }
|
||||
if (underlyingType == TimeSpanType) { json = $"\"{((TimeSpan)(object)value!).ToString("c", CultureInfo.InvariantCulture)}\""; return true; }
|
||||
if (ReferenceEquals(underlyingType, GuidType)) { json = $"\"{((Guid)value).ToString("D")}\""; return true; }
|
||||
if (ReferenceEquals(underlyingType, DateTimeOffsetType)) { json = $"\"{((DateTimeOffset)value).ToString("O", CultureInfo.InvariantCulture)}\""; return true; }
|
||||
if (ReferenceEquals(underlyingType, TimeSpanType)) { json = $"\"{((TimeSpan)value).ToString("c", CultureInfo.InvariantCulture)}\""; return true; }
|
||||
if (underlyingType.IsEnum) { json = Convert.ToInt32(value).ToString(CultureInfo.InvariantCulture); return true; }
|
||||
if (underlyingType == ByteType) { json = ((byte)(object)value!).ToString(CultureInfo.InvariantCulture); return true; }
|
||||
if (underlyingType == ShortType) { json = ((short)(object)value!).ToString(CultureInfo.InvariantCulture); return true; }
|
||||
if (underlyingType == UShortType) { json = ((ushort)(object)value!).ToString(CultureInfo.InvariantCulture); return true; }
|
||||
if (underlyingType == UIntType) { json = ((uint)(object)value!).ToString(CultureInfo.InvariantCulture); return true; }
|
||||
if (underlyingType == ULongType) { json = ((ulong)(object)value!).ToString(CultureInfo.InvariantCulture); return true; }
|
||||
if (underlyingType == SByteType) { json = ((sbyte)(object)value!).ToString(CultureInfo.InvariantCulture); return true; }
|
||||
if (underlyingType == CharType) { json = SerializeString(value!.ToString()!); return true; }
|
||||
|
||||
json = "";
|
||||
return false;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static string SerializeString(string value)
|
||||
{
|
||||
if (!NeedsEscaping(value)) return $"\"{value}\"";
|
||||
if (!NeedsEscaping(value)) return string.Concat("\"", value, "\"");
|
||||
|
||||
var sb = new StringBuilder(value.Length + 2);
|
||||
var sb = new StringBuilder(value.Length + 8);
|
||||
sb.Append('"');
|
||||
WriteEscapedString(sb, value);
|
||||
sb.Append('"');
|
||||
|
|
@ -110,16 +114,13 @@ public static class AcJsonSerializer
|
|||
|
||||
private static void ScanReferences(object? value, SerializationContext context, int depth)
|
||||
{
|
||||
if (value == null) return;
|
||||
if (depth > context.MaxDepth) return;
|
||||
if (value == null || depth > context.MaxDepth) return;
|
||||
|
||||
var type = value.GetType();
|
||||
|
||||
if (IsPrimitiveOrStringFast(type)) return;
|
||||
|
||||
if (!context.TrackForScanning(value)) return;
|
||||
|
||||
if (value is IEnumerable enumerable && type != StringType)
|
||||
if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType))
|
||||
{
|
||||
foreach (var item in enumerable)
|
||||
if (item != null) ScanReferences(item, context, depth + 1);
|
||||
|
|
@ -127,9 +128,11 @@ public static class AcJsonSerializer
|
|||
}
|
||||
|
||||
var metadata = GetTypeMetadata(type);
|
||||
foreach (var prop in metadata.Properties)
|
||||
var props = metadata.Properties;
|
||||
var propCount = props.Length;
|
||||
for (var i = 0; i < propCount; i++)
|
||||
{
|
||||
var propValue = prop.GetValue(value);
|
||||
var propValue = props[i].GetValue(value);
|
||||
if (propValue != null) ScanReferences(propValue, context, depth + 1);
|
||||
}
|
||||
}
|
||||
|
|
@ -140,133 +143,154 @@ public static class AcJsonSerializer
|
|||
|
||||
private static void WriteValue(object? value, SerializationContext context, int depth)
|
||||
{
|
||||
if (value == null) { context.WriteNull(); return; }
|
||||
if (value == null) { context.Writer.WriteNullValue(); return; }
|
||||
|
||||
var type = value.GetType();
|
||||
|
||||
if (TryWritePrimitive(value, type, context)) return;
|
||||
if (TryWritePrimitive(value, type, context.Writer)) return;
|
||||
|
||||
// Check depth limit for complex types
|
||||
if (depth > context.MaxDepth)
|
||||
{
|
||||
context.WriteNull();
|
||||
return;
|
||||
}
|
||||
if (depth > context.MaxDepth) { context.Writer.WriteNullValue(); return; }
|
||||
|
||||
if (value is IDictionary dictionary) { WriteDictionary(dictionary, context, depth); return; }
|
||||
if (value is IEnumerable enumerable && type != StringType) { WriteArray(enumerable, context, depth); return; }
|
||||
if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType)) { WriteArray(enumerable, context, depth); return; }
|
||||
|
||||
WriteObject(value, type, context, depth);
|
||||
}
|
||||
|
||||
private static void WriteObject(object value, Type type, SerializationContext context, int depth)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void WriteObject(object value, in Type type, SerializationContext context, int depth)
|
||||
{
|
||||
var writer = context.Writer;
|
||||
|
||||
if (context.UseReferenceHandling && context.TryGetExistingRef(value, out var refId))
|
||||
{
|
||||
context.WriteRef(refId);
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString(RefPropertyEncoded, refId);
|
||||
writer.WriteEndObject();
|
||||
return;
|
||||
}
|
||||
|
||||
context.WriteObjectStart();
|
||||
var isFirst = true;
|
||||
writer.WriteStartObject();
|
||||
|
||||
if (context.UseReferenceHandling && context.ShouldWriteId(value, out var id))
|
||||
{
|
||||
context.WritePropertyName("$id", ref isFirst);
|
||||
context.WriteString(id);
|
||||
writer.WriteString(IdPropertyEncoded, id);
|
||||
context.MarkAsWritten(value, id);
|
||||
}
|
||||
|
||||
var metadata = GetTypeMetadata(type);
|
||||
foreach (var prop in metadata.Properties)
|
||||
var props = metadata.Properties;
|
||||
var propCount = props.Length;
|
||||
var nextDepth = depth + 1;
|
||||
|
||||
for (var i = 0; i < propCount; i++)
|
||||
{
|
||||
var prop = props[i];
|
||||
var propValue = prop.GetValue(value);
|
||||
if (propValue == null) continue;
|
||||
if (IsSerializerDefaultValue(propValue, prop.PropertyType)) continue;
|
||||
if (IsDefaultValueFast(propValue, prop.PropertyTypeCode, prop.PropertyType)) continue;
|
||||
|
||||
context.WritePropertyName(prop.JsonName, ref isFirst);
|
||||
WriteValue(propValue, context, depth + 1);
|
||||
writer.WritePropertyName(prop.JsonNameEncoded);
|
||||
WriteValue(propValue, context, nextDepth);
|
||||
}
|
||||
|
||||
context.WriteObjectEnd();
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void WriteArray(IEnumerable enumerable, SerializationContext context, int depth)
|
||||
{
|
||||
context.WriteArrayStart();
|
||||
var isFirst = true;
|
||||
var writer = context.Writer;
|
||||
writer.WriteStartArray();
|
||||
var nextDepth = depth + 1;
|
||||
|
||||
foreach (var item in enumerable)
|
||||
{
|
||||
if (!isFirst) context.WriteComma();
|
||||
isFirst = false;
|
||||
WriteValue(item, context, depth + 1);
|
||||
WriteValue(item, context, nextDepth);
|
||||
}
|
||||
|
||||
context.WriteArrayEnd();
|
||||
writer.WriteEndArray();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void WriteDictionary(IDictionary dictionary, SerializationContext context, int depth)
|
||||
{
|
||||
context.WriteObjectStart();
|
||||
var isFirst = true;
|
||||
var writer = context.Writer;
|
||||
writer.WriteStartObject();
|
||||
var nextDepth = depth + 1;
|
||||
|
||||
foreach (DictionaryEntry entry in dictionary)
|
||||
{
|
||||
context.WritePropertyName(entry.Key?.ToString() ?? "", ref isFirst);
|
||||
WriteValue(entry.Value, context, depth + 1);
|
||||
writer.WritePropertyName(entry.Key?.ToString() ?? "");
|
||||
WriteValue(entry.Value, context, nextDepth);
|
||||
}
|
||||
|
||||
context.WriteObjectEnd();
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
private static bool TryWritePrimitive(object value, Type type, SerializationContext context)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool TryWritePrimitive(object value, in Type type, Utf8JsonWriter writer)
|
||||
{
|
||||
var underlyingType = Nullable.GetUnderlyingType(type) ?? type;
|
||||
var typeCode = Type.GetTypeCode(underlyingType);
|
||||
|
||||
if (underlyingType == StringType) { context.WriteString((string)value); return true; }
|
||||
if (underlyingType == IntType) { context.WriteInt((int)value); return true; }
|
||||
if (underlyingType == LongType) { context.WriteLong((long)value); return true; }
|
||||
if (underlyingType == BoolType) { context.WriteBool((bool)value); return true; }
|
||||
if (underlyingType == DoubleType) { context.WriteDouble((double)value); return true; }
|
||||
if (underlyingType == DecimalType) { context.WriteDecimal((decimal)value); return true; }
|
||||
if (underlyingType == FloatType) { context.WriteFloat((float)value); return true; }
|
||||
if (underlyingType == DateTimeType) { context.WriteDateTime((DateTime)value); return true; }
|
||||
if (underlyingType == DateTimeOffsetType) { context.WriteDateTimeOffset((DateTimeOffset)value); return true; }
|
||||
if (underlyingType == GuidType) { context.WriteGuid((Guid)value); return true; }
|
||||
if (underlyingType == TimeSpanType) { context.WriteTimeSpan((TimeSpan)value); return true; }
|
||||
if (underlyingType.IsEnum) { context.WriteInt(Convert.ToInt32(value)); return true; }
|
||||
if (underlyingType == ByteType) { context.WriteInt((byte)value); return true; }
|
||||
if (underlyingType == ShortType) { context.WriteInt((short)value); return true; }
|
||||
if (underlyingType == UShortType) { context.WriteInt((ushort)value); return true; }
|
||||
if (underlyingType == UIntType) { context.WriteLong((uint)value); return true; }
|
||||
if (underlyingType == ULongType) { context.WriteULong((ulong)value); return true; }
|
||||
if (underlyingType == SByteType) { context.WriteInt((sbyte)value); return true; }
|
||||
if (underlyingType == CharType) { context.WriteString(value.ToString()!); return true; }
|
||||
switch (typeCode)
|
||||
{
|
||||
case TypeCode.String: writer.WriteStringValue((string)value); return true;
|
||||
case TypeCode.Int32: writer.WriteNumberValue((int)value); return true;
|
||||
case TypeCode.Int64: writer.WriteNumberValue((long)value); return true;
|
||||
case TypeCode.Boolean: writer.WriteBooleanValue((bool)value); return true;
|
||||
case TypeCode.Double:
|
||||
var d = (double)value;
|
||||
if (double.IsNaN(d) || double.IsInfinity(d)) writer.WriteNullValue();
|
||||
else writer.WriteNumberValue(d);
|
||||
return true;
|
||||
case TypeCode.Decimal: writer.WriteNumberValue((decimal)value); return true;
|
||||
case TypeCode.Single:
|
||||
var f = (float)value;
|
||||
if (float.IsNaN(f) || float.IsInfinity(f)) writer.WriteNullValue();
|
||||
else writer.WriteNumberValue(f);
|
||||
return true;
|
||||
case TypeCode.DateTime: writer.WriteStringValue((DateTime)value); return true;
|
||||
case TypeCode.Byte: writer.WriteNumberValue((byte)value); return true;
|
||||
case TypeCode.Int16: writer.WriteNumberValue((short)value); return true;
|
||||
case TypeCode.UInt16: writer.WriteNumberValue((ushort)value); return true;
|
||||
case TypeCode.UInt32: writer.WriteNumberValue((uint)value); return true;
|
||||
case TypeCode.UInt64: writer.WriteNumberValue((ulong)value); return true;
|
||||
case TypeCode.SByte: writer.WriteNumberValue((sbyte)value); return true;
|
||||
case TypeCode.Char: writer.WriteStringValue(value.ToString()); return true;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(underlyingType, GuidType)) { writer.WriteStringValue((Guid)value); return true; }
|
||||
if (ReferenceEquals(underlyingType, DateTimeOffsetType)) { writer.WriteStringValue((DateTimeOffset)value); return true; }
|
||||
if (ReferenceEquals(underlyingType, TimeSpanType)) { writer.WriteStringValue(((TimeSpan)value).ToString("c", CultureInfo.InvariantCulture)); return true; }
|
||||
if (underlyingType.IsEnum) { writer.WriteNumberValue(Convert.ToInt32(value)); return true; }
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool IsSerializerDefaultValue(object value, Type propertyType)
|
||||
private static bool IsDefaultValueFast(object value, TypeCode typeCode, in Type propertyType)
|
||||
{
|
||||
var type = Nullable.GetUnderlyingType(propertyType) ?? propertyType;
|
||||
switch (typeCode)
|
||||
{
|
||||
case TypeCode.Int32: return (int)value == 0;
|
||||
case TypeCode.Int64: return (long)value == 0L;
|
||||
case TypeCode.Double: return (double)value == 0.0;
|
||||
case TypeCode.Decimal: return (decimal)value == 0m;
|
||||
case TypeCode.Single: return (float)value == 0f;
|
||||
case TypeCode.Byte: return (byte)value == 0;
|
||||
case TypeCode.Int16: return (short)value == 0;
|
||||
case TypeCode.UInt16: return (ushort)value == 0;
|
||||
case TypeCode.UInt32: return (uint)value == 0;
|
||||
case TypeCode.UInt64: return (ulong)value == 0;
|
||||
case TypeCode.SByte: return (sbyte)value == 0;
|
||||
case TypeCode.Boolean: return (bool)value == false;
|
||||
case TypeCode.String: return string.IsNullOrEmpty((string)value);
|
||||
}
|
||||
|
||||
if (type == IntType) return (int)value == 0;
|
||||
if (type == LongType) return (long)value == 0L;
|
||||
if (type == DoubleType) return (double)value == 0.0;
|
||||
if (type == DecimalType) return (decimal)value == 0m;
|
||||
if (type == FloatType) return (float)value == 0f;
|
||||
if (type == ByteType) return (byte)value == 0;
|
||||
if (type == ShortType) return (short)value == 0;
|
||||
if (type == UShortType) return (ushort)value == 0;
|
||||
if (type == UIntType) return (uint)value == 0;
|
||||
if (type == ULongType) return (ulong)value == 0;
|
||||
if (type == SByteType) return (sbyte)value == 0;
|
||||
if (type == BoolType) return (bool)value == false;
|
||||
if (type == StringType) return string.IsNullOrEmpty((string)value);
|
||||
if (type.IsEnum) return Convert.ToInt32(value) == 0;
|
||||
if (type == GuidType) return (Guid)value == Guid.Empty;
|
||||
if (propertyType.IsEnum) return Convert.ToInt32(value) == 0;
|
||||
if (ReferenceEquals(propertyType, GuidType)) return (Guid)value == Guid.Empty;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
@ -275,7 +299,8 @@ public static class AcJsonSerializer
|
|||
|
||||
#region Type Metadata
|
||||
|
||||
private static TypeMetadata GetTypeMetadata(Type type)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static TypeMetadata GetTypeMetadata(in Type type)
|
||||
=> TypeMetadataCache.GetOrAdd(type, static t => new TypeMetadata(t));
|
||||
|
||||
private sealed class TypeMetadata
|
||||
|
|
@ -295,14 +320,18 @@ public static class AcJsonSerializer
|
|||
|
||||
private sealed class PropertyAccessor
|
||||
{
|
||||
public string JsonName { get; }
|
||||
public Type PropertyType { get; }
|
||||
public readonly string JsonName;
|
||||
public readonly JsonEncodedText JsonNameEncoded; // STJ optimization - pre-encoded property name
|
||||
public readonly Type PropertyType;
|
||||
public readonly TypeCode PropertyTypeCode;
|
||||
private readonly Func<object, object?> _getter;
|
||||
|
||||
public PropertyAccessor(PropertyInfo prop)
|
||||
{
|
||||
JsonName = prop.Name;
|
||||
PropertyType = prop.PropertyType;
|
||||
JsonNameEncoded = JsonEncodedText.Encode(prop.Name);
|
||||
PropertyType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
|
||||
PropertyTypeCode = Type.GetTypeCode(PropertyType);
|
||||
_getter = CreateCompiledGetter(prop.DeclaringType!, prop);
|
||||
}
|
||||
|
||||
|
|
@ -321,35 +350,89 @@ public static class AcJsonSerializer
|
|||
|
||||
#endregion
|
||||
|
||||
#region Context Pool
|
||||
|
||||
private static class SerializationContextPool
|
||||
{
|
||||
private static readonly ConcurrentQueue<SerializationContext> Pool = new();
|
||||
private const int MaxPoolSize = 16;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SerializationContext Get(in AcJsonSerializerOptions options)
|
||||
{
|
||||
if (Pool.TryDequeue(out var context))
|
||||
{
|
||||
context.Reset(options);
|
||||
return context;
|
||||
}
|
||||
return new SerializationContext(options);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Return(SerializationContext context)
|
||||
{
|
||||
if (Pool.Count < MaxPoolSize)
|
||||
{
|
||||
context.Clear();
|
||||
Pool.Enqueue(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Serialization Context
|
||||
|
||||
private sealed class SerializationContext
|
||||
private sealed class SerializationContext : IDisposable
|
||||
{
|
||||
private readonly StringBuilder _sb = new(4096);
|
||||
private readonly Dictionary<object, int>? _scanOccurrences;
|
||||
private readonly Dictionary<object, string>? _writtenRefs;
|
||||
private readonly HashSet<object>? _multiReferenced;
|
||||
private int _nextId = 1;
|
||||
private readonly ArrayBufferWriter<byte> _buffer;
|
||||
public Utf8JsonWriter Writer { get; private set; }
|
||||
|
||||
private static readonly ArrayPool<char> CharPool = ArrayPool<char>.Shared;
|
||||
private readonly char[] _numberBuffer = CharPool.Rent(64);
|
||||
private Dictionary<object, int>? _scanOccurrences;
|
||||
private Dictionary<object, string>? _writtenRefs;
|
||||
private HashSet<object>? _multiReferenced;
|
||||
private int _nextId;
|
||||
|
||||
public bool UseReferenceHandling { get; }
|
||||
public byte MaxDepth { get; }
|
||||
public bool UseReferenceHandling { get; private set; }
|
||||
public byte MaxDepth { get; private set; }
|
||||
|
||||
public SerializationContext(AcJsonSerializerOptions options)
|
||||
private static readonly JsonWriterOptions WriterOptions = new()
|
||||
{
|
||||
Indented = false,
|
||||
SkipValidation = true // Skip validation for performance
|
||||
};
|
||||
|
||||
public SerializationContext(in AcJsonSerializerOptions options)
|
||||
{
|
||||
_buffer = new ArrayBufferWriter<byte>(4096);
|
||||
Writer = new Utf8JsonWriter(_buffer, WriterOptions);
|
||||
Reset(options);
|
||||
}
|
||||
|
||||
public void Reset(in AcJsonSerializerOptions options)
|
||||
{
|
||||
UseReferenceHandling = options.UseReferenceHandling;
|
||||
MaxDepth = options.MaxDepth;
|
||||
_nextId = 1;
|
||||
|
||||
if (UseReferenceHandling)
|
||||
{
|
||||
_scanOccurrences = new Dictionary<object, int>(64, ReferenceEqualityComparer.Instance);
|
||||
_writtenRefs = new Dictionary<object, string>(32, ReferenceEqualityComparer.Instance);
|
||||
_multiReferenced = new HashSet<object>(32, ReferenceEqualityComparer.Instance);
|
||||
_scanOccurrences ??= new Dictionary<object, int>(64, ReferenceEqualityComparer.Instance);
|
||||
_writtenRefs ??= new Dictionary<object, string>(32, ReferenceEqualityComparer.Instance);
|
||||
_multiReferenced ??= new HashSet<object>(32, ReferenceEqualityComparer.Instance);
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
Writer.Reset();
|
||||
_buffer.Clear();
|
||||
_scanOccurrences?.Clear();
|
||||
_writtenRefs?.Clear();
|
||||
_multiReferenced?.Clear();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TrackForScanning(object obj)
|
||||
{
|
||||
if (_scanOccurrences == null) return true;
|
||||
|
|
@ -360,8 +443,7 @@ public static class AcJsonSerializer
|
|||
return true;
|
||||
}
|
||||
|
||||
public void StartWriting() { }
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool ShouldWriteId(object obj, out string id)
|
||||
{
|
||||
if (_multiReferenced != null && _multiReferenced.Contains(obj) && !_writtenRefs!.ContainsKey(obj))
|
||||
|
|
@ -373,122 +455,26 @@ public static class AcJsonSerializer
|
|||
return false;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void MarkAsWritten(object obj, string id) => _writtenRefs![obj] = id;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryGetExistingRef(object obj, out string refId)
|
||||
{
|
||||
if (_writtenRefs != null) return _writtenRefs.TryGetValue(obj, out refId!);
|
||||
refId = "";
|
||||
return false;
|
||||
}
|
||||
|
||||
public void WriteRef(string refId) { _sb.Append("{\"$ref\":\""); _sb.Append(refId); _sb.Append("\"}"); }
|
||||
|
||||
public string GetResult() { CharPool.Return(_numberBuffer); return _sb.ToString(); }
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteNull() => _sb.Append("null");
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteObjectStart() => _sb.Append('{');
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteObjectEnd() => _sb.Append('}');
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteArrayStart() => _sb.Append('[');
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteArrayEnd() => _sb.Append(']');
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteComma() => _sb.Append(',');
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WritePropertyName(string name, ref bool isFirst)
|
||||
public string GetResult()
|
||||
{
|
||||
if (!isFirst) _sb.Append(',');
|
||||
isFirst = false;
|
||||
_sb.Append('"'); _sb.Append(name); _sb.Append("\":");
|
||||
Writer.Flush();
|
||||
return Encoding.UTF8.GetString(_buffer.WrittenSpan);
|
||||
}
|
||||
|
||||
public void WriteString(string value)
|
||||
public void Dispose()
|
||||
{
|
||||
_sb.Append('"');
|
||||
if (!NeedsEscaping(value)) _sb.Append(value);
|
||||
else WriteEscapedString(_sb, value);
|
||||
_sb.Append('"');
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteInt(int value)
|
||||
{
|
||||
if (value.TryFormat(_numberBuffer, out var written)) _sb.Append(_numberBuffer, 0, written);
|
||||
else _sb.Append(value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteLong(long value)
|
||||
{
|
||||
if (value.TryFormat(_numberBuffer, out var written)) _sb.Append(_numberBuffer, 0, written);
|
||||
else _sb.Append(value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteULong(ulong value)
|
||||
{
|
||||
if (value.TryFormat(_numberBuffer, out var written)) _sb.Append(_numberBuffer, 0, written);
|
||||
else _sb.Append(value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteBool(bool value) => _sb.Append(value ? "true" : "false");
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteDouble(double value)
|
||||
{
|
||||
if (double.IsNaN(value) || double.IsInfinity(value)) _sb.Append("null");
|
||||
else if (value.TryFormat(_numberBuffer, out var written, "G17", CultureInfo.InvariantCulture)) _sb.Append(_numberBuffer, 0, written);
|
||||
else _sb.Append(value.ToString("G17", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteFloat(float value)
|
||||
{
|
||||
if (float.IsNaN(value) || float.IsInfinity(value)) _sb.Append("null");
|
||||
else if (value.TryFormat(_numberBuffer, out var written, "G9", CultureInfo.InvariantCulture)) _sb.Append(_numberBuffer, 0, written);
|
||||
else _sb.Append(value.ToString("G9", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteDecimal(decimal value)
|
||||
{
|
||||
if (value.TryFormat(_numberBuffer, out var written, provider: CultureInfo.InvariantCulture)) _sb.Append(_numberBuffer, 0, written);
|
||||
else _sb.Append(value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
public void WriteDateTime(DateTime value)
|
||||
{
|
||||
_sb.Append('"');
|
||||
Span<char> buffer = stackalloc char[33];
|
||||
if (value.TryFormat(buffer, out var written, "O", CultureInfo.InvariantCulture)) _sb.Append(buffer[..written]);
|
||||
else _sb.Append(value.ToString("O", CultureInfo.InvariantCulture));
|
||||
_sb.Append('"');
|
||||
}
|
||||
|
||||
public void WriteDateTimeOffset(DateTimeOffset value)
|
||||
{
|
||||
_sb.Append('"');
|
||||
Span<char> buffer = stackalloc char[33];
|
||||
if (value.TryFormat(buffer, out var written, "O", CultureInfo.InvariantCulture)) _sb.Append(buffer[..written]);
|
||||
else _sb.Append(value.ToString("O", CultureInfo.InvariantCulture));
|
||||
_sb.Append('"');
|
||||
}
|
||||
|
||||
public void WriteGuid(Guid value)
|
||||
{
|
||||
_sb.Append('"');
|
||||
Span<char> buffer = stackalloc char[36];
|
||||
if (value.TryFormat(buffer, out var written, "D")) _sb.Append(buffer[..written]);
|
||||
else _sb.Append(value.ToString("D"));
|
||||
_sb.Append('"');
|
||||
}
|
||||
|
||||
public void WriteTimeSpan(TimeSpan value)
|
||||
{
|
||||
_sb.Append('"');
|
||||
Span<char> buffer = stackalloc char[26];
|
||||
if (value.TryFormat(buffer, out var written, "c", CultureInfo.InvariantCulture)) _sb.Append(buffer[..written]);
|
||||
else _sb.Append(value.ToString("c", CultureInfo.InvariantCulture));
|
||||
_sb.Append('"');
|
||||
Writer.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Buffers;
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Frozen;
|
||||
|
|
@ -51,6 +52,27 @@ public sealed class AcJsonSerializerOptions
|
|||
public static AcJsonSerializerOptions WithoutReferenceHandling() => new() { UseReferenceHandling = false };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cached result for IId type info lookup.
|
||||
/// </summary>
|
||||
public readonly struct IdTypeInfo
|
||||
{
|
||||
public readonly bool IsId;
|
||||
public readonly Type? IdType;
|
||||
|
||||
public IdTypeInfo(bool isId, Type? idType)
|
||||
{
|
||||
IsId = isId;
|
||||
IdType = idType;
|
||||
}
|
||||
|
||||
public void Deconstruct(out bool isId, out Type? idType)
|
||||
{
|
||||
isId = IsId;
|
||||
idType = IdType;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Central utilities for JSON serialization/deserialization.
|
||||
/// Contains shared type caches, primitive type checks, and string utilities.
|
||||
|
|
@ -109,7 +131,7 @@ public static class JsonUtilities
|
|||
|
||||
#region Type Caches
|
||||
|
||||
private static readonly ConcurrentDictionary<Type, (bool IsId, Type? IdType)> IdInfoCache = new();
|
||||
private static readonly ConcurrentDictionary<Type, IdTypeInfo> IdInfoCache = new();
|
||||
private static readonly ConcurrentDictionary<Type, Type?> CollectionElementCache = new();
|
||||
private static readonly ConcurrentDictionary<Type, bool> IsPrimitiveCache = new();
|
||||
private static readonly ConcurrentDictionary<Type, bool> IsCollectionCache = new();
|
||||
|
|
@ -119,6 +141,37 @@ public static class JsonUtilities
|
|||
|
||||
#endregion
|
||||
|
||||
#region UTF8 Buffer Pool
|
||||
|
||||
/// <summary>
|
||||
/// Rents a UTF8 byte buffer from the shared pool.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static byte[] RentUtf8Buffer(int minLength)
|
||||
=> ArrayPool<byte>.Shared.Rent(minLength);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a UTF8 byte buffer to the shared pool.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ReturnUtf8Buffer(byte[] buffer)
|
||||
=> ArrayPool<byte>.Shared.Return(buffer);
|
||||
|
||||
/// <summary>
|
||||
/// Converts a JSON string to UTF8 bytes using pooled buffer.
|
||||
/// Returns the actual byte count written.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static (byte[] buffer, int length) GetUtf8Bytes(string json)
|
||||
{
|
||||
var maxByteCount = Encoding.UTF8.GetMaxByteCount(json.Length);
|
||||
var buffer = RentUtf8Buffer(maxByteCount);
|
||||
var actualLength = Encoding.UTF8.GetBytes(json, 0, json.Length, buffer, 0);
|
||||
return (buffer, actualLength);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region String Utilities
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -211,6 +264,20 @@ public static class JsonUtilities
|
|||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a span needs JSON escaping.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool NeedsEscaping(ReadOnlySpan<char> value)
|
||||
{
|
||||
foreach (var c in value)
|
||||
{
|
||||
if (c < 32 || c == '"' || c == '\\')
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Escapes a string for JSON output.
|
||||
/// </summary>
|
||||
|
|
@ -238,6 +305,34 @@ public static class JsonUtilities
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Escapes a span for JSON output.
|
||||
/// </summary>
|
||||
public static void WriteEscapedString(StringBuilder sb, ReadOnlySpan<char> value)
|
||||
{
|
||||
foreach (var c in value)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case '"': sb.Append("\\\""); break;
|
||||
case '\\': sb.Append("\\\\"); break;
|
||||
case '\b': sb.Append("\\b"); break;
|
||||
case '\f': sb.Append("\\f"); break;
|
||||
case '\n': sb.Append("\\n"); break;
|
||||
case '\r': sb.Append("\\r"); break;
|
||||
case '\t': sb.Append("\\t"); break;
|
||||
default:
|
||||
if (c < 32)
|
||||
{
|
||||
sb.Append("\\u");
|
||||
sb.Append(((int)c).ToString("X4"));
|
||||
}
|
||||
else sb.Append(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
|
@ -263,7 +358,7 @@ public static class JsonUtilities
|
|||
/// Faster primitive check using TypeCode for hot paths.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsPrimitiveOrStringFast(Type type)
|
||||
public static bool IsPrimitiveOrStringFast(in Type type)
|
||||
{
|
||||
var typeCode = Type.GetTypeCode(type);
|
||||
return typeCode switch
|
||||
|
|
@ -272,7 +367,7 @@ public static class JsonUtilities
|
|||
TypeCode.Int16 or TypeCode.UInt16 or TypeCode.Int32 or TypeCode.UInt32 or
|
||||
TypeCode.Int64 or TypeCode.UInt64 or TypeCode.Single or TypeCode.Double or
|
||||
TypeCode.Decimal or TypeCode.DateTime or TypeCode.String => true,
|
||||
_ => type == GuidType || type == TimeSpanType || type == DateTimeOffsetType || type.IsEnum
|
||||
_ => ReferenceEquals(type, GuidType) || ReferenceEquals(type, TimeSpanType) || ReferenceEquals(type, DateTimeOffsetType) || type.IsEnum
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -280,28 +375,28 @@ public static class JsonUtilities
|
|||
/// Checks if type is a generic collection type (List, IList, ObservableCollection, etc.)
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsGenericCollectionType(Type type)
|
||||
public static bool IsGenericCollectionType(in Type type)
|
||||
{
|
||||
return IsCollectionCache.GetOrAdd(type, static t =>
|
||||
{
|
||||
if (t == StringType || t.IsPrimitive) return false;
|
||||
if (ReferenceEquals(t, StringType) || t.IsPrimitive) return false;
|
||||
if (t.IsArray) return true;
|
||||
|
||||
if (t.IsGenericType)
|
||||
{
|
||||
var genericDef = t.GetGenericTypeDefinition();
|
||||
if (genericDef == ListGenericType ||
|
||||
genericDef == IListGenericType ||
|
||||
if (ReferenceEquals(genericDef, ListGenericType) ||
|
||||
ReferenceEquals(genericDef, IListGenericType) ||
|
||||
genericDef == typeof(ICollection<>) ||
|
||||
genericDef == IEnumerableGenericType ||
|
||||
genericDef == ObservableCollectionType ||
|
||||
genericDef == CollectionType)
|
||||
ReferenceEquals(genericDef, IEnumerableGenericType) ||
|
||||
ReferenceEquals(genericDef, ObservableCollectionType) ||
|
||||
ReferenceEquals(genericDef, CollectionType))
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (var iface in t.GetInterfaces())
|
||||
{
|
||||
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == IListGenericType)
|
||||
if (iface.IsGenericType && ReferenceEquals(iface.GetGenericTypeDefinition(), IListGenericType))
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -313,7 +408,7 @@ public static class JsonUtilities
|
|||
/// Checks if type is a dictionary type.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsDictionaryType(Type type, out Type? keyType, out Type? valueType)
|
||||
public static bool IsDictionaryType(in Type type, out Type? keyType, out Type? valueType)
|
||||
{
|
||||
keyType = null;
|
||||
valueType = null;
|
||||
|
|
@ -321,7 +416,7 @@ public static class JsonUtilities
|
|||
if (!type.IsGenericType) return false;
|
||||
|
||||
var genericDef = type.GetGenericTypeDefinition();
|
||||
if (genericDef == DictionaryGenericType || genericDef == IDictionaryGenericType)
|
||||
if (ReferenceEquals(genericDef, DictionaryGenericType) || ReferenceEquals(genericDef, IDictionaryGenericType))
|
||||
{
|
||||
var args = type.GetGenericArguments();
|
||||
keyType = args[0];
|
||||
|
|
@ -331,7 +426,7 @@ public static class JsonUtilities
|
|||
|
||||
foreach (var iface in type.GetInterfaces())
|
||||
{
|
||||
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == IDictionaryGenericType)
|
||||
if (iface.IsGenericType && ReferenceEquals(iface.GetGenericTypeDefinition(), IDictionaryGenericType))
|
||||
{
|
||||
var args = iface.GetGenericArguments();
|
||||
keyType = args[0];
|
||||
|
|
@ -347,7 +442,7 @@ public static class JsonUtilities
|
|||
/// Gets the element type of a collection.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Type? GetCollectionElementType(Type collectionType)
|
||||
public static Type? GetCollectionElementType(in Type collectionType)
|
||||
{
|
||||
return CollectionElementCache.GetOrAdd(collectionType, static type =>
|
||||
{
|
||||
|
|
@ -357,14 +452,14 @@ public static class JsonUtilities
|
|||
if (type.IsGenericType)
|
||||
{
|
||||
var genericDef = type.GetGenericTypeDefinition();
|
||||
if (genericDef == ListGenericType || genericDef == IListGenericType ||
|
||||
genericDef == typeof(ICollection<>) || genericDef == IEnumerableGenericType)
|
||||
if (ReferenceEquals(genericDef, ListGenericType) || ReferenceEquals(genericDef, IListGenericType) ||
|
||||
genericDef == typeof(ICollection<>) || ReferenceEquals(genericDef, IEnumerableGenericType))
|
||||
return type.GetGenericArguments()[0];
|
||||
}
|
||||
|
||||
foreach (var iface in type.GetInterfaces())
|
||||
{
|
||||
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == IListGenericType)
|
||||
if (iface.IsGenericType && ReferenceEquals(iface.GetGenericTypeDefinition(), IListGenericType))
|
||||
return iface.GetGenericArguments()[0];
|
||||
}
|
||||
|
||||
|
|
@ -373,21 +468,21 @@ public static class JsonUtilities
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets IId info for a type.
|
||||
/// Gets IId info for a type. Returns struct to avoid allocation.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static (bool IsId, Type? IdType) GetIdInfo(Type type)
|
||||
public static IdTypeInfo GetIdInfo(in Type type)
|
||||
{
|
||||
return IdInfoCache.GetOrAdd(type, static t =>
|
||||
{
|
||||
foreach (var iface in t.GetInterfaces())
|
||||
{
|
||||
if (!iface.IsGenericType) continue;
|
||||
if (iface.GetGenericTypeDefinition() != IIdGenericType) continue;
|
||||
if (!ReferenceEquals(iface.GetGenericTypeDefinition(), IIdGenericType)) continue;
|
||||
var idType = iface.GetGenericArguments()[0];
|
||||
return (idType.IsValueType, idType);
|
||||
return new IdTypeInfo(idType.IsValueType, idType);
|
||||
}
|
||||
return (false, null);
|
||||
return new IdTypeInfo(false, null);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -406,11 +501,11 @@ public static class JsonUtilities
|
|||
/// Checks if collection contains primitive elements.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsPrimitiveElementCollection(Type type)
|
||||
public static bool IsPrimitiveElementCollection(in Type type)
|
||||
{
|
||||
return IsPrimitiveCollectionCache.GetOrAdd(type, static t =>
|
||||
{
|
||||
if (t == StringType) return false;
|
||||
if (ReferenceEquals(t, StringType)) return false;
|
||||
|
||||
Type? elementType = null;
|
||||
if (t.IsArray)
|
||||
|
|
@ -430,7 +525,7 @@ public static class JsonUtilities
|
|||
/// Gets or creates a list factory for a given element type.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Func<IList> GetOrCreateListFactory(Type elementType)
|
||||
public static Func<IList> GetOrCreateListFactory(in Type elementType)
|
||||
{
|
||||
return ListFactoryCache.GetOrAdd(elementType, static t =>
|
||||
{
|
||||
|
|
@ -445,7 +540,7 @@ public static class JsonUtilities
|
|||
/// Checks if value is the default value for its type.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsDefaultValue(object id, Type idType)
|
||||
public static bool IsDefaultValue(in object id, in Type idType)
|
||||
{
|
||||
if (ReferenceEquals(idType, IntType)) return (int)id == 0;
|
||||
if (ReferenceEquals(idType, LongType)) return (long)id == 0;
|
||||
|
|
|
|||
|
|
@ -40,18 +40,18 @@ public sealed class CachedPropertyInfo
|
|||
|
||||
if (!ShouldSkip)
|
||||
{
|
||||
var (isId, idType) = GetIdInfo(PropertyType);
|
||||
IsIId = isId;
|
||||
IdType = idType;
|
||||
var idInfo = GetIdInfo(PropertyType);
|
||||
IsIId = idInfo.IsId;
|
||||
IdType = idInfo.IdType;
|
||||
|
||||
if (!IsIId && typeof(IEnumerable).IsAssignableFrom(PropertyType) && PropertyType != StringType)
|
||||
if (!IsIId && typeof(IEnumerable).IsAssignableFrom(PropertyType) && !ReferenceEquals(PropertyType, StringType))
|
||||
{
|
||||
CollectionElementType = GetCollectionElementType(PropertyType);
|
||||
if (CollectionElementType != null)
|
||||
{
|
||||
var (elemIsId, elemIdType) = GetIdInfo(CollectionElementType);
|
||||
IsIIdCollection = elemIsId;
|
||||
CollectionElementIdType = elemIdType;
|
||||
var elemIdInfo = GetIdInfo(CollectionElementType);
|
||||
IsIIdCollection = elemIdInfo.IsId;
|
||||
CollectionElementIdType = elemIdInfo.IdType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -315,7 +315,7 @@ public class UnifiedMergeContractResolver : DefaultContractResolver
|
|||
var t = property.PropertyType;
|
||||
if (t == null) return property;
|
||||
|
||||
var config = GetOrCreatePropertyConfig(member, t);
|
||||
var config = GetOrCreatePropertyConfigRef(member, t);
|
||||
|
||||
if (config.IsPrimitiveElementCollection)
|
||||
{
|
||||
|
|
@ -343,7 +343,7 @@ public class UnifiedMergeContractResolver : DefaultContractResolver
|
|||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static CachedPropertyConfig GetOrCreatePropertyConfig(MemberInfo member, Type propertyType)
|
||||
private static CachedPropertyConfig GetOrCreatePropertyConfigRef(MemberInfo member, Type propertyType)
|
||||
=> PropertyConfigCache.GetOrAdd(member, _ => CreatePropertyConfig(member, propertyType));
|
||||
|
||||
private static CachedPropertyConfig CreatePropertyConfig(MemberInfo member, Type propertyType)
|
||||
|
|
@ -360,11 +360,11 @@ public class UnifiedMergeContractResolver : DefaultContractResolver
|
|||
config.ElementType = GetCollectionElementType(propertyType);
|
||||
if (config.ElementType != null)
|
||||
{
|
||||
var (hasId, elemIdType) = GetIdInfo(config.ElementType);
|
||||
if (hasId && elemIdType != null)
|
||||
var idInfo = GetIdInfo(config.ElementType);
|
||||
if (idInfo.IsId && idInfo.IdType != null)
|
||||
{
|
||||
config.IsIdCollection = true;
|
||||
config.IdType = elemIdType;
|
||||
config.IdType = idInfo.IdType;
|
||||
}
|
||||
config.IsPrimitiveElement = IsPrimitiveOrString(config.ElementType);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,137 +1,339 @@
|
|||
using AyCode.Core.Extensions;
|
||||
using AyCode.Core.Tests.TestModels;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Columns;
|
||||
using BenchmarkDotNet.Configs;
|
||||
using BenchmarkDotNet.Reports;
|
||||
using Newtonsoft.Json;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using JsonSerializer = System.Text.Json.JsonSerializer;
|
||||
|
||||
namespace AyCode.Core.Benchmarks;
|
||||
|
||||
/// <summary>
|
||||
/// Serialization benchmarks comparing AcJsonSerializer/Deserializer with Newtonsoft.Json.
|
||||
/// Uses shared TestModels from AyCode.Core.Tests for consistency.
|
||||
/// Serialization benchmarks comparing AyCode, Newtonsoft.Json, and System.Text.Json.
|
||||
/// Tests small, medium, and large data with and without reference handling.
|
||||
/// </summary>
|
||||
[MemoryDiagnoser]
|
||||
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
|
||||
[CategoriesColumn]
|
||||
public class SerializationBenchmarks
|
||||
{
|
||||
// Test data - uses shared TestModels
|
||||
private TestOrder _testOrder = null!;
|
||||
// Test data - small, medium, large
|
||||
private TestOrder _smallOrder = null!;
|
||||
private TestOrder _mediumOrder = null!;
|
||||
private TestOrder _largeOrder = null!;
|
||||
|
||||
// Pre-serialized JSON for deserialization benchmarks
|
||||
private string _newtonsoftJson = null!;
|
||||
private string _ayCodeJson = null!;
|
||||
private string _smallAyCodeJson = null!;
|
||||
private string _smallAyCodeNoRefJson = null!;
|
||||
private string _smallStjJson = null!;
|
||||
private string _smallStjNoRefJson = null!;
|
||||
private string _smallNewtonsoftJson = null!;
|
||||
private string _mediumAyCodeJson = null!;
|
||||
private string _mediumAyCodeNoRefJson = null!;
|
||||
private string _mediumStjJson = null!;
|
||||
private string _mediumStjNoRefJson = null!;
|
||||
private string _mediumNewtonsoftJson = null!;
|
||||
private string _largeAyCodeJson = null!;
|
||||
private string _largeAyCodeNoRefJson = null!;
|
||||
private string _largeStjJson = null!;
|
||||
private string _largeStjNoRefJson = null!;
|
||||
private string _largeNewtonsoftJson = null!;
|
||||
|
||||
// Target objects for Populate benchmarks
|
||||
private TestOrder _populateTarget = null!;
|
||||
// STJ options
|
||||
private JsonSerializerOptions _stjWithRefs = null!;
|
||||
private JsonSerializerOptions _stjNoRefs = null!;
|
||||
|
||||
// Settings
|
||||
private JsonSerializerSettings _newtonsoftNoRefSettings = null!;
|
||||
// AyCode options
|
||||
private AcJsonSerializerOptions _ayCodeWithRefs = null!;
|
||||
private AcJsonSerializerOptions _ayCodeNoRefs = null!;
|
||||
|
||||
// Newtonsoft settings
|
||||
private JsonSerializerSettings _newtonsoftSettings = null!;
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
// Newtonsoft WITHOUT reference handling (baseline)
|
||||
_newtonsoftNoRefSettings = new JsonSerializerSettings
|
||||
{
|
||||
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
DefaultValueHandling = DefaultValueHandling.Ignore,
|
||||
Formatting = Formatting.None
|
||||
};
|
||||
// Small: ~20 objects (1 item × 1 pallet × 2 measurements × 3 points)
|
||||
_smallOrder = TestDataFactory.CreateBenchmarkOrder(
|
||||
itemCount: 1,
|
||||
palletsPerItem: 1,
|
||||
measurementsPerPallet: 2,
|
||||
pointsPerMeasurement: 3);
|
||||
|
||||
// Create benchmark data using shared factory
|
||||
// ~1500 objects: 5 items × 4 pallets × 3 measurements × 5 points = 300 points + containers
|
||||
_testOrder = TestDataFactory.CreateBenchmarkOrder(
|
||||
// Medium: ~300 objects (3 items × 2 pallets × 2 measurements × 5 points)
|
||||
_mediumOrder = TestDataFactory.CreateBenchmarkOrder(
|
||||
itemCount: 3,
|
||||
palletsPerItem: 2,
|
||||
measurementsPerPallet: 2,
|
||||
pointsPerMeasurement: 5);
|
||||
|
||||
// Large: ~1500 objects (5 items × 4 pallets × 3 measurements × 5 points)
|
||||
_largeOrder = TestDataFactory.CreateBenchmarkOrder(
|
||||
itemCount: 5,
|
||||
palletsPerItem: 4,
|
||||
measurementsPerPallet: 3,
|
||||
pointsPerMeasurement: 5);
|
||||
|
||||
// Pre-serialize for deserialization benchmarks
|
||||
_newtonsoftJson = JsonConvert.SerializeObject(_testOrder, _newtonsoftNoRefSettings);
|
||||
_ayCodeJson = _testOrder.ToJson();
|
||||
// STJ options with reference handling
|
||||
_stjWithRefs = new JsonSerializerOptions
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
PropertyNamingPolicy = null,
|
||||
WriteIndented = false,
|
||||
ReferenceHandler = ReferenceHandler.Preserve,
|
||||
MaxDepth = 256
|
||||
};
|
||||
|
||||
// Create target for populate benchmarks
|
||||
_populateTarget = new TestOrder();
|
||||
// STJ options without reference handling
|
||||
_stjNoRefs = new JsonSerializerOptions
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
PropertyNamingPolicy = null,
|
||||
WriteIndented = false,
|
||||
ReferenceHandler = ReferenceHandler.IgnoreCycles,
|
||||
MaxDepth = 256
|
||||
};
|
||||
|
||||
// AyCode options
|
||||
_ayCodeWithRefs = AcJsonSerializerOptions.Default;
|
||||
_ayCodeNoRefs = AcJsonSerializerOptions.WithoutReferenceHandling();
|
||||
|
||||
// Newtonsoft settings
|
||||
_newtonsoftSettings = new JsonSerializerSettings
|
||||
{
|
||||
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
|
||||
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
Formatting = Formatting.None
|
||||
};
|
||||
|
||||
// Pre-serialize for deserialization benchmarks
|
||||
_smallAyCodeJson = AcJsonSerializer.Serialize(_smallOrder, _ayCodeWithRefs);
|
||||
_smallAyCodeNoRefJson = AcJsonSerializer.Serialize(_smallOrder, _ayCodeNoRefs);
|
||||
_smallStjJson = JsonSerializer.Serialize(_smallOrder, _stjWithRefs);
|
||||
_smallStjNoRefJson = JsonSerializer.Serialize(_smallOrder, _stjNoRefs);
|
||||
_smallNewtonsoftJson = JsonConvert.SerializeObject(_smallOrder, _newtonsoftSettings);
|
||||
|
||||
_mediumAyCodeJson = AcJsonSerializer.Serialize(_mediumOrder, _ayCodeWithRefs);
|
||||
_mediumAyCodeNoRefJson = AcJsonSerializer.Serialize(_mediumOrder, _ayCodeNoRefs);
|
||||
_mediumStjJson = JsonSerializer.Serialize(_mediumOrder, _stjWithRefs);
|
||||
_mediumStjNoRefJson = JsonSerializer.Serialize(_mediumOrder, _stjNoRefs);
|
||||
_mediumNewtonsoftJson = JsonConvert.SerializeObject(_mediumOrder, _newtonsoftSettings);
|
||||
|
||||
_largeAyCodeJson = AcJsonSerializer.Serialize(_largeOrder, _ayCodeWithRefs);
|
||||
_largeAyCodeNoRefJson = AcJsonSerializer.Serialize(_largeOrder, _ayCodeNoRefs);
|
||||
_largeStjJson = JsonSerializer.Serialize(_largeOrder, _stjWithRefs);
|
||||
_largeStjNoRefJson = JsonSerializer.Serialize(_largeOrder, _stjNoRefs);
|
||||
_largeNewtonsoftJson = JsonConvert.SerializeObject(_largeOrder, _newtonsoftSettings);
|
||||
|
||||
// Output sizes for comparison
|
||||
var newtonsoftBytes = Encoding.UTF8.GetByteCount(_newtonsoftJson);
|
||||
var ayCodeBytes = Encoding.UTF8.GetByteCount(_ayCodeJson);
|
||||
|
||||
Console.WriteLine("=== JSON Size Comparison ===");
|
||||
Console.WriteLine($"Newtonsoft (skip defaults): {_newtonsoftJson.Length:N0} chars ({newtonsoftBytes:N0} bytes)");
|
||||
Console.WriteLine($"AyCode (refs+skip): {_ayCodeJson.Length:N0} chars ({ayCodeBytes:N0} bytes)");
|
||||
Console.WriteLine($"Size reduction: {(1.0 - (double)ayCodeBytes / newtonsoftBytes) * 100:F1}%");
|
||||
Console.WriteLine($"Small: AyCode(refs)={_smallAyCodeJson.Length:N0}, AyCode(noRef)={_smallAyCodeNoRefJson.Length:N0}, STJ(refs)={_smallStjJson.Length:N0}, STJ(noRef)={_smallStjNoRefJson.Length:N0}");
|
||||
Console.WriteLine($"Medium: AyCode(refs)={_mediumAyCodeJson.Length:N0}, AyCode(noRef)={_mediumAyCodeNoRefJson.Length:N0}, STJ(refs)={_mediumStjJson.Length:N0}, STJ(noRef)={_mediumStjNoRefJson.Length:N0}");
|
||||
Console.WriteLine($"Large: AyCode(refs)={_largeAyCodeJson.Length:N0}, AyCode(noRef)={_largeAyCodeNoRefJson.Length:N0}, STJ(refs)={_largeStjJson.Length:N0}, STJ(noRef)={_largeStjNoRefJson.Length:N0}");
|
||||
}
|
||||
|
||||
#region Serialization Benchmarks
|
||||
#region Serialize Large - With Refs
|
||||
|
||||
[Benchmark(Description = "Newtonsoft (no refs)")]
|
||||
[BenchmarkCategory("Serialize")]
|
||||
public string Serialize_Newtonsoft_NoRefs()
|
||||
=> JsonConvert.SerializeObject(_testOrder, _newtonsoftNoRefSettings);
|
||||
[Benchmark(Description = "AyCode Serialize")]
|
||||
[BenchmarkCategory("Serialize-Large-WithRefs")]
|
||||
public string Serialize_Large_AyCode_WithRefs()
|
||||
=> AcJsonSerializer.Serialize(_largeOrder, _ayCodeWithRefs);
|
||||
|
||||
[Benchmark(Description = "AyCode (with refs)")]
|
||||
[BenchmarkCategory("Serialize")]
|
||||
public string Serialize_AyCode_WithRefs()
|
||||
=> _testOrder.ToJson();
|
||||
|
||||
[Benchmark(Description = "AcJsonSerializer (custom)")]
|
||||
[BenchmarkCategory("Serialize")]
|
||||
public string Serialize_AcJsonSerializer()
|
||||
=> AcJsonSerializer.Serialize(_testOrder);
|
||||
[Benchmark(Description = "STJ Serialize", Baseline = true)]
|
||||
[BenchmarkCategory("Serialize-Large-WithRefs")]
|
||||
public string Serialize_Large_STJ_WithRefs()
|
||||
=> JsonSerializer.Serialize(_largeOrder, _stjWithRefs);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Deserialization Benchmarks
|
||||
#region Serialize Large - No Refs
|
||||
|
||||
[Benchmark(Description = "Newtonsoft (no refs)")]
|
||||
[BenchmarkCategory("Deserialize")]
|
||||
public TestOrder? Deserialize_Newtonsoft_NoRefs()
|
||||
=> JsonConvert.DeserializeObject<TestOrder>(_newtonsoftJson, _newtonsoftNoRefSettings);
|
||||
[Benchmark(Description = "AyCode Serialize")]
|
||||
[BenchmarkCategory("Serialize-Large-NoRefs")]
|
||||
public string Serialize_Large_AyCode_NoRefs()
|
||||
=> AcJsonSerializer.Serialize(_largeOrder, _ayCodeNoRefs);
|
||||
|
||||
[Benchmark(Description = "AyCode (with refs)")]
|
||||
[BenchmarkCategory("Deserialize")]
|
||||
public TestOrder? Deserialize_AyCode_WithRefs()
|
||||
=> _ayCodeJson.JsonTo<TestOrder>();
|
||||
|
||||
[Benchmark(Description = "AcJsonDeserializer (custom)")]
|
||||
[BenchmarkCategory("Deserialize")]
|
||||
public TestOrder? Deserialize_AcJsonDeserializer()
|
||||
=> AcJsonDeserializer.Deserialize<TestOrder>(_ayCodeJson);
|
||||
[Benchmark(Description = "STJ Serialize", Baseline = true)]
|
||||
[BenchmarkCategory("Serialize-Large-NoRefs")]
|
||||
public string Serialize_Large_STJ_NoRefs()
|
||||
=> JsonSerializer.Serialize(_largeOrder, _stjNoRefs);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Populate Benchmarks
|
||||
#region Serialize Medium - With Refs
|
||||
|
||||
[Benchmark(Description = "AcJsonDeserializer.Populate")]
|
||||
[BenchmarkCategory("Populate")]
|
||||
public void Populate_AcJsonDeserializer()
|
||||
[Benchmark(Description = "AyCode Serialize")]
|
||||
[BenchmarkCategory("Serialize-Medium-WithRefs")]
|
||||
public string Serialize_Medium_AyCode_WithRefs()
|
||||
=> AcJsonSerializer.Serialize(_mediumOrder, _ayCodeWithRefs);
|
||||
|
||||
[Benchmark(Description = "STJ Serialize", Baseline = true)]
|
||||
[BenchmarkCategory("Serialize-Medium-WithRefs")]
|
||||
public string Serialize_Medium_STJ_WithRefs()
|
||||
=> JsonSerializer.Serialize(_mediumOrder, _stjWithRefs);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Serialize Medium - No Refs
|
||||
|
||||
[Benchmark(Description = "AyCode Serialize")]
|
||||
[BenchmarkCategory("Serialize-Medium-NoRefs")]
|
||||
public string Serialize_Medium_AyCode_NoRefs()
|
||||
=> AcJsonSerializer.Serialize(_mediumOrder, _ayCodeNoRefs);
|
||||
|
||||
[Benchmark(Description = "STJ Serialize", Baseline = true)]
|
||||
[BenchmarkCategory("Serialize-Medium-NoRefs")]
|
||||
public string Serialize_Medium_STJ_NoRefs()
|
||||
=> JsonSerializer.Serialize(_mediumOrder, _stjNoRefs);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Small Data Deserialization - With Refs
|
||||
|
||||
[Benchmark(Description = "AyCode WithRefs")]
|
||||
[BenchmarkCategory("Deserialize-Small-WithRefs")]
|
||||
public TestOrder? Deserialize_Small_AyCode_WithRefs()
|
||||
=> AcJsonDeserializer.Deserialize<TestOrder>(_smallAyCodeJson, _ayCodeWithRefs);
|
||||
|
||||
[Benchmark(Description = "STJ WithRefs", Baseline = true)]
|
||||
[BenchmarkCategory("Deserialize-Small-WithRefs")]
|
||||
public TestOrder? Deserialize_Small_STJ_WithRefs()
|
||||
=> JsonSerializer.Deserialize<TestOrder>(_smallStjJson, _stjWithRefs);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Small Data Deserialization - No Refs
|
||||
|
||||
[Benchmark(Description = "AyCode NoRefs")]
|
||||
[BenchmarkCategory("Deserialize-Small-NoRefs")]
|
||||
public TestOrder? Deserialize_Small_AyCode_NoRefs()
|
||||
=> AcJsonDeserializer.Deserialize<TestOrder>(_smallAyCodeNoRefJson, _ayCodeNoRefs);
|
||||
|
||||
[Benchmark(Description = "STJ NoRefs", Baseline = true)]
|
||||
[BenchmarkCategory("Deserialize-Small-NoRefs")]
|
||||
public TestOrder? Deserialize_Small_STJ_NoRefs()
|
||||
=> JsonSerializer.Deserialize<TestOrder>(_smallStjNoRefJson, _stjNoRefs);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Medium Data Deserialization - With Refs
|
||||
|
||||
[Benchmark(Description = "AyCode WithRefs")]
|
||||
[BenchmarkCategory("Deserialize-Medium-WithRefs")]
|
||||
public TestOrder? Deserialize_Medium_AyCode_WithRefs()
|
||||
=> AcJsonDeserializer.Deserialize<TestOrder>(_mediumAyCodeJson, _ayCodeWithRefs);
|
||||
|
||||
[Benchmark(Description = "STJ WithRefs", Baseline = true)]
|
||||
[BenchmarkCategory("Deserialize-Medium-WithRefs")]
|
||||
public TestOrder? Deserialize_Medium_STJ_WithRefs()
|
||||
=> JsonSerializer.Deserialize<TestOrder>(_mediumStjJson, _stjWithRefs);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Medium Data Deserialization - No Refs
|
||||
|
||||
[Benchmark(Description = "AyCode NoRefs")]
|
||||
[BenchmarkCategory("Deserialize-Medium-NoRefs")]
|
||||
public TestOrder? Deserialize_Medium_AyCode_NoRefs()
|
||||
=> AcJsonDeserializer.Deserialize<TestOrder>(_mediumAyCodeNoRefJson, _ayCodeNoRefs);
|
||||
|
||||
[Benchmark(Description = "STJ NoRefs", Baseline = true)]
|
||||
[BenchmarkCategory("Deserialize-Medium-NoRefs")]
|
||||
public TestOrder? Deserialize_Medium_STJ_NoRefs()
|
||||
=> JsonSerializer.Deserialize<TestOrder>(_mediumStjNoRefJson, _stjNoRefs);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Large Data Deserialization - With Refs
|
||||
|
||||
[Benchmark(Description = "AyCode WithRefs")]
|
||||
[BenchmarkCategory("Deserialize-Large-WithRefs")]
|
||||
public TestOrder? Deserialize_Large_AyCode_WithRefs()
|
||||
=> AcJsonDeserializer.Deserialize<TestOrder>(_largeAyCodeJson, _ayCodeWithRefs);
|
||||
|
||||
[Benchmark(Description = "STJ WithRefs", Baseline = true)]
|
||||
[BenchmarkCategory("Deserialize-Large-WithRefs")]
|
||||
public TestOrder? Deserialize_Large_STJ_WithRefs()
|
||||
=> JsonSerializer.Deserialize<TestOrder>(_largeStjJson, _stjWithRefs);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Large Data Deserialization - No Refs
|
||||
|
||||
[Benchmark(Description = "AyCode NoRefs")]
|
||||
[BenchmarkCategory("Deserialize-Large-NoRefs")]
|
||||
public TestOrder? Deserialize_Large_AyCode_NoRefs()
|
||||
=> AcJsonDeserializer.Deserialize<TestOrder>(_largeAyCodeNoRefJson, _ayCodeNoRefs);
|
||||
|
||||
[Benchmark(Description = "STJ NoRefs", Baseline = true)]
|
||||
[BenchmarkCategory("Deserialize-Large-NoRefs")]
|
||||
public TestOrder? Deserialize_Large_STJ_NoRefs()
|
||||
=> JsonSerializer.Deserialize<TestOrder>(_largeStjNoRefJson, _stjNoRefs);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Populate Benchmarks - Small
|
||||
|
||||
[Benchmark(Description = "AyCode Populate")]
|
||||
[BenchmarkCategory("Populate-Small")]
|
||||
public void Populate_Small_AyCode()
|
||||
{
|
||||
// Create fresh target for each iteration to avoid state pollution
|
||||
var target = new TestOrder();
|
||||
AcJsonDeserializer.Populate(_ayCodeJson, target);
|
||||
AcJsonDeserializer.Populate(_smallAyCodeJson, target);
|
||||
}
|
||||
|
||||
[Benchmark(Description = "Newtonsoft PopulateObject")]
|
||||
[BenchmarkCategory("Populate")]
|
||||
public void Populate_Newtonsoft()
|
||||
[Benchmark(Description = "Newtonsoft PopulateObject", Baseline = true)]
|
||||
[BenchmarkCategory("Populate-Small")]
|
||||
public void Populate_Small_Newtonsoft()
|
||||
{
|
||||
// Create fresh target for each iteration to match the other benchmark
|
||||
var target = new TestOrder();
|
||||
JsonConvert.PopulateObject(_newtonsoftJson, target, _newtonsoftNoRefSettings);
|
||||
JsonConvert.PopulateObject(_smallNewtonsoftJson, target, _newtonsoftSettings);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region JSON Size Comparison
|
||||
#region Populate Benchmarks - Medium
|
||||
|
||||
[Benchmark(Description = "JSON Size - Newtonsoft")]
|
||||
[BenchmarkCategory("Size")]
|
||||
public int JsonSize_Newtonsoft() => _newtonsoftJson.Length;
|
||||
[Benchmark(Description = "AyCode Populate")]
|
||||
[BenchmarkCategory("Populate-Medium")]
|
||||
public void Populate_Medium_AyCode()
|
||||
{
|
||||
var target = new TestOrder();
|
||||
AcJsonDeserializer.Populate(_mediumAyCodeJson, target);
|
||||
}
|
||||
|
||||
[Benchmark(Description = "JSON Size - AyCode")]
|
||||
[BenchmarkCategory("Size")]
|
||||
public int JsonSize_AyCode() => _ayCodeJson.Length;
|
||||
[Benchmark(Description = "Newtonsoft PopulateObject", Baseline = true)]
|
||||
[BenchmarkCategory("Populate-Medium")]
|
||||
public void Populate_Medium_Newtonsoft()
|
||||
{
|
||||
var target = new TestOrder();
|
||||
JsonConvert.PopulateObject(_mediumNewtonsoftJson, target, _newtonsoftSettings);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Populate Benchmarks - Large
|
||||
|
||||
[Benchmark(Description = "AyCode Populate")]
|
||||
[BenchmarkCategory("Populate-Large")]
|
||||
public void Populate_Large_AyCode()
|
||||
{
|
||||
var target = new TestOrder();
|
||||
AcJsonDeserializer.Populate(_largeAyCodeJson, target);
|
||||
}
|
||||
|
||||
[Benchmark(Description = "Newtonsoft PopulateObject", Baseline = true)]
|
||||
[BenchmarkCategory("Populate-Large")]
|
||||
public void Populate_Large_Newtonsoft()
|
||||
{
|
||||
var target = new TestOrder();
|
||||
JsonConvert.PopulateObject(_largeNewtonsoftJson, target, _newtonsoftSettings);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Loading…
Reference in New Issue