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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,3 +1,4 @@
using System.Buffers;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Frozen;
@ -51,6 +52,27 @@ public sealed class AcJsonSerializerOptions
public static AcJsonSerializerOptions WithoutReferenceHandling() => new() { UseReferenceHandling = false };
}
/// <summary>
/// Cached result for IId type info lookup.
/// </summary>
public readonly struct IdTypeInfo
{
public readonly bool IsId;
public readonly Type? IdType;
public IdTypeInfo(bool isId, Type? idType)
{
IsId = isId;
IdType = idType;
}
public void Deconstruct(out bool isId, out Type? idType)
{
isId = IsId;
idType = IdType;
}
}
/// <summary>
/// Central utilities for JSON serialization/deserialization.
/// Contains shared type caches, primitive type checks, and string utilities.
@ -109,7 +131,7 @@ public static class JsonUtilities
#region Type Caches
private static readonly ConcurrentDictionary<Type, (bool IsId, Type? IdType)> IdInfoCache = new();
private static readonly ConcurrentDictionary<Type, IdTypeInfo> IdInfoCache = new();
private static readonly ConcurrentDictionary<Type, Type?> CollectionElementCache = new();
private static readonly ConcurrentDictionary<Type, bool> IsPrimitiveCache = new();
private static readonly ConcurrentDictionary<Type, bool> IsCollectionCache = new();
@ -119,6 +141,37 @@ public static class JsonUtilities
#endregion
#region UTF8 Buffer Pool
/// <summary>
/// Rents a UTF8 byte buffer from the shared pool.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte[] RentUtf8Buffer(int minLength)
=> ArrayPool<byte>.Shared.Rent(minLength);
/// <summary>
/// Returns a UTF8 byte buffer to the shared pool.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ReturnUtf8Buffer(byte[] buffer)
=> ArrayPool<byte>.Shared.Return(buffer);
/// <summary>
/// Converts a JSON string to UTF8 bytes using pooled buffer.
/// Returns the actual byte count written.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static (byte[] buffer, int length) GetUtf8Bytes(string json)
{
var maxByteCount = Encoding.UTF8.GetMaxByteCount(json.Length);
var buffer = RentUtf8Buffer(maxByteCount);
var actualLength = Encoding.UTF8.GetBytes(json, 0, json.Length, buffer, 0);
return (buffer, actualLength);
}
#endregion
#region String Utilities
/// <summary>
@ -211,6 +264,20 @@ public static class JsonUtilities
return false;
}
/// <summary>
/// Checks if a span needs JSON escaping.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool NeedsEscaping(ReadOnlySpan<char> value)
{
foreach (var c in value)
{
if (c < 32 || c == '"' || c == '\\')
return true;
}
return false;
}
/// <summary>
/// Escapes a string for JSON output.
/// </summary>
@ -238,6 +305,34 @@ public static class JsonUtilities
}
}
}
/// <summary>
/// Escapes a span for JSON output.
/// </summary>
public static void WriteEscapedString(StringBuilder sb, ReadOnlySpan<char> value)
{
foreach (var c in value)
{
switch (c)
{
case '"': sb.Append("\\\""); break;
case '\\': sb.Append("\\\\"); break;
case '\b': sb.Append("\\b"); break;
case '\f': sb.Append("\\f"); break;
case '\n': sb.Append("\\n"); break;
case '\r': sb.Append("\\r"); break;
case '\t': sb.Append("\\t"); break;
default:
if (c < 32)
{
sb.Append("\\u");
sb.Append(((int)c).ToString("X4"));
}
else sb.Append(c);
break;
}
}
}
#endregion
@ -263,7 +358,7 @@ public static class JsonUtilities
/// Faster primitive check using TypeCode for hot paths.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsPrimitiveOrStringFast(Type type)
public static bool IsPrimitiveOrStringFast(in Type type)
{
var typeCode = Type.GetTypeCode(type);
return typeCode switch
@ -272,7 +367,7 @@ public static class JsonUtilities
TypeCode.Int16 or TypeCode.UInt16 or TypeCode.Int32 or TypeCode.UInt32 or
TypeCode.Int64 or TypeCode.UInt64 or TypeCode.Single or TypeCode.Double or
TypeCode.Decimal or TypeCode.DateTime or TypeCode.String => true,
_ => type == GuidType || type == TimeSpanType || type == DateTimeOffsetType || type.IsEnum
_ => ReferenceEquals(type, GuidType) || ReferenceEquals(type, TimeSpanType) || ReferenceEquals(type, DateTimeOffsetType) || type.IsEnum
};
}
@ -280,28 +375,28 @@ public static class JsonUtilities
/// Checks if type is a generic collection type (List, IList, ObservableCollection, etc.)
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsGenericCollectionType(Type type)
public static bool IsGenericCollectionType(in Type type)
{
return IsCollectionCache.GetOrAdd(type, static t =>
{
if (t == StringType || t.IsPrimitive) return false;
if (ReferenceEquals(t, StringType) || t.IsPrimitive) return false;
if (t.IsArray) return true;
if (t.IsGenericType)
{
var genericDef = t.GetGenericTypeDefinition();
if (genericDef == ListGenericType ||
genericDef == IListGenericType ||
if (ReferenceEquals(genericDef, ListGenericType) ||
ReferenceEquals(genericDef, IListGenericType) ||
genericDef == typeof(ICollection<>) ||
genericDef == IEnumerableGenericType ||
genericDef == ObservableCollectionType ||
genericDef == CollectionType)
ReferenceEquals(genericDef, IEnumerableGenericType) ||
ReferenceEquals(genericDef, ObservableCollectionType) ||
ReferenceEquals(genericDef, CollectionType))
return true;
}
foreach (var iface in t.GetInterfaces())
{
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == IListGenericType)
if (iface.IsGenericType && ReferenceEquals(iface.GetGenericTypeDefinition(), IListGenericType))
return true;
}
@ -313,7 +408,7 @@ public static class JsonUtilities
/// Checks if type is a dictionary type.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsDictionaryType(Type type, out Type? keyType, out Type? valueType)
public static bool IsDictionaryType(in Type type, out Type? keyType, out Type? valueType)
{
keyType = null;
valueType = null;
@ -321,7 +416,7 @@ public static class JsonUtilities
if (!type.IsGenericType) return false;
var genericDef = type.GetGenericTypeDefinition();
if (genericDef == DictionaryGenericType || genericDef == IDictionaryGenericType)
if (ReferenceEquals(genericDef, DictionaryGenericType) || ReferenceEquals(genericDef, IDictionaryGenericType))
{
var args = type.GetGenericArguments();
keyType = args[0];
@ -331,7 +426,7 @@ public static class JsonUtilities
foreach (var iface in type.GetInterfaces())
{
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == IDictionaryGenericType)
if (iface.IsGenericType && ReferenceEquals(iface.GetGenericTypeDefinition(), IDictionaryGenericType))
{
var args = iface.GetGenericArguments();
keyType = args[0];
@ -347,7 +442,7 @@ public static class JsonUtilities
/// Gets the element type of a collection.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Type? GetCollectionElementType(Type collectionType)
public static Type? GetCollectionElementType(in Type collectionType)
{
return CollectionElementCache.GetOrAdd(collectionType, static type =>
{
@ -357,14 +452,14 @@ public static class JsonUtilities
if (type.IsGenericType)
{
var genericDef = type.GetGenericTypeDefinition();
if (genericDef == ListGenericType || genericDef == IListGenericType ||
genericDef == typeof(ICollection<>) || genericDef == IEnumerableGenericType)
if (ReferenceEquals(genericDef, ListGenericType) || ReferenceEquals(genericDef, IListGenericType) ||
genericDef == typeof(ICollection<>) || ReferenceEquals(genericDef, IEnumerableGenericType))
return type.GetGenericArguments()[0];
}
foreach (var iface in type.GetInterfaces())
{
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == IListGenericType)
if (iface.IsGenericType && ReferenceEquals(iface.GetGenericTypeDefinition(), IListGenericType))
return iface.GetGenericArguments()[0];
}
@ -373,21 +468,21 @@ public static class JsonUtilities
}
/// <summary>
/// Gets IId info for a type.
/// Gets IId info for a type. Returns struct to avoid allocation.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static (bool IsId, Type? IdType) GetIdInfo(Type type)
public static IdTypeInfo GetIdInfo(in Type type)
{
return IdInfoCache.GetOrAdd(type, static t =>
{
foreach (var iface in t.GetInterfaces())
{
if (!iface.IsGenericType) continue;
if (iface.GetGenericTypeDefinition() != IIdGenericType) continue;
if (!ReferenceEquals(iface.GetGenericTypeDefinition(), IIdGenericType)) continue;
var idType = iface.GetGenericArguments()[0];
return (idType.IsValueType, idType);
return new IdTypeInfo(idType.IsValueType, idType);
}
return (false, null);
return new IdTypeInfo(false, null);
});
}
@ -406,11 +501,11 @@ public static class JsonUtilities
/// Checks if collection contains primitive elements.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsPrimitiveElementCollection(Type type)
public static bool IsPrimitiveElementCollection(in Type type)
{
return IsPrimitiveCollectionCache.GetOrAdd(type, static t =>
{
if (t == StringType) return false;
if (ReferenceEquals(t, StringType)) return false;
Type? elementType = null;
if (t.IsArray)
@ -430,7 +525,7 @@ public static class JsonUtilities
/// Gets or creates a list factory for a given element type.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Func<IList> GetOrCreateListFactory(Type elementType)
public static Func<IList> GetOrCreateListFactory(in Type elementType)
{
return ListFactoryCache.GetOrAdd(elementType, static t =>
{
@ -445,7 +540,7 @@ public static class JsonUtilities
/// Checks if value is the default value for its type.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsDefaultValue(object id, Type idType)
public static bool IsDefaultValue(in object id, in Type idType)
{
if (ReferenceEquals(idType, IntType)) return (int)id == 0;
if (ReferenceEquals(idType, LongType)) return (long)id == 0;

View File

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

View File

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