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:
Loretta 2025-12-12 16:23:54 +01:00
parent ad426feba4
commit a945db9b09
5 changed files with 1742 additions and 683 deletions

View File

@ -1,9 +1,12 @@
using System.Buffers;
using System.Collections; using System.Collections;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.Globalization; using System.Globalization;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json; using System.Text.Json;
using AyCode.Core.Helpers; using AyCode.Core.Helpers;
using AyCode.Core.Interfaces; using AyCode.Core.Interfaces;
@ -30,23 +33,28 @@ public class AcJsonDeserializationException : Exception
/// <summary> /// <summary>
/// High-performance custom JSON deserializer optimized for IId&lt;T&gt; reference handling. /// High-performance custom JSON deserializer optimized for IId&lt;T&gt; reference handling.
/// Supports MaxDepth and UseReferenceHandling options. /// Uses Utf8JsonReader for streaming deserialization when possible (STJ approach).
/// </summary> /// </summary>
public static class AcJsonDeserializer public static class AcJsonDeserializer
{ {
private static readonly ConcurrentDictionary<Type, DeserializeTypeMetadata> TypeMetadataCache = new(); 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 #region Public API
/// <summary> /// <summary>
/// Deserialize JSON string to a new object of type T with default options. /// Deserialize JSON string to a new object of type T with default options.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T? Deserialize<T>(string json) => Deserialize<T>(json, AcJsonSerializerOptions.Default); public static T? Deserialize<T>(string json) => Deserialize<T>(json, AcJsonSerializerOptions.Default);
/// <summary> /// <summary>
/// Deserialize JSON string to a new object of type T with specified options. /// Deserialize JSON string to a new object of type T with specified options.
/// </summary> /// </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; if (string.IsNullOrEmpty(json) || json == "null") return default;
@ -54,18 +62,30 @@ public static class AcJsonDeserializer
try try
{ {
ValidateJson(json, targetType); if (TryDeserializePrimitiveFast(json, targetType, out var primitiveResult))
if (TryDeserializePrimitive(json, targetType, out var primitiveResult))
return (T?)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); using var doc = JsonDocument.Parse(json);
var context = DeserializationContextPool.Get(options);
var result = ReadValue(doc.RootElement, targetType, context, 0); try
context.ResolveReferences(); {
var result = ReadValue(doc.RootElement, targetType, context, 0);
return (T?)result; context.ResolveReferences();
return (T?)result;
}
finally
{
DeserializationContextPool.Return(context);
}
} }
catch (AcJsonDeserializationException) { throw; } catch (AcJsonDeserializationException) { throw; }
catch (System.Text.Json.JsonException ex) catch (System.Text.Json.JsonException ex)
@ -81,37 +101,48 @@ public static class AcJsonDeserializer
/// <summary> /// <summary>
/// Deserialize JSON string to specified type with default options. /// Deserialize JSON string to specified type with default options.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static object? Deserialize(string json, Type targetType) => Deserialize(json, targetType, AcJsonSerializerOptions.Default); public static object? Deserialize(string json, Type targetType) => Deserialize(json, targetType, AcJsonSerializerOptions.Default);
/// <summary> /// <summary>
/// Deserialize JSON string to specified type with specified options. /// Deserialize JSON string to specified type with specified options.
/// </summary> /// </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; if (string.IsNullOrEmpty(json) || json == "null") return null;
try try
{ {
ValidateJson(json, targetType); var firstChar = json[0];
var isArrayJson = firstChar == '[';
var isObjectJson = firstChar == '{';
var isArrayJson = json.Length > 0 && json[0] == '['; if (!isArrayJson && !isObjectJson)
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 (TryDeserializePrimitive(json, targetType, out var primitiveResult)) if (TryDeserializePrimitiveFast(json, targetType, out var primitiveResult))
return 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); using var doc = JsonDocument.Parse(json);
var context = DeserializationContextPool.Get(options);
var result = ReadValue(doc.RootElement, targetType, context, 0); try
context.ResolveReferences(); {
var result = ReadValue(doc.RootElement, targetType, context, 0);
return result; context.ResolveReferences();
return result;
}
finally
{
DeserializationContextPool.Return(context);
}
} }
catch (AcJsonDeserializationException) { throw; } catch (AcJsonDeserializationException) { throw; }
catch (System.Text.Json.JsonException ex) catch (System.Text.Json.JsonException ex)
@ -127,74 +158,115 @@ public static class AcJsonDeserializer
/// <summary> /// <summary>
/// Populate existing object with JSON data (merge mode) with default options. /// Populate existing object with JSON data (merge mode) with default options.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Populate<T>(string json, T target) where T : class public static void Populate<T>(string json, T target) where T : class
=> Populate(json, target, AcJsonSerializerOptions.Default); => Populate(json, target, AcJsonSerializerOptions.Default);
/// <summary> /// <summary>
/// Populate existing object with JSON data (merge mode) with specified options. /// Populate existing object with JSON data (merge mode) with specified options.
/// </summary> /// </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); ArgumentNullException.ThrowIfNull(target);
if (string.IsNullOrEmpty(json) || json == "null") return; if (string.IsNullOrEmpty(json) || json == "null") return;
Populate(json, (object)target, target.GetType(), options); PopulateInternal(json, target, target.GetType(), options);
} }
/// <summary> /// <summary>
/// Populate existing object with JSON data with default options. /// Populate existing object with JSON data with default options.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Populate(string json, object target) public static void Populate(string json, object target)
=> Populate(json, target, target.GetType(), AcJsonSerializerOptions.Default); => Populate(json, target, target.GetType(), AcJsonSerializerOptions.Default);
/// <summary> /// <summary>
/// Populate existing object with JSON data with specified options. /// Populate existing object with JSON data with specified options.
/// </summary> /// </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); => Populate(json, target, target.GetType(), options);
/// <summary> /// <summary>
/// Populate existing object with JSON data with default options. /// Populate existing object with JSON data with default options.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Populate(string json, object target, Type targetType) public static void Populate(string json, object target, Type targetType)
=> Populate(json, target, targetType, AcJsonSerializerOptions.Default); => Populate(json, target, targetType, AcJsonSerializerOptions.Default);
/// <summary> /// <summary>
/// Populate existing object with JSON data with specified options. /// Populate existing object with JSON data with specified options.
/// </summary> /// </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); ArgumentNullException.ThrowIfNull(target);
if (string.IsNullOrEmpty(json) || json == "null") return; 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 try
{ {
ValidateJson(json, targetType); ValidateJson(json, targetType);
var context = new DeserializationContext(options) { IsMergeMode = true }; // Fast path for no reference handling - use Utf8JsonReader streaming
using var doc = JsonDocument.Parse(json); if (!options.UseReferenceHandling)
{
var firstChar = json[0];
if (firstChar == '[')
{
if (target is IList targetList)
PopulateListWithUtf8Reader(json, targetList, targetType, options.MaxDepth);
else
throw new AcJsonDeserializationException($"Cannot populate non-list target '{targetType.Name}' with JSON array", json, targetType);
return;
}
if (firstChar == '{')
{
var metadata = GetTypeMetadata(targetType);
PopulateObjectWithUtf8Reader(json, target, metadata, options.MaxDepth);
return;
}
throw new AcJsonDeserializationException($"Cannot populate object with JSON starting with '{firstChar}'", json, targetType);
}
// Reference handling requires DOM for forward references
using var doc = JsonDocument.Parse(json);
var rootElement = doc.RootElement; var rootElement = doc.RootElement;
if (rootElement.ValueKind == JsonValueKind.Array) var context = DeserializationContextPool.Get(options);
context.IsMergeMode = true;
try
{ {
if (target is IList targetList) if (rootElement.ValueKind == JsonValueKind.Array)
PopulateList(rootElement, targetList, targetType, context, 0); {
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 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(); context.ResolveReferences();
return;
} }
finally
if (rootElement.ValueKind == JsonValueKind.Object)
{ {
var metadata = GetTypeMetadata(targetType); DeserializationContextPool.Return(context);
PopulateObjectInternalMerge(rootElement, target, metadata, context, 0);
} }
else
throw new AcJsonDeserializationException($"Cannot populate object with JSON value of kind '{rootElement.ValueKind}'", json, targetType);
context.ResolveReferences();
} }
catch (AcJsonDeserializationException) { throw; } catch (AcJsonDeserializationException) { throw; }
catch (System.Text.Json.JsonException ex) catch (System.Text.Json.JsonException ex)
@ -209,71 +281,202 @@ public static class AcJsonDeserializer
#endregion #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; var (buffer, length) = GetUtf8Bytes(json);
try
if (json.Length > 2 && json[0] == '"' && json[^1] == '"')
{ {
var inner = json[1..^1]; var reader = new Utf8JsonReader(buffer.AsSpan(0, length), new JsonReaderOptions { MaxDepth = maxDepth });
if (inner.Contains("\\\"") && (inner.Contains("{") || inner.Contains("["))) if (!reader.Read()) return default;
throw new AcJsonDeserializationException( return (T?)ReadValueFromReader(ref reader, typeof(T), maxDepth, 0);
$"Detected double-serialized JSON string. Target type: {targetType.Name}.", }
json, targetType); 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)] [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), var reader = new Utf8JsonReader(buffer.AsSpan(0, length), new JsonReaderOptions { MaxDepth = maxDepth });
JsonValueKind.Array => ReadArray(element, targetType, context, depth), if (!reader.Read()) return null;
JsonValueKind.Null => null, return ReadValueFromReader(ref reader, targetType, maxDepth, 0);
_ => ReadPrimitive(element, targetType, element.ValueKind) }
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)] [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) var type = Nullable.GetUnderlyingType(targetType) ?? targetType;
if (context.UseReferenceHandling && element.TryGetProperty("$ref", out var refElement))
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()!; var str = reader.GetString();
return context.TryGetReferencedObject(refId, out var refObj) ? refObj : new DeferredReference(refId, targetType); 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)) 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); var metadata = GetTypeMetadata(targetType);
object? instance; var instance = metadata.CompiledConstructor?.Invoke();
if (metadata.CompiledConstructor != null) if (instance == null)
instance = metadata.CompiledConstructor.Invoke();
else
{ {
try { instance = Activator.CreateInstance(targetType); } try { instance = Activator.CreateInstance(targetType); }
catch (MissingMethodException ex) catch (MissingMethodException ex)
@ -286,172 +489,310 @@ public static class AcJsonDeserializer
if (instance == null) return null; if (instance == null) return null;
if (context.UseReferenceHandling && element.TryGetProperty("$id", out var idElement)) var propsDict = metadata.PropertySettersFrozen;
context.RegisterObject(idElement.GetString()!, instance); var nextDepth = depth + 1;
PopulateObjectInternal(element, instance, metadata, context, depth); 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; 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; reader.Skip();
return null;
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);
} }
}
private static void PopulateObjectInternalMerge(JsonElement element, object target, DeserializeTypeMetadata metadata, DeserializationContext context, int depth) var instance = metadata.CompiledConstructor?.Invoke();
{ if (instance == null)
foreach (var jsonProp in element.EnumerateObject())
{ {
var propName = jsonProp.Name; try { instance = Activator.CreateInstance(targetType); }
catch (MissingMethodException ex)
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)
{ {
// At max depth, only set primitives throw new AcJsonDeserializationException(
if (propValueKind != JsonValueKind.Object && propValueKind != JsonValueKind.Array) $"Cannot deserialize type '{targetType.FullName}' because it does not have a parameterless constructor.",
{ null, targetType, ex);
var primitiveValue = ReadPrimitive(propValue, propInfo.PropertyType, propValueKind);
propInfo.SetValue(target, primitiveValue);
}
continue;
} }
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 (instance == null) return null;
if (elementType == null) return;
var acObservable = targetList as IAcObservableCollection;
acObservable?.BeginUpdate();
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); reader.Skip();
if (value != null) continue;
targetList.Add(value);
} }
// 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? ReadArrayFromReader(ref Utf8JsonReader reader, Type targetType, byte maxDepth, int depth)
private static object? ReadArray(JsonElement element, Type targetType, DeserializationContext context, int depth)
{ {
// Check depth limit if (depth > maxDepth)
if (depth > context.MaxDepth) return null; {
reader.Skip();
return null;
}
var elementType = GetCollectionElementType(targetType); var elementType = GetCollectionElementType(targetType);
if (elementType == null) return null; 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) 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); var array = Array.CreateInstance(elementType, list.Count);
list.CopyTo(array, 0); list.CopyTo(array, 0);
return array; return array;
} }
IList? targetList = null; return list;
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;
} }
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 elementType = propInfo.ElementType!;
var idGetter = propInfo.ElementIdGetter!; 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()) foreach (var jsonItem in arrayElement.EnumerateArray())
{ {
if (jsonItem.ValueKind != JsonValueKind.Object) continue; if (jsonItem.ValueKind != JsonValueKind.Object) continue;
@ -494,38 +1193,70 @@ public static class AcJsonDeserializer
if (existingById.TryGetValue(itemId, out var existingItem)) if (existingById.TryGetValue(itemId, out var existingItem))
{ {
var itemMetadata = GetTypeMetadata(elementType); var itemMetadata = GetTypeMetadata(elementType);
PopulateObjectInternalMerge(jsonItem, existingItem, itemMetadata, context, depth + 1); PopulateObjectInternalMerge(jsonItem, existingItem, itemMetadata, context, nextDepth);
continue; continue;
} }
} }
var newItem = ReadValue(jsonItem, elementType, context, depth + 1); var newItem = ReadValue(jsonItem, elementType, context, nextDepth);
if (newItem != null) existingList.Add(newItem); if (newItem != null) existingList.Add(newItem);
} }
} }
finally { acObservable?.EndUpdate(); } 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)] [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; var type = Nullable.GetUnderlyingType(targetType) ?? targetType;
if (valueKind == JsonValueKind.Number) if (valueKind == JsonValueKind.Number)
{ {
if (ReferenceEquals(type, IntType)) return element.GetInt32(); var typeCode = Type.GetTypeCode(type);
if (ReferenceEquals(type, LongType)) return element.GetInt64(); return typeCode switch
if (ReferenceEquals(type, DoubleType)) return element.GetDouble(); {
if (ReferenceEquals(type, DecimalType)) return element.GetDecimal(); TypeCode.Int32 => element.GetInt32(),
if (ReferenceEquals(type, FloatType)) return element.GetSingle(); TypeCode.Int64 => element.GetInt64(),
if (type == ByteType) return element.GetByte(); TypeCode.Double => element.GetDouble(),
if (type == ShortType) return element.GetInt16(); TypeCode.Decimal => element.GetDecimal(),
if (type == UShortType) return element.GetUInt16(); TypeCode.Single => element.GetSingle(),
if (type == UIntType) return element.GetUInt32(); TypeCode.Byte => element.GetByte(),
if (type == ULongType) return element.GetUInt64(); TypeCode.Int16 => element.GetInt16(),
if (type == SByteType) return element.GetSByte(); TypeCode.UInt16 => element.GetUInt16(),
if (type.IsEnum) return Enum.ToObject(type, element.GetInt32()); TypeCode.UInt32 => element.GetUInt32(),
return null; TypeCode.UInt64 => element.GetUInt64(),
TypeCode.SByte => element.GetSByte(),
_ => type.IsEnum ? Enum.ToObject(type, element.GetInt32()) : null
};
} }
if (valueKind == JsonValueKind.String) if (valueKind == JsonValueKind.String)
@ -533,8 +1264,8 @@ public static class AcJsonDeserializer
if (ReferenceEquals(type, StringType)) return element.GetString(); if (ReferenceEquals(type, StringType)) return element.GetString();
if (ReferenceEquals(type, DateTimeType)) return element.GetDateTime(); if (ReferenceEquals(type, DateTimeType)) return element.GetDateTime();
if (ReferenceEquals(type, GuidType)) return element.GetGuid(); if (ReferenceEquals(type, GuidType)) return element.GetGuid();
if (type == DateTimeOffsetType) return element.GetDateTimeOffset(); if (ReferenceEquals(type, DateTimeOffsetType)) return element.GetDateTimeOffset();
if (type == TimeSpanType) return TimeSpan.Parse(element.GetString()!, CultureInfo.InvariantCulture); if (ReferenceEquals(type, TimeSpanType)) return TimeSpan.Parse(element.GetString()!, CultureInfo.InvariantCulture);
if (type.IsEnum) return Enum.Parse(type, element.GetString()!); if (type.IsEnum) return Enum.Parse(type, element.GetString()!);
return null; return null;
} }
@ -545,56 +1276,62 @@ public static class AcJsonDeserializer
return null; 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 #endregion
#region Primitive Deserialization #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; var type = Nullable.GetUnderlyingType(targetType) ?? targetType;
if (ReferenceEquals(type, StringType)) // Handle enums first
if (type.IsEnum)
{ {
using var doc = JsonDocument.Parse(json); if (json.Length > 0 && json[0] == '"')
result = doc.RootElement.GetString(); {
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; return true;
} }
if (ReferenceEquals(type, IntType)) { result = int.Parse(json, CultureInfo.InvariantCulture); return true; } var typeCode = Type.GetTypeCode(type);
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; }
if (ReferenceEquals(type, DateTimeType)) switch (typeCode)
{ {
using var doc = JsonDocument.Parse(json); case TypeCode.Int32: result = int.Parse(json, CultureInfo.InvariantCulture); return true;
result = doc.RootElement.GetDateTime(); case TypeCode.Int64: result = long.Parse(json, CultureInfo.InvariantCulture); return true;
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)) if (ReferenceEquals(type, GuidType))
@ -604,32 +1341,17 @@ public static class AcJsonDeserializer
return true; return true;
} }
if (type == DateTimeOffsetType) { using var doc = JsonDocument.Parse(json); result = doc.RootElement.GetDateTimeOffset(); return true; } if (ReferenceEquals(type, DateTimeOffsetType))
if (type == TimeSpanType) { using var doc = JsonDocument.Parse(json); result = TimeSpan.Parse(doc.RootElement.GetString()!, CultureInfo.InvariantCulture); return true; }
if (type.IsEnum)
{ {
if (json.StartsWith('"')) using var doc = JsonDocument.Parse(json);
{ result = doc.RootElement.GetDateTimeOffset();
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; return true;
} }
if (type == ByteType) { result = byte.Parse(json, CultureInfo.InvariantCulture); return true; } if (ReferenceEquals(type, TimeSpanType))
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)
{ {
using var doc = JsonDocument.Parse(json); using var doc = JsonDocument.Parse(json);
var s = doc.RootElement.GetString(); result = TimeSpan.Parse(doc.RootElement.GetString()!, CultureInfo.InvariantCulture);
result = s?.Length > 0 ? s[0] : '\0';
return true; return true;
} }
@ -639,15 +1361,46 @@ public static class AcJsonDeserializer
#endregion #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 #region Type Metadata
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static DeserializeTypeMetadata GetTypeMetadata(Type type) private static DeserializeTypeMetadata GetTypeMetadata(in Type type)
=> TypeMetadataCache.GetOrAdd(type, static t => new DeserializeTypeMetadata(t)); => TypeMetadataCache.GetOrAdd(type, static t => new DeserializeTypeMetadata(t));
private sealed class DeserializeTypeMetadata private sealed class DeserializeTypeMetadata
{ {
public Dictionary<string, PropertySetterInfo> PropertySetters { get; } public FrozenDictionary<string, PropertySetterInfo> PropertySettersFrozen { get; }
public Func<object>? CompiledConstructor { get; } public Func<object>? CompiledConstructor { get; }
public DeserializeTypeMetadata(Type type) public DeserializeTypeMetadata(Type type)
@ -670,20 +1423,28 @@ public static class AcJsonDeserializer
propsList.Add(p); 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) 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 private sealed class PropertySetterInfo
{ {
public Type PropertyType { get; } public readonly Type PropertyType;
public bool IsCollection { get; } public readonly Type UnderlyingType; // Cached: Nullable.GetUnderlyingType(PropertyType) ?? PropertyType
public bool ElementIsIId { get; } public readonly TypeCode PropertyTypeCode; // Cached TypeCode for fast primitive reading
public Type? ElementType { get; } public readonly bool IsNullable;
public Type? ElementIdType { get; } public readonly bool IsIIdCollection;
public Func<object, object?>? ElementIdGetter { get; } public readonly Type? ElementType;
public readonly Type? ElementIdType;
public readonly Func<object, object?>? ElementIdGetter;
private readonly Action<object, object?> _setter; private readonly Action<object, object?> _setter;
private readonly Func<object, object?> _getter; private readonly Func<object, object?> _getter;
@ -691,21 +1452,26 @@ public static class AcJsonDeserializer
public PropertySetterInfo(PropertyInfo prop, Type declaringType) public PropertySetterInfo(PropertyInfo prop, Type declaringType)
{ {
PropertyType = prop.PropertyType; PropertyType = prop.PropertyType;
var underlying = Nullable.GetUnderlyingType(PropertyType);
IsNullable = underlying != null;
UnderlyingType = underlying ?? PropertyType;
PropertyTypeCode = Type.GetTypeCode(UnderlyingType);
_setter = CreateCompiledSetter(declaringType, prop); _setter = CreateCompiledSetter(declaringType, prop);
_getter = CreateCompiledGetter(declaringType, prop); _getter = CreateCompiledGetter(declaringType, prop);
ElementType = GetCollectionElementType(PropertyType); ElementType = GetCollectionElementType(PropertyType);
IsCollection = ElementType != null && ElementType != typeof(object) && var isCollection = ElementType != null && ElementType != typeof(object) &&
typeof(IEnumerable).IsAssignableFrom(PropertyType) && typeof(IEnumerable).IsAssignableFrom(PropertyType) &&
PropertyType != StringType; !ReferenceEquals(PropertyType, StringType);
if (IsCollection && ElementType != null) if (isCollection && ElementType != null)
{ {
var (isId, idType) = GetIdInfo(ElementType); var idInfo = GetIdInfo(ElementType);
if (isId) if (idInfo.IsId)
{ {
ElementIsIId = true; IsIIdCollection = true;
ElementIdType = idType; ElementIdType = idInfo.IdType;
var idProp = ElementType.GetProperty("Id"); var idProp = ElementType.GetProperty("Id");
if (idProp != null) if (idProp != null)
ElementIdGetter = CreateCompiledGetter(ElementType, idProp); ElementIdGetter = CreateCompiledGetter(ElementType, idProp);
@ -762,16 +1528,26 @@ public static class AcJsonDeserializer
private Dictionary<string, object>? _idToObject; private Dictionary<string, object>? _idToObject;
private List<PropertyToResolve>? _propertiesToResolve; private List<PropertyToResolve>? _propertiesToResolve;
public bool IsMergeMode { get; init; } public bool IsMergeMode { get; set; }
public bool UseReferenceHandling { get; } public bool UseReferenceHandling { get; private set; }
public byte MaxDepth { get; } 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; UseReferenceHandling = options.UseReferenceHandling;
MaxDepth = options.MaxDepth; MaxDepth = options.MaxDepth;
IsMergeMode = false;
}
public void Clear()
{
_idToObject?.Clear();
_propertiesToResolve?.Clear();
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]

View File

@ -6,6 +6,7 @@ using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text; using System.Text;
using System.Text.Json;
using AyCode.Core.Interfaces; using AyCode.Core.Interfaces;
using Newtonsoft.Json; using Newtonsoft.Json;
using static AyCode.Core.Extensions.JsonUtilities; using static AyCode.Core.Extensions.JsonUtilities;
@ -14,92 +15,95 @@ namespace AyCode.Core.Extensions;
/// <summary> /// <summary>
/// High-performance custom JSON serializer optimized for IId&lt;T&gt; reference handling. /// High-performance custom JSON serializer optimized for IId&lt;T&gt; reference handling.
/// Features: /// Uses Utf8JsonWriter for high-performance UTF-8 output (STJ approach).
/// - 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
/// </summary> /// </summary>
public static class AcJsonSerializer public static class AcJsonSerializer
{ {
private static readonly ConcurrentDictionary<Type, TypeMetadata> TypeMetadataCache = new(); 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> /// <summary>
/// Serialize object to JSON string with default options. /// Serialize object to JSON string with default options.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Serialize<T>(T value) => Serialize(value, AcJsonSerializerOptions.Default); public static string Serialize<T>(T value) => Serialize(value, AcJsonSerializerOptions.Default);
/// <summary> /// <summary>
/// Serialize object to JSON string with specified options. /// Serialize object to JSON string with specified options.
/// </summary> /// </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"; 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; return primitiveJson;
var context = new SerializationContext(options); var context = SerializationContextPool.Get(options);
try
if (options.UseReferenceHandling) {
ScanReferences(value, context, 0); if (options.UseReferenceHandling)
ScanReferences(value, context, 0);
context.StartWriting();
WriteValue(value, context, 0); WriteValue(value, context, 0);
return context.GetResult();
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 underlyingType = Nullable.GetUnderlyingType(type) ?? type;
var typeCode = Type.GetTypeCode(underlyingType);
if (underlyingType == StringType) { json = SerializeString((string)(object)value!); return true; } switch (typeCode)
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)
{ {
var d = (double)(object)value!; case TypeCode.String: json = SerializeString((string)value); return true;
json = double.IsNaN(d) || double.IsInfinity(d) ? "null" : d.ToString("G17", CultureInfo.InvariantCulture); case TypeCode.Int32: json = ((int)value).ToString(CultureInfo.InvariantCulture); return true;
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 (ReferenceEquals(underlyingType, GuidType)) { json = $"\"{((Guid)value).ToString("D")}\""; return true; }
if (ReferenceEquals(underlyingType, DateTimeOffsetType)) { json = $"\"{((DateTimeOffset)value).ToString("O", CultureInfo.InvariantCulture)}\""; return true; }
if (underlyingType == FloatType) if (ReferenceEquals(underlyingType, TimeSpanType)) { json = $"\"{((TimeSpan)value).ToString("c", CultureInfo.InvariantCulture)}\""; return true; }
{
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 (underlyingType.IsEnum) { json = Convert.ToInt32(value).ToString(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 = ""; json = "";
return false; return false;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static string SerializeString(string value) 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('"'); sb.Append('"');
WriteEscapedString(sb, value); WriteEscapedString(sb, value);
sb.Append('"'); sb.Append('"');
@ -110,16 +114,13 @@ public static class AcJsonSerializer
private static void ScanReferences(object? value, SerializationContext context, int depth) private static void ScanReferences(object? value, SerializationContext context, int depth)
{ {
if (value == null) return; if (value == null || depth > context.MaxDepth) return;
if (depth > context.MaxDepth) return;
var type = value.GetType(); var type = value.GetType();
if (IsPrimitiveOrStringFast(type)) return; if (IsPrimitiveOrStringFast(type)) return;
if (!context.TrackForScanning(value)) 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) foreach (var item in enumerable)
if (item != null) ScanReferences(item, context, depth + 1); if (item != null) ScanReferences(item, context, depth + 1);
@ -127,9 +128,11 @@ public static class AcJsonSerializer
} }
var metadata = GetTypeMetadata(type); 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); 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) 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(); 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.Writer.WriteNullValue(); return; }
if (depth > context.MaxDepth)
{
context.WriteNull();
return;
}
if (value is IDictionary dictionary) { WriteDictionary(dictionary, context, depth); 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); 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)) if (context.UseReferenceHandling && context.TryGetExistingRef(value, out var refId))
{ {
context.WriteRef(refId); writer.WriteStartObject();
writer.WriteString(RefPropertyEncoded, refId);
writer.WriteEndObject();
return; return;
} }
context.WriteObjectStart(); writer.WriteStartObject();
var isFirst = true;
if (context.UseReferenceHandling && context.ShouldWriteId(value, out var id)) if (context.UseReferenceHandling && context.ShouldWriteId(value, out var id))
{ {
context.WritePropertyName("$id", ref isFirst); writer.WriteString(IdPropertyEncoded, id);
context.WriteString(id);
context.MarkAsWritten(value, id); context.MarkAsWritten(value, id);
} }
var metadata = GetTypeMetadata(type); 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); var propValue = prop.GetValue(value);
if (propValue == null) continue; if (propValue == null) continue;
if (IsSerializerDefaultValue(propValue, prop.PropertyType)) continue; if (IsDefaultValueFast(propValue, prop.PropertyTypeCode, prop.PropertyType)) continue;
context.WritePropertyName(prop.JsonName, ref isFirst); writer.WritePropertyName(prop.JsonNameEncoded);
WriteValue(propValue, context, depth + 1); WriteValue(propValue, context, nextDepth);
} }
context.WriteObjectEnd(); writer.WriteEndObject();
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteArray(IEnumerable enumerable, SerializationContext context, int depth) private static void WriteArray(IEnumerable enumerable, SerializationContext context, int depth)
{ {
context.WriteArrayStart(); var writer = context.Writer;
var isFirst = true; writer.WriteStartArray();
var nextDepth = depth + 1;
foreach (var item in enumerable) foreach (var item in enumerable)
{ {
if (!isFirst) context.WriteComma(); WriteValue(item, context, nextDepth);
isFirst = false;
WriteValue(item, context, depth + 1);
} }
context.WriteArrayEnd(); writer.WriteEndArray();
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteDictionary(IDictionary dictionary, SerializationContext context, int depth) private static void WriteDictionary(IDictionary dictionary, SerializationContext context, int depth)
{ {
context.WriteObjectStart(); var writer = context.Writer;
var isFirst = true; writer.WriteStartObject();
var nextDepth = depth + 1;
foreach (DictionaryEntry entry in dictionary) foreach (DictionaryEntry entry in dictionary)
{ {
context.WritePropertyName(entry.Key?.ToString() ?? "", ref isFirst); writer.WritePropertyName(entry.Key?.ToString() ?? "");
WriteValue(entry.Value, context, depth + 1); 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 underlyingType = Nullable.GetUnderlyingType(type) ?? type;
var typeCode = Type.GetTypeCode(underlyingType);
if (underlyingType == StringType) { context.WriteString((string)value); return true; } switch (typeCode)
if (underlyingType == IntType) { context.WriteInt((int)value); return true; } {
if (underlyingType == LongType) { context.WriteLong((long)value); return true; } case TypeCode.String: writer.WriteStringValue((string)value); return true;
if (underlyingType == BoolType) { context.WriteBool((bool)value); return true; } case TypeCode.Int32: writer.WriteNumberValue((int)value); return true;
if (underlyingType == DoubleType) { context.WriteDouble((double)value); return true; } case TypeCode.Int64: writer.WriteNumberValue((long)value); return true;
if (underlyingType == DecimalType) { context.WriteDecimal((decimal)value); return true; } case TypeCode.Boolean: writer.WriteBooleanValue((bool)value); return true;
if (underlyingType == FloatType) { context.WriteFloat((float)value); return true; } case TypeCode.Double:
if (underlyingType == DateTimeType) { context.WriteDateTime((DateTime)value); return true; } var d = (double)value;
if (underlyingType == DateTimeOffsetType) { context.WriteDateTimeOffset((DateTimeOffset)value); return true; } if (double.IsNaN(d) || double.IsInfinity(d)) writer.WriteNullValue();
if (underlyingType == GuidType) { context.WriteGuid((Guid)value); return true; } else writer.WriteNumberValue(d);
if (underlyingType == TimeSpanType) { context.WriteTimeSpan((TimeSpan)value); return true; } return true;
if (underlyingType.IsEnum) { context.WriteInt(Convert.ToInt32(value)); return true; } case TypeCode.Decimal: writer.WriteNumberValue((decimal)value); return true;
if (underlyingType == ByteType) { context.WriteInt((byte)value); return true; } case TypeCode.Single:
if (underlyingType == ShortType) { context.WriteInt((short)value); return true; } var f = (float)value;
if (underlyingType == UShortType) { context.WriteInt((ushort)value); return true; } if (float.IsNaN(f) || float.IsInfinity(f)) writer.WriteNullValue();
if (underlyingType == UIntType) { context.WriteLong((uint)value); return true; } else writer.WriteNumberValue(f);
if (underlyingType == ULongType) { context.WriteULong((ulong)value); return true; } return true;
if (underlyingType == SByteType) { context.WriteInt((sbyte)value); return true; } case TypeCode.DateTime: writer.WriteStringValue((DateTime)value); return true;
if (underlyingType == CharType) { context.WriteString(value.ToString()!); 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; return false;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [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 (propertyType.IsEnum) return Convert.ToInt32(value) == 0;
if (type == LongType) return (long)value == 0L; if (ReferenceEquals(propertyType, GuidType)) return (Guid)value == Guid.Empty;
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;
return false; return false;
} }
@ -275,7 +299,8 @@ public static class AcJsonSerializer
#region Type Metadata #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)); => TypeMetadataCache.GetOrAdd(type, static t => new TypeMetadata(t));
private sealed class TypeMetadata private sealed class TypeMetadata
@ -295,14 +320,18 @@ public static class AcJsonSerializer
private sealed class PropertyAccessor private sealed class PropertyAccessor
{ {
public string JsonName { get; } public readonly string JsonName;
public Type PropertyType { get; } public readonly JsonEncodedText JsonNameEncoded; // STJ optimization - pre-encoded property name
public readonly Type PropertyType;
public readonly TypeCode PropertyTypeCode;
private readonly Func<object, object?> _getter; private readonly Func<object, object?> _getter;
public PropertyAccessor(PropertyInfo prop) public PropertyAccessor(PropertyInfo prop)
{ {
JsonName = prop.Name; 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); _getter = CreateCompiledGetter(prop.DeclaringType!, prop);
} }
@ -321,35 +350,89 @@ public static class AcJsonSerializer
#endregion #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 #region Serialization Context
private sealed class SerializationContext private sealed class SerializationContext : IDisposable
{ {
private readonly StringBuilder _sb = new(4096); private readonly ArrayBufferWriter<byte> _buffer;
private readonly Dictionary<object, int>? _scanOccurrences; public Utf8JsonWriter Writer { get; private set; }
private readonly Dictionary<object, string>? _writtenRefs;
private readonly HashSet<object>? _multiReferenced;
private int _nextId = 1;
private static readonly ArrayPool<char> CharPool = ArrayPool<char>.Shared; private Dictionary<object, int>? _scanOccurrences;
private readonly char[] _numberBuffer = CharPool.Rent(64); private Dictionary<object, string>? _writtenRefs;
private HashSet<object>? _multiReferenced;
private int _nextId;
public bool UseReferenceHandling { get; } public bool UseReferenceHandling { get; private set; }
public byte MaxDepth { get; } 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; UseReferenceHandling = options.UseReferenceHandling;
MaxDepth = options.MaxDepth; MaxDepth = options.MaxDepth;
_nextId = 1;
if (UseReferenceHandling) if (UseReferenceHandling)
{ {
_scanOccurrences = new Dictionary<object, int>(64, ReferenceEqualityComparer.Instance); _scanOccurrences ??= new Dictionary<object, int>(64, ReferenceEqualityComparer.Instance);
_writtenRefs = new Dictionary<object, string>(32, ReferenceEqualityComparer.Instance); _writtenRefs ??= new Dictionary<object, string>(32, ReferenceEqualityComparer.Instance);
_multiReferenced = new HashSet<object>(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) public bool TrackForScanning(object obj)
{ {
if (_scanOccurrences == null) return true; if (_scanOccurrences == null) return true;
@ -360,8 +443,7 @@ public static class AcJsonSerializer
return true; return true;
} }
public void StartWriting() { } [MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool ShouldWriteId(object obj, out string id) public bool ShouldWriteId(object obj, out string id)
{ {
if (_multiReferenced != null && _multiReferenced.Contains(obj) && !_writtenRefs!.ContainsKey(obj)) if (_multiReferenced != null && _multiReferenced.Contains(obj) && !_writtenRefs!.ContainsKey(obj))
@ -373,122 +455,26 @@ public static class AcJsonSerializer
return false; return false;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void MarkAsWritten(object obj, string id) => _writtenRefs![obj] = id; public void MarkAsWritten(object obj, string id) => _writtenRefs![obj] = id;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetExistingRef(object obj, out string refId) public bool TryGetExistingRef(object obj, out string refId)
{ {
if (_writtenRefs != null) return _writtenRefs.TryGetValue(obj, out refId!); if (_writtenRefs != null) return _writtenRefs.TryGetValue(obj, out refId!);
refId = ""; refId = "";
return false; return false;
} }
public void WriteRef(string refId) { _sb.Append("{\"$ref\":\""); _sb.Append(refId); _sb.Append("\"}"); }
public string GetResult() { CharPool.Return(_numberBuffer); return _sb.ToString(); } public string GetResult()
[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)
{ {
if (!isFirst) _sb.Append(','); Writer.Flush();
isFirst = false; return Encoding.UTF8.GetString(_buffer.WrittenSpan);
_sb.Append('"'); _sb.Append(name); _sb.Append("\":");
} }
public void WriteString(string value) public void Dispose()
{ {
_sb.Append('"'); Writer.Dispose();
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('"');
} }
} }

View File

@ -1,3 +1,4 @@
using System.Buffers;
using System.Collections; using System.Collections;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Frozen; using System.Collections.Frozen;
@ -51,6 +52,27 @@ public sealed class AcJsonSerializerOptions
public static AcJsonSerializerOptions WithoutReferenceHandling() => new() { UseReferenceHandling = false }; 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> /// <summary>
/// Central utilities for JSON serialization/deserialization. /// Central utilities for JSON serialization/deserialization.
/// Contains shared type caches, primitive type checks, and string utilities. /// Contains shared type caches, primitive type checks, and string utilities.
@ -109,7 +131,7 @@ public static class JsonUtilities
#region Type Caches #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, Type?> CollectionElementCache = new();
private static readonly ConcurrentDictionary<Type, bool> IsPrimitiveCache = new(); private static readonly ConcurrentDictionary<Type, bool> IsPrimitiveCache = new();
private static readonly ConcurrentDictionary<Type, bool> IsCollectionCache = new(); private static readonly ConcurrentDictionary<Type, bool> IsCollectionCache = new();
@ -119,6 +141,37 @@ public static class JsonUtilities
#endregion #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 #region String Utilities
/// <summary> /// <summary>
@ -211,6 +264,20 @@ public static class JsonUtilities
return false; 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> /// <summary>
/// Escapes a string for JSON output. /// Escapes a string for JSON output.
/// </summary> /// </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 #endregion
@ -263,7 +358,7 @@ public static class JsonUtilities
/// Faster primitive check using TypeCode for hot paths. /// Faster primitive check using TypeCode for hot paths.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsPrimitiveOrStringFast(Type type) public static bool IsPrimitiveOrStringFast(in Type type)
{ {
var typeCode = Type.GetTypeCode(type); var typeCode = Type.GetTypeCode(type);
return typeCode switch return typeCode switch
@ -272,7 +367,7 @@ public static class JsonUtilities
TypeCode.Int16 or TypeCode.UInt16 or TypeCode.Int32 or TypeCode.UInt32 or 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.Int64 or TypeCode.UInt64 or TypeCode.Single or TypeCode.Double or
TypeCode.Decimal or TypeCode.DateTime or TypeCode.String => true, 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.) /// Checks if type is a generic collection type (List, IList, ObservableCollection, etc.)
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsGenericCollectionType(Type type) public static bool IsGenericCollectionType(in Type type)
{ {
return IsCollectionCache.GetOrAdd(type, static t => 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.IsArray) return true;
if (t.IsGenericType) if (t.IsGenericType)
{ {
var genericDef = t.GetGenericTypeDefinition(); var genericDef = t.GetGenericTypeDefinition();
if (genericDef == ListGenericType || if (ReferenceEquals(genericDef, ListGenericType) ||
genericDef == IListGenericType || ReferenceEquals(genericDef, IListGenericType) ||
genericDef == typeof(ICollection<>) || genericDef == typeof(ICollection<>) ||
genericDef == IEnumerableGenericType || ReferenceEquals(genericDef, IEnumerableGenericType) ||
genericDef == ObservableCollectionType || ReferenceEquals(genericDef, ObservableCollectionType) ||
genericDef == CollectionType) ReferenceEquals(genericDef, CollectionType))
return true; return true;
} }
foreach (var iface in t.GetInterfaces()) foreach (var iface in t.GetInterfaces())
{ {
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == IListGenericType) if (iface.IsGenericType && ReferenceEquals(iface.GetGenericTypeDefinition(), IListGenericType))
return true; return true;
} }
@ -313,7 +408,7 @@ public static class JsonUtilities
/// Checks if type is a dictionary type. /// Checks if type is a dictionary type.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [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; keyType = null;
valueType = null; valueType = null;
@ -321,7 +416,7 @@ public static class JsonUtilities
if (!type.IsGenericType) return false; if (!type.IsGenericType) return false;
var genericDef = type.GetGenericTypeDefinition(); var genericDef = type.GetGenericTypeDefinition();
if (genericDef == DictionaryGenericType || genericDef == IDictionaryGenericType) if (ReferenceEquals(genericDef, DictionaryGenericType) || ReferenceEquals(genericDef, IDictionaryGenericType))
{ {
var args = type.GetGenericArguments(); var args = type.GetGenericArguments();
keyType = args[0]; keyType = args[0];
@ -331,7 +426,7 @@ public static class JsonUtilities
foreach (var iface in type.GetInterfaces()) foreach (var iface in type.GetInterfaces())
{ {
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == IDictionaryGenericType) if (iface.IsGenericType && ReferenceEquals(iface.GetGenericTypeDefinition(), IDictionaryGenericType))
{ {
var args = iface.GetGenericArguments(); var args = iface.GetGenericArguments();
keyType = args[0]; keyType = args[0];
@ -347,7 +442,7 @@ public static class JsonUtilities
/// Gets the element type of a collection. /// Gets the element type of a collection.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Type? GetCollectionElementType(Type collectionType) public static Type? GetCollectionElementType(in Type collectionType)
{ {
return CollectionElementCache.GetOrAdd(collectionType, static type => return CollectionElementCache.GetOrAdd(collectionType, static type =>
{ {
@ -357,14 +452,14 @@ public static class JsonUtilities
if (type.IsGenericType) if (type.IsGenericType)
{ {
var genericDef = type.GetGenericTypeDefinition(); var genericDef = type.GetGenericTypeDefinition();
if (genericDef == ListGenericType || genericDef == IListGenericType || if (ReferenceEquals(genericDef, ListGenericType) || ReferenceEquals(genericDef, IListGenericType) ||
genericDef == typeof(ICollection<>) || genericDef == IEnumerableGenericType) genericDef == typeof(ICollection<>) || ReferenceEquals(genericDef, IEnumerableGenericType))
return type.GetGenericArguments()[0]; return type.GetGenericArguments()[0];
} }
foreach (var iface in type.GetInterfaces()) foreach (var iface in type.GetInterfaces())
{ {
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == IListGenericType) if (iface.IsGenericType && ReferenceEquals(iface.GetGenericTypeDefinition(), IListGenericType))
return iface.GetGenericArguments()[0]; return iface.GetGenericArguments()[0];
} }
@ -373,21 +468,21 @@ public static class JsonUtilities
} }
/// <summary> /// <summary>
/// Gets IId info for a type. /// Gets IId info for a type. Returns struct to avoid allocation.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [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 => return IdInfoCache.GetOrAdd(type, static t =>
{ {
foreach (var iface in t.GetInterfaces()) foreach (var iface in t.GetInterfaces())
{ {
if (!iface.IsGenericType) continue; if (!iface.IsGenericType) continue;
if (iface.GetGenericTypeDefinition() != IIdGenericType) continue; if (!ReferenceEquals(iface.GetGenericTypeDefinition(), IIdGenericType)) continue;
var idType = iface.GetGenericArguments()[0]; 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. /// Checks if collection contains primitive elements.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsPrimitiveElementCollection(Type type) public static bool IsPrimitiveElementCollection(in Type type)
{ {
return IsPrimitiveCollectionCache.GetOrAdd(type, static t => return IsPrimitiveCollectionCache.GetOrAdd(type, static t =>
{ {
if (t == StringType) return false; if (ReferenceEquals(t, StringType)) return false;
Type? elementType = null; Type? elementType = null;
if (t.IsArray) if (t.IsArray)
@ -430,7 +525,7 @@ public static class JsonUtilities
/// Gets or creates a list factory for a given element type. /// Gets or creates a list factory for a given element type.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Func<IList> GetOrCreateListFactory(Type elementType) public static Func<IList> GetOrCreateListFactory(in Type elementType)
{ {
return ListFactoryCache.GetOrAdd(elementType, static t => return ListFactoryCache.GetOrAdd(elementType, static t =>
{ {
@ -445,7 +540,7 @@ public static class JsonUtilities
/// Checks if value is the default value for its type. /// Checks if value is the default value for its type.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [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, IntType)) return (int)id == 0;
if (ReferenceEquals(idType, LongType)) return (long)id == 0; if (ReferenceEquals(idType, LongType)) return (long)id == 0;

View File

@ -40,18 +40,18 @@ public sealed class CachedPropertyInfo
if (!ShouldSkip) if (!ShouldSkip)
{ {
var (isId, idType) = GetIdInfo(PropertyType); var idInfo = GetIdInfo(PropertyType);
IsIId = isId; IsIId = idInfo.IsId;
IdType = idType; IdType = idInfo.IdType;
if (!IsIId && typeof(IEnumerable).IsAssignableFrom(PropertyType) && PropertyType != StringType) if (!IsIId && typeof(IEnumerable).IsAssignableFrom(PropertyType) && !ReferenceEquals(PropertyType, StringType))
{ {
CollectionElementType = GetCollectionElementType(PropertyType); CollectionElementType = GetCollectionElementType(PropertyType);
if (CollectionElementType != null) if (CollectionElementType != null)
{ {
var (elemIsId, elemIdType) = GetIdInfo(CollectionElementType); var elemIdInfo = GetIdInfo(CollectionElementType);
IsIIdCollection = elemIsId; IsIIdCollection = elemIdInfo.IsId;
CollectionElementIdType = elemIdType; CollectionElementIdType = elemIdInfo.IdType;
} }
} }
} }
@ -315,7 +315,7 @@ public class UnifiedMergeContractResolver : DefaultContractResolver
var t = property.PropertyType; var t = property.PropertyType;
if (t == null) return property; if (t == null) return property;
var config = GetOrCreatePropertyConfig(member, t); var config = GetOrCreatePropertyConfigRef(member, t);
if (config.IsPrimitiveElementCollection) if (config.IsPrimitiveElementCollection)
{ {
@ -343,7 +343,7 @@ public class UnifiedMergeContractResolver : DefaultContractResolver
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [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)); => PropertyConfigCache.GetOrAdd(member, _ => CreatePropertyConfig(member, propertyType));
private static CachedPropertyConfig CreatePropertyConfig(MemberInfo member, Type propertyType) private static CachedPropertyConfig CreatePropertyConfig(MemberInfo member, Type propertyType)
@ -360,11 +360,11 @@ public class UnifiedMergeContractResolver : DefaultContractResolver
config.ElementType = GetCollectionElementType(propertyType); config.ElementType = GetCollectionElementType(propertyType);
if (config.ElementType != null) if (config.ElementType != null)
{ {
var (hasId, elemIdType) = GetIdInfo(config.ElementType); var idInfo = GetIdInfo(config.ElementType);
if (hasId && elemIdType != null) if (idInfo.IsId && idInfo.IdType != null)
{ {
config.IsIdCollection = true; config.IsIdCollection = true;
config.IdType = elemIdType; config.IdType = idInfo.IdType;
} }
config.IsPrimitiveElement = IsPrimitiveOrString(config.ElementType); config.IsPrimitiveElement = IsPrimitiveOrString(config.ElementType);
} }

View File

@ -1,137 +1,339 @@
using AyCode.Core.Extensions; using AyCode.Core.Extensions;
using AyCode.Core.Tests.TestModels; using AyCode.Core.Tests.TestModels;
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Reports;
using Newtonsoft.Json; using Newtonsoft.Json;
using System.Text; using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using JsonSerializer = System.Text.Json.JsonSerializer;
namespace AyCode.Core.Benchmarks; namespace AyCode.Core.Benchmarks;
/// <summary> /// <summary>
/// Serialization benchmarks comparing AcJsonSerializer/Deserializer with Newtonsoft.Json. /// Serialization benchmarks comparing AyCode, Newtonsoft.Json, and System.Text.Json.
/// Uses shared TestModels from AyCode.Core.Tests for consistency. /// Tests small, medium, and large data with and without reference handling.
/// </summary> /// </summary>
[MemoryDiagnoser] [MemoryDiagnoser]
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
[CategoriesColumn]
public class SerializationBenchmarks public class SerializationBenchmarks
{ {
// Test data - uses shared TestModels // Test data - small, medium, large
private TestOrder _testOrder = null!; private TestOrder _smallOrder = null!;
private TestOrder _mediumOrder = null!;
private TestOrder _largeOrder = null!;
// Pre-serialized JSON for deserialization benchmarks // Pre-serialized JSON for deserialization benchmarks
private string _newtonsoftJson = null!; private string _smallAyCodeJson = null!;
private string _ayCodeJson = 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 // STJ options
private TestOrder _populateTarget = null!; private JsonSerializerOptions _stjWithRefs = null!;
private JsonSerializerOptions _stjNoRefs = null!;
// Settings // AyCode options
private JsonSerializerSettings _newtonsoftNoRefSettings = null!; private AcJsonSerializerOptions _ayCodeWithRefs = null!;
private AcJsonSerializerOptions _ayCodeNoRefs = null!;
// Newtonsoft settings
private JsonSerializerSettings _newtonsoftSettings = null!;
[GlobalSetup] [GlobalSetup]
public void Setup() public void Setup()
{ {
// Newtonsoft WITHOUT reference handling (baseline) // Small: ~20 objects (1 item × 1 pallet × 2 measurements × 3 points)
_newtonsoftNoRefSettings = new JsonSerializerSettings _smallOrder = TestDataFactory.CreateBenchmarkOrder(
{ itemCount: 1,
ReferenceLoopHandling = ReferenceLoopHandling.Ignore, palletsPerItem: 1,
NullValueHandling = NullValueHandling.Ignore, measurementsPerPallet: 2,
DefaultValueHandling = DefaultValueHandling.Ignore, pointsPerMeasurement: 3);
Formatting = Formatting.None
};
// Create benchmark data using shared factory // Medium: ~300 objects (3 items × 2 pallets × 2 measurements × 5 points)
// ~1500 objects: 5 items × 4 pallets × 3 measurements × 5 points = 300 points + containers _mediumOrder = TestDataFactory.CreateBenchmarkOrder(
_testOrder = 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, itemCount: 5,
palletsPerItem: 4, palletsPerItem: 4,
measurementsPerPallet: 3, measurementsPerPallet: 3,
pointsPerMeasurement: 5); pointsPerMeasurement: 5);
// Pre-serialize for deserialization benchmarks // STJ options with reference handling
_newtonsoftJson = JsonConvert.SerializeObject(_testOrder, _newtonsoftNoRefSettings); _stjWithRefs = new JsonSerializerOptions
_ayCodeJson = _testOrder.ToJson(); {
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNamingPolicy = null,
WriteIndented = false,
ReferenceHandler = ReferenceHandler.Preserve,
MaxDepth = 256
};
// Create target for populate benchmarks // STJ options without reference handling
_populateTarget = new TestOrder(); _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 // Output sizes for comparison
var newtonsoftBytes = Encoding.UTF8.GetByteCount(_newtonsoftJson);
var ayCodeBytes = Encoding.UTF8.GetByteCount(_ayCodeJson);
Console.WriteLine("=== JSON Size Comparison ==="); Console.WriteLine("=== JSON Size Comparison ===");
Console.WriteLine($"Newtonsoft (skip defaults): {_newtonsoftJson.Length:N0} chars ({newtonsoftBytes:N0} bytes)"); 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($"AyCode (refs+skip): {_ayCodeJson.Length:N0} chars ({ayCodeBytes:N0} bytes)"); 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($"Size reduction: {(1.0 - (double)ayCodeBytes / newtonsoftBytes) * 100:F1}%"); 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)")] [Benchmark(Description = "AyCode Serialize")]
[BenchmarkCategory("Serialize")] [BenchmarkCategory("Serialize-Large-WithRefs")]
public string Serialize_Newtonsoft_NoRefs() public string Serialize_Large_AyCode_WithRefs()
=> JsonConvert.SerializeObject(_testOrder, _newtonsoftNoRefSettings); => AcJsonSerializer.Serialize(_largeOrder, _ayCodeWithRefs);
[Benchmark(Description = "AyCode (with refs)")] [Benchmark(Description = "STJ Serialize", Baseline = true)]
[BenchmarkCategory("Serialize")] [BenchmarkCategory("Serialize-Large-WithRefs")]
public string Serialize_AyCode_WithRefs() public string Serialize_Large_STJ_WithRefs()
=> _testOrder.ToJson(); => JsonSerializer.Serialize(_largeOrder, _stjWithRefs);
[Benchmark(Description = "AcJsonSerializer (custom)")]
[BenchmarkCategory("Serialize")]
public string Serialize_AcJsonSerializer()
=> AcJsonSerializer.Serialize(_testOrder);
#endregion #endregion
#region Deserialization Benchmarks #region Serialize Large - No Refs
[Benchmark(Description = "Newtonsoft (no refs)")] [Benchmark(Description = "AyCode Serialize")]
[BenchmarkCategory("Deserialize")] [BenchmarkCategory("Serialize-Large-NoRefs")]
public TestOrder? Deserialize_Newtonsoft_NoRefs() public string Serialize_Large_AyCode_NoRefs()
=> JsonConvert.DeserializeObject<TestOrder>(_newtonsoftJson, _newtonsoftNoRefSettings); => AcJsonSerializer.Serialize(_largeOrder, _ayCodeNoRefs);
[Benchmark(Description = "AyCode (with refs)")] [Benchmark(Description = "STJ Serialize", Baseline = true)]
[BenchmarkCategory("Deserialize")] [BenchmarkCategory("Serialize-Large-NoRefs")]
public TestOrder? Deserialize_AyCode_WithRefs() public string Serialize_Large_STJ_NoRefs()
=> _ayCodeJson.JsonTo<TestOrder>(); => JsonSerializer.Serialize(_largeOrder, _stjNoRefs);
[Benchmark(Description = "AcJsonDeserializer (custom)")]
[BenchmarkCategory("Deserialize")]
public TestOrder? Deserialize_AcJsonDeserializer()
=> AcJsonDeserializer.Deserialize<TestOrder>(_ayCodeJson);
#endregion #endregion
#region Populate Benchmarks #region Serialize Medium - With Refs
[Benchmark(Description = "AcJsonDeserializer.Populate")] [Benchmark(Description = "AyCode Serialize")]
[BenchmarkCategory("Populate")] [BenchmarkCategory("Serialize-Medium-WithRefs")]
public void Populate_AcJsonDeserializer() 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(); var target = new TestOrder();
AcJsonDeserializer.Populate(_ayCodeJson, target); AcJsonDeserializer.Populate(_smallAyCodeJson, target);
} }
[Benchmark(Description = "Newtonsoft PopulateObject")] [Benchmark(Description = "Newtonsoft PopulateObject", Baseline = true)]
[BenchmarkCategory("Populate")] [BenchmarkCategory("Populate-Small")]
public void Populate_Newtonsoft() public void Populate_Small_Newtonsoft()
{ {
// Create fresh target for each iteration to match the other benchmark
var target = new TestOrder(); var target = new TestOrder();
JsonConvert.PopulateObject(_newtonsoftJson, target, _newtonsoftNoRefSettings); JsonConvert.PopulateObject(_smallNewtonsoftJson, target, _newtonsoftSettings);
} }
#endregion #endregion
#region JSON Size Comparison #region Populate Benchmarks - Medium
[Benchmark(Description = "JSON Size - Newtonsoft")] [Benchmark(Description = "AyCode Populate")]
[BenchmarkCategory("Size")] [BenchmarkCategory("Populate-Medium")]
public int JsonSize_Newtonsoft() => _newtonsoftJson.Length; public void Populate_Medium_AyCode()
{
var target = new TestOrder();
AcJsonDeserializer.Populate(_mediumAyCodeJson, target);
}
[Benchmark(Description = "JSON Size - AyCode")] [Benchmark(Description = "Newtonsoft PopulateObject", Baseline = true)]
[BenchmarkCategory("Size")] [BenchmarkCategory("Populate-Medium")]
public int JsonSize_AyCode() => _ayCodeJson.Length; 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 #endregion
} }