High-perf streaming JSON (de)serialization, refactor
Major upgrade to AcJsonSerializer/AcJsonDeserializer: - Add Utf8JsonReader/Writer fast-paths for streaming, allocation-free (de)serialization when reference handling is not needed, matching STJ performance. - Populate/merge now uses streaming for in-place updates. - Type metadata caches TypeCode, UnderlyingType, and uses frozen dictionaries for hot property lookup. - Context pooling reduces allocations and GC pressure. - Primitive (de)serialization uses TypeCode-based fast paths; improved enum, Guid, DateTime, etc. handling. - Shared UTF8 buffer pool for efficient encoding/decoding. - Pre-encoded property names and STJ writer for output. - Improved validation, error handling, and double-serialization detection. - Expanded benchmarks: small/medium/large, with/without refs, AyCode vs STJ vs Newtonsoft, grouped by scenario. - General code modernization: aggressive inlining, ref params, ReferenceEquals, improved naming, and comments. - Unified IId<T> and collection element detection; consistent $id/$ref handling. Brings AyCode JSON (de)serializer to near-parity with STJ for non-ref scenarios, while retaining advanced features and improving maintainability and performance.
This commit is contained in:
parent
ad426feba4
commit
a945db9b09
File diff suppressed because it is too large
Load Diff
|
|
@ -6,6 +6,7 @@ using System.Linq.Expressions;
|
|||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using AyCode.Core.Interfaces;
|
||||
using Newtonsoft.Json;
|
||||
using static AyCode.Core.Extensions.JsonUtilities;
|
||||
|
|
@ -14,92 +15,95 @@ namespace AyCode.Core.Extensions;
|
|||
|
||||
/// <summary>
|
||||
/// High-performance custom JSON serializer optimized for IId<T> reference handling.
|
||||
/// Features:
|
||||
/// - Single-pass serialization with inline $id/$ref emission
|
||||
/// - StringBuilder-based output (no intermediate string allocations)
|
||||
/// - Compiled expression tree property accessors
|
||||
/// - Smart reference tracking: only emits $id when object is actually referenced later
|
||||
/// - MaxDepth support for controlling serialization depth
|
||||
/// Uses Utf8JsonWriter for high-performance UTF-8 output (STJ approach).
|
||||
/// </summary>
|
||||
public static class AcJsonSerializer
|
||||
{
|
||||
private static readonly ConcurrentDictionary<Type, TypeMetadata> TypeMetadataCache = new();
|
||||
|
||||
// Pre-encoded property names for $id/$ref (STJ optimization)
|
||||
private static readonly JsonEncodedText IdPropertyEncoded = JsonEncodedText.Encode("$id");
|
||||
private static readonly JsonEncodedText RefPropertyEncoded = JsonEncodedText.Encode("$ref");
|
||||
|
||||
/// <summary>
|
||||
/// Serialize object to JSON string with default options.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static string Serialize<T>(T value) => Serialize(value, AcJsonSerializerOptions.Default);
|
||||
|
||||
/// <summary>
|
||||
/// Serialize object to JSON string with specified options.
|
||||
/// </summary>
|
||||
public static string Serialize<T>(T value, AcJsonSerializerOptions options)
|
||||
public static string Serialize<T>(T value, in AcJsonSerializerOptions options)
|
||||
{
|
||||
if (value == null) return "null";
|
||||
|
||||
var type = typeof(T);
|
||||
var type = value.GetType();
|
||||
|
||||
if (TrySerializePrimitive(value, type, out var primitiveJson))
|
||||
if (TrySerializePrimitiveRuntime(value, type, out var primitiveJson))
|
||||
return primitiveJson;
|
||||
|
||||
var context = new SerializationContext(options);
|
||||
|
||||
if (options.UseReferenceHandling)
|
||||
ScanReferences(value, context, 0);
|
||||
|
||||
context.StartWriting();
|
||||
WriteValue(value, context, 0);
|
||||
|
||||
return context.GetResult();
|
||||
var context = SerializationContextPool.Get(options);
|
||||
try
|
||||
{
|
||||
if (options.UseReferenceHandling)
|
||||
ScanReferences(value, context, 0);
|
||||
|
||||
WriteValue(value, context, 0);
|
||||
return context.GetResult();
|
||||
}
|
||||
finally
|
||||
{
|
||||
SerializationContextPool.Return(context);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TrySerializePrimitive<T>(T value, Type type, out string json)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool TrySerializePrimitiveRuntime(object value, in Type type, out string json)
|
||||
{
|
||||
var underlyingType = Nullable.GetUnderlyingType(type) ?? type;
|
||||
var typeCode = Type.GetTypeCode(underlyingType);
|
||||
|
||||
if (underlyingType == StringType) { json = SerializeString((string)(object)value!); return true; }
|
||||
if (underlyingType == IntType) { json = ((int)(object)value!).ToString(CultureInfo.InvariantCulture); return true; }
|
||||
if (underlyingType == LongType) { json = ((long)(object)value!).ToString(CultureInfo.InvariantCulture); return true; }
|
||||
if (underlyingType == BoolType) { json = (bool)(object)value! ? "true" : "false"; return true; }
|
||||
|
||||
if (underlyingType == DoubleType)
|
||||
switch (typeCode)
|
||||
{
|
||||
var d = (double)(object)value!;
|
||||
json = double.IsNaN(d) || double.IsInfinity(d) ? "null" : d.ToString("G17", CultureInfo.InvariantCulture);
|
||||
return true;
|
||||
case TypeCode.String: json = SerializeString((string)value); return true;
|
||||
case TypeCode.Int32: json = ((int)value).ToString(CultureInfo.InvariantCulture); return true;
|
||||
case TypeCode.Int64: json = ((long)value).ToString(CultureInfo.InvariantCulture); return true;
|
||||
case TypeCode.Boolean: json = (bool)value ? "true" : "false"; return true;
|
||||
case TypeCode.Double:
|
||||
var d = (double)value;
|
||||
json = double.IsNaN(d) || double.IsInfinity(d) ? "null" : d.ToString("G17", CultureInfo.InvariantCulture);
|
||||
return true;
|
||||
case TypeCode.Decimal: json = ((decimal)value).ToString(CultureInfo.InvariantCulture); return true;
|
||||
case TypeCode.Single:
|
||||
var f = (float)value;
|
||||
json = float.IsNaN(f) || float.IsInfinity(f) ? "null" : f.ToString("G9", CultureInfo.InvariantCulture);
|
||||
return true;
|
||||
case TypeCode.DateTime: json = $"\"{((DateTime)value).ToString("O", CultureInfo.InvariantCulture)}\""; return true;
|
||||
case TypeCode.Byte: json = ((byte)value).ToString(CultureInfo.InvariantCulture); return true;
|
||||
case TypeCode.Int16: json = ((short)value).ToString(CultureInfo.InvariantCulture); return true;
|
||||
case TypeCode.UInt16: json = ((ushort)value).ToString(CultureInfo.InvariantCulture); return true;
|
||||
case TypeCode.UInt32: json = ((uint)value).ToString(CultureInfo.InvariantCulture); return true;
|
||||
case TypeCode.UInt64: json = ((ulong)value).ToString(CultureInfo.InvariantCulture); return true;
|
||||
case TypeCode.SByte: json = ((sbyte)value).ToString(CultureInfo.InvariantCulture); return true;
|
||||
case TypeCode.Char: json = SerializeString(value.ToString()!); return true;
|
||||
}
|
||||
|
||||
if (underlyingType == DecimalType) { json = ((decimal)(object)value!).ToString(CultureInfo.InvariantCulture); return true; }
|
||||
|
||||
if (underlyingType == FloatType)
|
||||
{
|
||||
var f = (float)(object)value!;
|
||||
json = float.IsNaN(f) || float.IsInfinity(f) ? "null" : f.ToString("G9", CultureInfo.InvariantCulture);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (underlyingType == DateTimeType) { json = $"\"{((DateTime)(object)value!).ToString("O", CultureInfo.InvariantCulture)}\""; return true; }
|
||||
if (underlyingType == DateTimeOffsetType) { json = $"\"{((DateTimeOffset)(object)value!).ToString("O", CultureInfo.InvariantCulture)}\""; return true; }
|
||||
if (underlyingType == GuidType) { json = $"\"{((Guid)(object)value!).ToString("D")}\""; return true; }
|
||||
if (underlyingType == TimeSpanType) { json = $"\"{((TimeSpan)(object)value!).ToString("c", CultureInfo.InvariantCulture)}\""; return true; }
|
||||
if (ReferenceEquals(underlyingType, GuidType)) { json = $"\"{((Guid)value).ToString("D")}\""; return true; }
|
||||
if (ReferenceEquals(underlyingType, DateTimeOffsetType)) { json = $"\"{((DateTimeOffset)value).ToString("O", CultureInfo.InvariantCulture)}\""; return true; }
|
||||
if (ReferenceEquals(underlyingType, TimeSpanType)) { json = $"\"{((TimeSpan)value).ToString("c", CultureInfo.InvariantCulture)}\""; return true; }
|
||||
if (underlyingType.IsEnum) { json = Convert.ToInt32(value).ToString(CultureInfo.InvariantCulture); return true; }
|
||||
if (underlyingType == ByteType) { json = ((byte)(object)value!).ToString(CultureInfo.InvariantCulture); return true; }
|
||||
if (underlyingType == ShortType) { json = ((short)(object)value!).ToString(CultureInfo.InvariantCulture); return true; }
|
||||
if (underlyingType == UShortType) { json = ((ushort)(object)value!).ToString(CultureInfo.InvariantCulture); return true; }
|
||||
if (underlyingType == UIntType) { json = ((uint)(object)value!).ToString(CultureInfo.InvariantCulture); return true; }
|
||||
if (underlyingType == ULongType) { json = ((ulong)(object)value!).ToString(CultureInfo.InvariantCulture); return true; }
|
||||
if (underlyingType == SByteType) { json = ((sbyte)(object)value!).ToString(CultureInfo.InvariantCulture); return true; }
|
||||
if (underlyingType == CharType) { json = SerializeString(value!.ToString()!); return true; }
|
||||
|
||||
json = "";
|
||||
return false;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static string SerializeString(string value)
|
||||
{
|
||||
if (!NeedsEscaping(value)) return $"\"{value}\"";
|
||||
if (!NeedsEscaping(value)) return string.Concat("\"", value, "\"");
|
||||
|
||||
var sb = new StringBuilder(value.Length + 2);
|
||||
var sb = new StringBuilder(value.Length + 8);
|
||||
sb.Append('"');
|
||||
WriteEscapedString(sb, value);
|
||||
sb.Append('"');
|
||||
|
|
@ -110,16 +114,13 @@ public static class AcJsonSerializer
|
|||
|
||||
private static void ScanReferences(object? value, SerializationContext context, int depth)
|
||||
{
|
||||
if (value == null) return;
|
||||
if (depth > context.MaxDepth) return;
|
||||
if (value == null || depth > context.MaxDepth) return;
|
||||
|
||||
var type = value.GetType();
|
||||
|
||||
if (IsPrimitiveOrStringFast(type)) return;
|
||||
|
||||
if (!context.TrackForScanning(value)) return;
|
||||
|
||||
if (value is IEnumerable enumerable && type != StringType)
|
||||
if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType))
|
||||
{
|
||||
foreach (var item in enumerable)
|
||||
if (item != null) ScanReferences(item, context, depth + 1);
|
||||
|
|
@ -127,9 +128,11 @@ public static class AcJsonSerializer
|
|||
}
|
||||
|
||||
var metadata = GetTypeMetadata(type);
|
||||
foreach (var prop in metadata.Properties)
|
||||
var props = metadata.Properties;
|
||||
var propCount = props.Length;
|
||||
for (var i = 0; i < propCount; i++)
|
||||
{
|
||||
var propValue = prop.GetValue(value);
|
||||
var propValue = props[i].GetValue(value);
|
||||
if (propValue != null) ScanReferences(propValue, context, depth + 1);
|
||||
}
|
||||
}
|
||||
|
|
@ -140,133 +143,154 @@ public static class AcJsonSerializer
|
|||
|
||||
private static void WriteValue(object? value, SerializationContext context, int depth)
|
||||
{
|
||||
if (value == null) { context.WriteNull(); return; }
|
||||
if (value == null) { context.Writer.WriteNullValue(); return; }
|
||||
|
||||
var type = value.GetType();
|
||||
|
||||
if (TryWritePrimitive(value, type, context)) return;
|
||||
if (TryWritePrimitive(value, type, context.Writer)) return;
|
||||
|
||||
// Check depth limit for complex types
|
||||
if (depth > context.MaxDepth)
|
||||
{
|
||||
context.WriteNull();
|
||||
return;
|
||||
}
|
||||
if (depth > context.MaxDepth) { context.Writer.WriteNullValue(); return; }
|
||||
|
||||
if (value is IDictionary dictionary) { WriteDictionary(dictionary, context, depth); return; }
|
||||
if (value is IEnumerable enumerable && type != StringType) { WriteArray(enumerable, context, depth); return; }
|
||||
if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType)) { WriteArray(enumerable, context, depth); return; }
|
||||
|
||||
WriteObject(value, type, context, depth);
|
||||
}
|
||||
|
||||
private static void WriteObject(object value, Type type, SerializationContext context, int depth)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void WriteObject(object value, in Type type, SerializationContext context, int depth)
|
||||
{
|
||||
var writer = context.Writer;
|
||||
|
||||
if (context.UseReferenceHandling && context.TryGetExistingRef(value, out var refId))
|
||||
{
|
||||
context.WriteRef(refId);
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString(RefPropertyEncoded, refId);
|
||||
writer.WriteEndObject();
|
||||
return;
|
||||
}
|
||||
|
||||
context.WriteObjectStart();
|
||||
var isFirst = true;
|
||||
writer.WriteStartObject();
|
||||
|
||||
if (context.UseReferenceHandling && context.ShouldWriteId(value, out var id))
|
||||
{
|
||||
context.WritePropertyName("$id", ref isFirst);
|
||||
context.WriteString(id);
|
||||
writer.WriteString(IdPropertyEncoded, id);
|
||||
context.MarkAsWritten(value, id);
|
||||
}
|
||||
|
||||
var metadata = GetTypeMetadata(type);
|
||||
foreach (var prop in metadata.Properties)
|
||||
var props = metadata.Properties;
|
||||
var propCount = props.Length;
|
||||
var nextDepth = depth + 1;
|
||||
|
||||
for (var i = 0; i < propCount; i++)
|
||||
{
|
||||
var prop = props[i];
|
||||
var propValue = prop.GetValue(value);
|
||||
if (propValue == null) continue;
|
||||
if (IsSerializerDefaultValue(propValue, prop.PropertyType)) continue;
|
||||
if (IsDefaultValueFast(propValue, prop.PropertyTypeCode, prop.PropertyType)) continue;
|
||||
|
||||
context.WritePropertyName(prop.JsonName, ref isFirst);
|
||||
WriteValue(propValue, context, depth + 1);
|
||||
writer.WritePropertyName(prop.JsonNameEncoded);
|
||||
WriteValue(propValue, context, nextDepth);
|
||||
}
|
||||
|
||||
context.WriteObjectEnd();
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void WriteArray(IEnumerable enumerable, SerializationContext context, int depth)
|
||||
{
|
||||
context.WriteArrayStart();
|
||||
var isFirst = true;
|
||||
var writer = context.Writer;
|
||||
writer.WriteStartArray();
|
||||
var nextDepth = depth + 1;
|
||||
|
||||
foreach (var item in enumerable)
|
||||
{
|
||||
if (!isFirst) context.WriteComma();
|
||||
isFirst = false;
|
||||
WriteValue(item, context, depth + 1);
|
||||
WriteValue(item, context, nextDepth);
|
||||
}
|
||||
|
||||
context.WriteArrayEnd();
|
||||
writer.WriteEndArray();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void WriteDictionary(IDictionary dictionary, SerializationContext context, int depth)
|
||||
{
|
||||
context.WriteObjectStart();
|
||||
var isFirst = true;
|
||||
var writer = context.Writer;
|
||||
writer.WriteStartObject();
|
||||
var nextDepth = depth + 1;
|
||||
|
||||
foreach (DictionaryEntry entry in dictionary)
|
||||
{
|
||||
context.WritePropertyName(entry.Key?.ToString() ?? "", ref isFirst);
|
||||
WriteValue(entry.Value, context, depth + 1);
|
||||
writer.WritePropertyName(entry.Key?.ToString() ?? "");
|
||||
WriteValue(entry.Value, context, nextDepth);
|
||||
}
|
||||
|
||||
context.WriteObjectEnd();
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
private static bool TryWritePrimitive(object value, Type type, SerializationContext context)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool TryWritePrimitive(object value, in Type type, Utf8JsonWriter writer)
|
||||
{
|
||||
var underlyingType = Nullable.GetUnderlyingType(type) ?? type;
|
||||
var typeCode = Type.GetTypeCode(underlyingType);
|
||||
|
||||
if (underlyingType == StringType) { context.WriteString((string)value); return true; }
|
||||
if (underlyingType == IntType) { context.WriteInt((int)value); return true; }
|
||||
if (underlyingType == LongType) { context.WriteLong((long)value); return true; }
|
||||
if (underlyingType == BoolType) { context.WriteBool((bool)value); return true; }
|
||||
if (underlyingType == DoubleType) { context.WriteDouble((double)value); return true; }
|
||||
if (underlyingType == DecimalType) { context.WriteDecimal((decimal)value); return true; }
|
||||
if (underlyingType == FloatType) { context.WriteFloat((float)value); return true; }
|
||||
if (underlyingType == DateTimeType) { context.WriteDateTime((DateTime)value); return true; }
|
||||
if (underlyingType == DateTimeOffsetType) { context.WriteDateTimeOffset((DateTimeOffset)value); return true; }
|
||||
if (underlyingType == GuidType) { context.WriteGuid((Guid)value); return true; }
|
||||
if (underlyingType == TimeSpanType) { context.WriteTimeSpan((TimeSpan)value); return true; }
|
||||
if (underlyingType.IsEnum) { context.WriteInt(Convert.ToInt32(value)); return true; }
|
||||
if (underlyingType == ByteType) { context.WriteInt((byte)value); return true; }
|
||||
if (underlyingType == ShortType) { context.WriteInt((short)value); return true; }
|
||||
if (underlyingType == UShortType) { context.WriteInt((ushort)value); return true; }
|
||||
if (underlyingType == UIntType) { context.WriteLong((uint)value); return true; }
|
||||
if (underlyingType == ULongType) { context.WriteULong((ulong)value); return true; }
|
||||
if (underlyingType == SByteType) { context.WriteInt((sbyte)value); return true; }
|
||||
if (underlyingType == CharType) { context.WriteString(value.ToString()!); return true; }
|
||||
switch (typeCode)
|
||||
{
|
||||
case TypeCode.String: writer.WriteStringValue((string)value); return true;
|
||||
case TypeCode.Int32: writer.WriteNumberValue((int)value); return true;
|
||||
case TypeCode.Int64: writer.WriteNumberValue((long)value); return true;
|
||||
case TypeCode.Boolean: writer.WriteBooleanValue((bool)value); return true;
|
||||
case TypeCode.Double:
|
||||
var d = (double)value;
|
||||
if (double.IsNaN(d) || double.IsInfinity(d)) writer.WriteNullValue();
|
||||
else writer.WriteNumberValue(d);
|
||||
return true;
|
||||
case TypeCode.Decimal: writer.WriteNumberValue((decimal)value); return true;
|
||||
case TypeCode.Single:
|
||||
var f = (float)value;
|
||||
if (float.IsNaN(f) || float.IsInfinity(f)) writer.WriteNullValue();
|
||||
else writer.WriteNumberValue(f);
|
||||
return true;
|
||||
case TypeCode.DateTime: writer.WriteStringValue((DateTime)value); return true;
|
||||
case TypeCode.Byte: writer.WriteNumberValue((byte)value); return true;
|
||||
case TypeCode.Int16: writer.WriteNumberValue((short)value); return true;
|
||||
case TypeCode.UInt16: writer.WriteNumberValue((ushort)value); return true;
|
||||
case TypeCode.UInt32: writer.WriteNumberValue((uint)value); return true;
|
||||
case TypeCode.UInt64: writer.WriteNumberValue((ulong)value); return true;
|
||||
case TypeCode.SByte: writer.WriteNumberValue((sbyte)value); return true;
|
||||
case TypeCode.Char: writer.WriteStringValue(value.ToString()); return true;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(underlyingType, GuidType)) { writer.WriteStringValue((Guid)value); return true; }
|
||||
if (ReferenceEquals(underlyingType, DateTimeOffsetType)) { writer.WriteStringValue((DateTimeOffset)value); return true; }
|
||||
if (ReferenceEquals(underlyingType, TimeSpanType)) { writer.WriteStringValue(((TimeSpan)value).ToString("c", CultureInfo.InvariantCulture)); return true; }
|
||||
if (underlyingType.IsEnum) { writer.WriteNumberValue(Convert.ToInt32(value)); return true; }
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool IsSerializerDefaultValue(object value, Type propertyType)
|
||||
private static bool IsDefaultValueFast(object value, TypeCode typeCode, in Type propertyType)
|
||||
{
|
||||
var type = Nullable.GetUnderlyingType(propertyType) ?? propertyType;
|
||||
switch (typeCode)
|
||||
{
|
||||
case TypeCode.Int32: return (int)value == 0;
|
||||
case TypeCode.Int64: return (long)value == 0L;
|
||||
case TypeCode.Double: return (double)value == 0.0;
|
||||
case TypeCode.Decimal: return (decimal)value == 0m;
|
||||
case TypeCode.Single: return (float)value == 0f;
|
||||
case TypeCode.Byte: return (byte)value == 0;
|
||||
case TypeCode.Int16: return (short)value == 0;
|
||||
case TypeCode.UInt16: return (ushort)value == 0;
|
||||
case TypeCode.UInt32: return (uint)value == 0;
|
||||
case TypeCode.UInt64: return (ulong)value == 0;
|
||||
case TypeCode.SByte: return (sbyte)value == 0;
|
||||
case TypeCode.Boolean: return (bool)value == false;
|
||||
case TypeCode.String: return string.IsNullOrEmpty((string)value);
|
||||
}
|
||||
|
||||
if (type == IntType) return (int)value == 0;
|
||||
if (type == LongType) return (long)value == 0L;
|
||||
if (type == DoubleType) return (double)value == 0.0;
|
||||
if (type == DecimalType) return (decimal)value == 0m;
|
||||
if (type == FloatType) return (float)value == 0f;
|
||||
if (type == ByteType) return (byte)value == 0;
|
||||
if (type == ShortType) return (short)value == 0;
|
||||
if (type == UShortType) return (ushort)value == 0;
|
||||
if (type == UIntType) return (uint)value == 0;
|
||||
if (type == ULongType) return (ulong)value == 0;
|
||||
if (type == SByteType) return (sbyte)value == 0;
|
||||
if (type == BoolType) return (bool)value == false;
|
||||
if (type == StringType) return string.IsNullOrEmpty((string)value);
|
||||
if (type.IsEnum) return Convert.ToInt32(value) == 0;
|
||||
if (type == GuidType) return (Guid)value == Guid.Empty;
|
||||
if (propertyType.IsEnum) return Convert.ToInt32(value) == 0;
|
||||
if (ReferenceEquals(propertyType, GuidType)) return (Guid)value == Guid.Empty;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
@ -275,7 +299,8 @@ public static class AcJsonSerializer
|
|||
|
||||
#region Type Metadata
|
||||
|
||||
private static TypeMetadata GetTypeMetadata(Type type)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static TypeMetadata GetTypeMetadata(in Type type)
|
||||
=> TypeMetadataCache.GetOrAdd(type, static t => new TypeMetadata(t));
|
||||
|
||||
private sealed class TypeMetadata
|
||||
|
|
@ -295,14 +320,18 @@ public static class AcJsonSerializer
|
|||
|
||||
private sealed class PropertyAccessor
|
||||
{
|
||||
public string JsonName { get; }
|
||||
public Type PropertyType { get; }
|
||||
public readonly string JsonName;
|
||||
public readonly JsonEncodedText JsonNameEncoded; // STJ optimization - pre-encoded property name
|
||||
public readonly Type PropertyType;
|
||||
public readonly TypeCode PropertyTypeCode;
|
||||
private readonly Func<object, object?> _getter;
|
||||
|
||||
public PropertyAccessor(PropertyInfo prop)
|
||||
{
|
||||
JsonName = prop.Name;
|
||||
PropertyType = prop.PropertyType;
|
||||
JsonNameEncoded = JsonEncodedText.Encode(prop.Name);
|
||||
PropertyType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
|
||||
PropertyTypeCode = Type.GetTypeCode(PropertyType);
|
||||
_getter = CreateCompiledGetter(prop.DeclaringType!, prop);
|
||||
}
|
||||
|
||||
|
|
@ -321,35 +350,89 @@ public static class AcJsonSerializer
|
|||
|
||||
#endregion
|
||||
|
||||
#region Context Pool
|
||||
|
||||
private static class SerializationContextPool
|
||||
{
|
||||
private static readonly ConcurrentQueue<SerializationContext> Pool = new();
|
||||
private const int MaxPoolSize = 16;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SerializationContext Get(in AcJsonSerializerOptions options)
|
||||
{
|
||||
if (Pool.TryDequeue(out var context))
|
||||
{
|
||||
context.Reset(options);
|
||||
return context;
|
||||
}
|
||||
return new SerializationContext(options);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Return(SerializationContext context)
|
||||
{
|
||||
if (Pool.Count < MaxPoolSize)
|
||||
{
|
||||
context.Clear();
|
||||
Pool.Enqueue(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Serialization Context
|
||||
|
||||
private sealed class SerializationContext
|
||||
private sealed class SerializationContext : IDisposable
|
||||
{
|
||||
private readonly StringBuilder _sb = new(4096);
|
||||
private readonly Dictionary<object, int>? _scanOccurrences;
|
||||
private readonly Dictionary<object, string>? _writtenRefs;
|
||||
private readonly HashSet<object>? _multiReferenced;
|
||||
private int _nextId = 1;
|
||||
private readonly ArrayBufferWriter<byte> _buffer;
|
||||
public Utf8JsonWriter Writer { get; private set; }
|
||||
|
||||
private static readonly ArrayPool<char> CharPool = ArrayPool<char>.Shared;
|
||||
private readonly char[] _numberBuffer = CharPool.Rent(64);
|
||||
private Dictionary<object, int>? _scanOccurrences;
|
||||
private Dictionary<object, string>? _writtenRefs;
|
||||
private HashSet<object>? _multiReferenced;
|
||||
private int _nextId;
|
||||
|
||||
public bool UseReferenceHandling { get; }
|
||||
public byte MaxDepth { get; }
|
||||
public bool UseReferenceHandling { get; private set; }
|
||||
public byte MaxDepth { get; private set; }
|
||||
|
||||
public SerializationContext(AcJsonSerializerOptions options)
|
||||
private static readonly JsonWriterOptions WriterOptions = new()
|
||||
{
|
||||
Indented = false,
|
||||
SkipValidation = true // Skip validation for performance
|
||||
};
|
||||
|
||||
public SerializationContext(in AcJsonSerializerOptions options)
|
||||
{
|
||||
_buffer = new ArrayBufferWriter<byte>(4096);
|
||||
Writer = new Utf8JsonWriter(_buffer, WriterOptions);
|
||||
Reset(options);
|
||||
}
|
||||
|
||||
public void Reset(in AcJsonSerializerOptions options)
|
||||
{
|
||||
UseReferenceHandling = options.UseReferenceHandling;
|
||||
MaxDepth = options.MaxDepth;
|
||||
_nextId = 1;
|
||||
|
||||
if (UseReferenceHandling)
|
||||
{
|
||||
_scanOccurrences = new Dictionary<object, int>(64, ReferenceEqualityComparer.Instance);
|
||||
_writtenRefs = new Dictionary<object, string>(32, ReferenceEqualityComparer.Instance);
|
||||
_multiReferenced = new HashSet<object>(32, ReferenceEqualityComparer.Instance);
|
||||
_scanOccurrences ??= new Dictionary<object, int>(64, ReferenceEqualityComparer.Instance);
|
||||
_writtenRefs ??= new Dictionary<object, string>(32, ReferenceEqualityComparer.Instance);
|
||||
_multiReferenced ??= new HashSet<object>(32, ReferenceEqualityComparer.Instance);
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
Writer.Reset();
|
||||
_buffer.Clear();
|
||||
_scanOccurrences?.Clear();
|
||||
_writtenRefs?.Clear();
|
||||
_multiReferenced?.Clear();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TrackForScanning(object obj)
|
||||
{
|
||||
if (_scanOccurrences == null) return true;
|
||||
|
|
@ -360,8 +443,7 @@ public static class AcJsonSerializer
|
|||
return true;
|
||||
}
|
||||
|
||||
public void StartWriting() { }
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool ShouldWriteId(object obj, out string id)
|
||||
{
|
||||
if (_multiReferenced != null && _multiReferenced.Contains(obj) && !_writtenRefs!.ContainsKey(obj))
|
||||
|
|
@ -373,122 +455,26 @@ public static class AcJsonSerializer
|
|||
return false;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void MarkAsWritten(object obj, string id) => _writtenRefs![obj] = id;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryGetExistingRef(object obj, out string refId)
|
||||
{
|
||||
if (_writtenRefs != null) return _writtenRefs.TryGetValue(obj, out refId!);
|
||||
refId = "";
|
||||
return false;
|
||||
}
|
||||
|
||||
public void WriteRef(string refId) { _sb.Append("{\"$ref\":\""); _sb.Append(refId); _sb.Append("\"}"); }
|
||||
|
||||
public string GetResult() { CharPool.Return(_numberBuffer); return _sb.ToString(); }
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteNull() => _sb.Append("null");
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteObjectStart() => _sb.Append('{');
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteObjectEnd() => _sb.Append('}');
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteArrayStart() => _sb.Append('[');
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteArrayEnd() => _sb.Append(']');
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteComma() => _sb.Append(',');
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WritePropertyName(string name, ref bool isFirst)
|
||||
public string GetResult()
|
||||
{
|
||||
if (!isFirst) _sb.Append(',');
|
||||
isFirst = false;
|
||||
_sb.Append('"'); _sb.Append(name); _sb.Append("\":");
|
||||
Writer.Flush();
|
||||
return Encoding.UTF8.GetString(_buffer.WrittenSpan);
|
||||
}
|
||||
|
||||
public void WriteString(string value)
|
||||
public void Dispose()
|
||||
{
|
||||
_sb.Append('"');
|
||||
if (!NeedsEscaping(value)) _sb.Append(value);
|
||||
else WriteEscapedString(_sb, value);
|
||||
_sb.Append('"');
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteInt(int value)
|
||||
{
|
||||
if (value.TryFormat(_numberBuffer, out var written)) _sb.Append(_numberBuffer, 0, written);
|
||||
else _sb.Append(value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteLong(long value)
|
||||
{
|
||||
if (value.TryFormat(_numberBuffer, out var written)) _sb.Append(_numberBuffer, 0, written);
|
||||
else _sb.Append(value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteULong(ulong value)
|
||||
{
|
||||
if (value.TryFormat(_numberBuffer, out var written)) _sb.Append(_numberBuffer, 0, written);
|
||||
else _sb.Append(value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteBool(bool value) => _sb.Append(value ? "true" : "false");
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteDouble(double value)
|
||||
{
|
||||
if (double.IsNaN(value) || double.IsInfinity(value)) _sb.Append("null");
|
||||
else if (value.TryFormat(_numberBuffer, out var written, "G17", CultureInfo.InvariantCulture)) _sb.Append(_numberBuffer, 0, written);
|
||||
else _sb.Append(value.ToString("G17", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteFloat(float value)
|
||||
{
|
||||
if (float.IsNaN(value) || float.IsInfinity(value)) _sb.Append("null");
|
||||
else if (value.TryFormat(_numberBuffer, out var written, "G9", CultureInfo.InvariantCulture)) _sb.Append(_numberBuffer, 0, written);
|
||||
else _sb.Append(value.ToString("G9", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteDecimal(decimal value)
|
||||
{
|
||||
if (value.TryFormat(_numberBuffer, out var written, provider: CultureInfo.InvariantCulture)) _sb.Append(_numberBuffer, 0, written);
|
||||
else _sb.Append(value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
public void WriteDateTime(DateTime value)
|
||||
{
|
||||
_sb.Append('"');
|
||||
Span<char> buffer = stackalloc char[33];
|
||||
if (value.TryFormat(buffer, out var written, "O", CultureInfo.InvariantCulture)) _sb.Append(buffer[..written]);
|
||||
else _sb.Append(value.ToString("O", CultureInfo.InvariantCulture));
|
||||
_sb.Append('"');
|
||||
}
|
||||
|
||||
public void WriteDateTimeOffset(DateTimeOffset value)
|
||||
{
|
||||
_sb.Append('"');
|
||||
Span<char> buffer = stackalloc char[33];
|
||||
if (value.TryFormat(buffer, out var written, "O", CultureInfo.InvariantCulture)) _sb.Append(buffer[..written]);
|
||||
else _sb.Append(value.ToString("O", CultureInfo.InvariantCulture));
|
||||
_sb.Append('"');
|
||||
}
|
||||
|
||||
public void WriteGuid(Guid value)
|
||||
{
|
||||
_sb.Append('"');
|
||||
Span<char> buffer = stackalloc char[36];
|
||||
if (value.TryFormat(buffer, out var written, "D")) _sb.Append(buffer[..written]);
|
||||
else _sb.Append(value.ToString("D"));
|
||||
_sb.Append('"');
|
||||
}
|
||||
|
||||
public void WriteTimeSpan(TimeSpan value)
|
||||
{
|
||||
_sb.Append('"');
|
||||
Span<char> buffer = stackalloc char[26];
|
||||
if (value.TryFormat(buffer, out var written, "c", CultureInfo.InvariantCulture)) _sb.Append(buffer[..written]);
|
||||
else _sb.Append(value.ToString("c", CultureInfo.InvariantCulture));
|
||||
_sb.Append('"');
|
||||
Writer.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Buffers;
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Frozen;
|
||||
|
|
@ -51,6 +52,27 @@ public sealed class AcJsonSerializerOptions
|
|||
public static AcJsonSerializerOptions WithoutReferenceHandling() => new() { UseReferenceHandling = false };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cached result for IId type info lookup.
|
||||
/// </summary>
|
||||
public readonly struct IdTypeInfo
|
||||
{
|
||||
public readonly bool IsId;
|
||||
public readonly Type? IdType;
|
||||
|
||||
public IdTypeInfo(bool isId, Type? idType)
|
||||
{
|
||||
IsId = isId;
|
||||
IdType = idType;
|
||||
}
|
||||
|
||||
public void Deconstruct(out bool isId, out Type? idType)
|
||||
{
|
||||
isId = IsId;
|
||||
idType = IdType;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Central utilities for JSON serialization/deserialization.
|
||||
/// Contains shared type caches, primitive type checks, and string utilities.
|
||||
|
|
@ -109,7 +131,7 @@ public static class JsonUtilities
|
|||
|
||||
#region Type Caches
|
||||
|
||||
private static readonly ConcurrentDictionary<Type, (bool IsId, Type? IdType)> IdInfoCache = new();
|
||||
private static readonly ConcurrentDictionary<Type, IdTypeInfo> IdInfoCache = new();
|
||||
private static readonly ConcurrentDictionary<Type, Type?> CollectionElementCache = new();
|
||||
private static readonly ConcurrentDictionary<Type, bool> IsPrimitiveCache = new();
|
||||
private static readonly ConcurrentDictionary<Type, bool> IsCollectionCache = new();
|
||||
|
|
@ -119,6 +141,37 @@ public static class JsonUtilities
|
|||
|
||||
#endregion
|
||||
|
||||
#region UTF8 Buffer Pool
|
||||
|
||||
/// <summary>
|
||||
/// Rents a UTF8 byte buffer from the shared pool.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static byte[] RentUtf8Buffer(int minLength)
|
||||
=> ArrayPool<byte>.Shared.Rent(minLength);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a UTF8 byte buffer to the shared pool.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ReturnUtf8Buffer(byte[] buffer)
|
||||
=> ArrayPool<byte>.Shared.Return(buffer);
|
||||
|
||||
/// <summary>
|
||||
/// Converts a JSON string to UTF8 bytes using pooled buffer.
|
||||
/// Returns the actual byte count written.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static (byte[] buffer, int length) GetUtf8Bytes(string json)
|
||||
{
|
||||
var maxByteCount = Encoding.UTF8.GetMaxByteCount(json.Length);
|
||||
var buffer = RentUtf8Buffer(maxByteCount);
|
||||
var actualLength = Encoding.UTF8.GetBytes(json, 0, json.Length, buffer, 0);
|
||||
return (buffer, actualLength);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region String Utilities
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -211,6 +264,20 @@ public static class JsonUtilities
|
|||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a span needs JSON escaping.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool NeedsEscaping(ReadOnlySpan<char> value)
|
||||
{
|
||||
foreach (var c in value)
|
||||
{
|
||||
if (c < 32 || c == '"' || c == '\\')
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Escapes a string for JSON output.
|
||||
/// </summary>
|
||||
|
|
@ -238,6 +305,34 @@ public static class JsonUtilities
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Escapes a span for JSON output.
|
||||
/// </summary>
|
||||
public static void WriteEscapedString(StringBuilder sb, ReadOnlySpan<char> value)
|
||||
{
|
||||
foreach (var c in value)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case '"': sb.Append("\\\""); break;
|
||||
case '\\': sb.Append("\\\\"); break;
|
||||
case '\b': sb.Append("\\b"); break;
|
||||
case '\f': sb.Append("\\f"); break;
|
||||
case '\n': sb.Append("\\n"); break;
|
||||
case '\r': sb.Append("\\r"); break;
|
||||
case '\t': sb.Append("\\t"); break;
|
||||
default:
|
||||
if (c < 32)
|
||||
{
|
||||
sb.Append("\\u");
|
||||
sb.Append(((int)c).ToString("X4"));
|
||||
}
|
||||
else sb.Append(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
|
@ -263,7 +358,7 @@ public static class JsonUtilities
|
|||
/// Faster primitive check using TypeCode for hot paths.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsPrimitiveOrStringFast(Type type)
|
||||
public static bool IsPrimitiveOrStringFast(in Type type)
|
||||
{
|
||||
var typeCode = Type.GetTypeCode(type);
|
||||
return typeCode switch
|
||||
|
|
@ -272,7 +367,7 @@ public static class JsonUtilities
|
|||
TypeCode.Int16 or TypeCode.UInt16 or TypeCode.Int32 or TypeCode.UInt32 or
|
||||
TypeCode.Int64 or TypeCode.UInt64 or TypeCode.Single or TypeCode.Double or
|
||||
TypeCode.Decimal or TypeCode.DateTime or TypeCode.String => true,
|
||||
_ => type == GuidType || type == TimeSpanType || type == DateTimeOffsetType || type.IsEnum
|
||||
_ => ReferenceEquals(type, GuidType) || ReferenceEquals(type, TimeSpanType) || ReferenceEquals(type, DateTimeOffsetType) || type.IsEnum
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -280,28 +375,28 @@ public static class JsonUtilities
|
|||
/// Checks if type is a generic collection type (List, IList, ObservableCollection, etc.)
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsGenericCollectionType(Type type)
|
||||
public static bool IsGenericCollectionType(in Type type)
|
||||
{
|
||||
return IsCollectionCache.GetOrAdd(type, static t =>
|
||||
{
|
||||
if (t == StringType || t.IsPrimitive) return false;
|
||||
if (ReferenceEquals(t, StringType) || t.IsPrimitive) return false;
|
||||
if (t.IsArray) return true;
|
||||
|
||||
if (t.IsGenericType)
|
||||
{
|
||||
var genericDef = t.GetGenericTypeDefinition();
|
||||
if (genericDef == ListGenericType ||
|
||||
genericDef == IListGenericType ||
|
||||
if (ReferenceEquals(genericDef, ListGenericType) ||
|
||||
ReferenceEquals(genericDef, IListGenericType) ||
|
||||
genericDef == typeof(ICollection<>) ||
|
||||
genericDef == IEnumerableGenericType ||
|
||||
genericDef == ObservableCollectionType ||
|
||||
genericDef == CollectionType)
|
||||
ReferenceEquals(genericDef, IEnumerableGenericType) ||
|
||||
ReferenceEquals(genericDef, ObservableCollectionType) ||
|
||||
ReferenceEquals(genericDef, CollectionType))
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (var iface in t.GetInterfaces())
|
||||
{
|
||||
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == IListGenericType)
|
||||
if (iface.IsGenericType && ReferenceEquals(iface.GetGenericTypeDefinition(), IListGenericType))
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -313,7 +408,7 @@ public static class JsonUtilities
|
|||
/// Checks if type is a dictionary type.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsDictionaryType(Type type, out Type? keyType, out Type? valueType)
|
||||
public static bool IsDictionaryType(in Type type, out Type? keyType, out Type? valueType)
|
||||
{
|
||||
keyType = null;
|
||||
valueType = null;
|
||||
|
|
@ -321,7 +416,7 @@ public static class JsonUtilities
|
|||
if (!type.IsGenericType) return false;
|
||||
|
||||
var genericDef = type.GetGenericTypeDefinition();
|
||||
if (genericDef == DictionaryGenericType || genericDef == IDictionaryGenericType)
|
||||
if (ReferenceEquals(genericDef, DictionaryGenericType) || ReferenceEquals(genericDef, IDictionaryGenericType))
|
||||
{
|
||||
var args = type.GetGenericArguments();
|
||||
keyType = args[0];
|
||||
|
|
@ -331,7 +426,7 @@ public static class JsonUtilities
|
|||
|
||||
foreach (var iface in type.GetInterfaces())
|
||||
{
|
||||
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == IDictionaryGenericType)
|
||||
if (iface.IsGenericType && ReferenceEquals(iface.GetGenericTypeDefinition(), IDictionaryGenericType))
|
||||
{
|
||||
var args = iface.GetGenericArguments();
|
||||
keyType = args[0];
|
||||
|
|
@ -347,7 +442,7 @@ public static class JsonUtilities
|
|||
/// Gets the element type of a collection.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Type? GetCollectionElementType(Type collectionType)
|
||||
public static Type? GetCollectionElementType(in Type collectionType)
|
||||
{
|
||||
return CollectionElementCache.GetOrAdd(collectionType, static type =>
|
||||
{
|
||||
|
|
@ -357,14 +452,14 @@ public static class JsonUtilities
|
|||
if (type.IsGenericType)
|
||||
{
|
||||
var genericDef = type.GetGenericTypeDefinition();
|
||||
if (genericDef == ListGenericType || genericDef == IListGenericType ||
|
||||
genericDef == typeof(ICollection<>) || genericDef == IEnumerableGenericType)
|
||||
if (ReferenceEquals(genericDef, ListGenericType) || ReferenceEquals(genericDef, IListGenericType) ||
|
||||
genericDef == typeof(ICollection<>) || ReferenceEquals(genericDef, IEnumerableGenericType))
|
||||
return type.GetGenericArguments()[0];
|
||||
}
|
||||
|
||||
foreach (var iface in type.GetInterfaces())
|
||||
{
|
||||
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == IListGenericType)
|
||||
if (iface.IsGenericType && ReferenceEquals(iface.GetGenericTypeDefinition(), IListGenericType))
|
||||
return iface.GetGenericArguments()[0];
|
||||
}
|
||||
|
||||
|
|
@ -373,21 +468,21 @@ public static class JsonUtilities
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets IId info for a type.
|
||||
/// Gets IId info for a type. Returns struct to avoid allocation.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static (bool IsId, Type? IdType) GetIdInfo(Type type)
|
||||
public static IdTypeInfo GetIdInfo(in Type type)
|
||||
{
|
||||
return IdInfoCache.GetOrAdd(type, static t =>
|
||||
{
|
||||
foreach (var iface in t.GetInterfaces())
|
||||
{
|
||||
if (!iface.IsGenericType) continue;
|
||||
if (iface.GetGenericTypeDefinition() != IIdGenericType) continue;
|
||||
if (!ReferenceEquals(iface.GetGenericTypeDefinition(), IIdGenericType)) continue;
|
||||
var idType = iface.GetGenericArguments()[0];
|
||||
return (idType.IsValueType, idType);
|
||||
return new IdTypeInfo(idType.IsValueType, idType);
|
||||
}
|
||||
return (false, null);
|
||||
return new IdTypeInfo(false, null);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -406,11 +501,11 @@ public static class JsonUtilities
|
|||
/// Checks if collection contains primitive elements.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsPrimitiveElementCollection(Type type)
|
||||
public static bool IsPrimitiveElementCollection(in Type type)
|
||||
{
|
||||
return IsPrimitiveCollectionCache.GetOrAdd(type, static t =>
|
||||
{
|
||||
if (t == StringType) return false;
|
||||
if (ReferenceEquals(t, StringType)) return false;
|
||||
|
||||
Type? elementType = null;
|
||||
if (t.IsArray)
|
||||
|
|
@ -430,7 +525,7 @@ public static class JsonUtilities
|
|||
/// Gets or creates a list factory for a given element type.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Func<IList> GetOrCreateListFactory(Type elementType)
|
||||
public static Func<IList> GetOrCreateListFactory(in Type elementType)
|
||||
{
|
||||
return ListFactoryCache.GetOrAdd(elementType, static t =>
|
||||
{
|
||||
|
|
@ -445,7 +540,7 @@ public static class JsonUtilities
|
|||
/// Checks if value is the default value for its type.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsDefaultValue(object id, Type idType)
|
||||
public static bool IsDefaultValue(in object id, in Type idType)
|
||||
{
|
||||
if (ReferenceEquals(idType, IntType)) return (int)id == 0;
|
||||
if (ReferenceEquals(idType, LongType)) return (long)id == 0;
|
||||
|
|
|
|||
|
|
@ -40,18 +40,18 @@ public sealed class CachedPropertyInfo
|
|||
|
||||
if (!ShouldSkip)
|
||||
{
|
||||
var (isId, idType) = GetIdInfo(PropertyType);
|
||||
IsIId = isId;
|
||||
IdType = idType;
|
||||
var idInfo = GetIdInfo(PropertyType);
|
||||
IsIId = idInfo.IsId;
|
||||
IdType = idInfo.IdType;
|
||||
|
||||
if (!IsIId && typeof(IEnumerable).IsAssignableFrom(PropertyType) && PropertyType != StringType)
|
||||
if (!IsIId && typeof(IEnumerable).IsAssignableFrom(PropertyType) && !ReferenceEquals(PropertyType, StringType))
|
||||
{
|
||||
CollectionElementType = GetCollectionElementType(PropertyType);
|
||||
if (CollectionElementType != null)
|
||||
{
|
||||
var (elemIsId, elemIdType) = GetIdInfo(CollectionElementType);
|
||||
IsIIdCollection = elemIsId;
|
||||
CollectionElementIdType = elemIdType;
|
||||
var elemIdInfo = GetIdInfo(CollectionElementType);
|
||||
IsIIdCollection = elemIdInfo.IsId;
|
||||
CollectionElementIdType = elemIdInfo.IdType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -315,7 +315,7 @@ public class UnifiedMergeContractResolver : DefaultContractResolver
|
|||
var t = property.PropertyType;
|
||||
if (t == null) return property;
|
||||
|
||||
var config = GetOrCreatePropertyConfig(member, t);
|
||||
var config = GetOrCreatePropertyConfigRef(member, t);
|
||||
|
||||
if (config.IsPrimitiveElementCollection)
|
||||
{
|
||||
|
|
@ -343,7 +343,7 @@ public class UnifiedMergeContractResolver : DefaultContractResolver
|
|||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static CachedPropertyConfig GetOrCreatePropertyConfig(MemberInfo member, Type propertyType)
|
||||
private static CachedPropertyConfig GetOrCreatePropertyConfigRef(MemberInfo member, Type propertyType)
|
||||
=> PropertyConfigCache.GetOrAdd(member, _ => CreatePropertyConfig(member, propertyType));
|
||||
|
||||
private static CachedPropertyConfig CreatePropertyConfig(MemberInfo member, Type propertyType)
|
||||
|
|
@ -360,11 +360,11 @@ public class UnifiedMergeContractResolver : DefaultContractResolver
|
|||
config.ElementType = GetCollectionElementType(propertyType);
|
||||
if (config.ElementType != null)
|
||||
{
|
||||
var (hasId, elemIdType) = GetIdInfo(config.ElementType);
|
||||
if (hasId && elemIdType != null)
|
||||
var idInfo = GetIdInfo(config.ElementType);
|
||||
if (idInfo.IsId && idInfo.IdType != null)
|
||||
{
|
||||
config.IsIdCollection = true;
|
||||
config.IdType = elemIdType;
|
||||
config.IdType = idInfo.IdType;
|
||||
}
|
||||
config.IsPrimitiveElement = IsPrimitiveOrString(config.ElementType);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,137 +1,339 @@
|
|||
using AyCode.Core.Extensions;
|
||||
using AyCode.Core.Tests.TestModels;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Columns;
|
||||
using BenchmarkDotNet.Configs;
|
||||
using BenchmarkDotNet.Reports;
|
||||
using Newtonsoft.Json;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using JsonSerializer = System.Text.Json.JsonSerializer;
|
||||
|
||||
namespace AyCode.Core.Benchmarks;
|
||||
|
||||
/// <summary>
|
||||
/// Serialization benchmarks comparing AcJsonSerializer/Deserializer with Newtonsoft.Json.
|
||||
/// Uses shared TestModels from AyCode.Core.Tests for consistency.
|
||||
/// Serialization benchmarks comparing AyCode, Newtonsoft.Json, and System.Text.Json.
|
||||
/// Tests small, medium, and large data with and without reference handling.
|
||||
/// </summary>
|
||||
[MemoryDiagnoser]
|
||||
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
|
||||
[CategoriesColumn]
|
||||
public class SerializationBenchmarks
|
||||
{
|
||||
// Test data - uses shared TestModels
|
||||
private TestOrder _testOrder = null!;
|
||||
// Test data - small, medium, large
|
||||
private TestOrder _smallOrder = null!;
|
||||
private TestOrder _mediumOrder = null!;
|
||||
private TestOrder _largeOrder = null!;
|
||||
|
||||
// Pre-serialized JSON for deserialization benchmarks
|
||||
private string _newtonsoftJson = null!;
|
||||
private string _ayCodeJson = null!;
|
||||
private string _smallAyCodeJson = null!;
|
||||
private string _smallAyCodeNoRefJson = null!;
|
||||
private string _smallStjJson = null!;
|
||||
private string _smallStjNoRefJson = null!;
|
||||
private string _smallNewtonsoftJson = null!;
|
||||
private string _mediumAyCodeJson = null!;
|
||||
private string _mediumAyCodeNoRefJson = null!;
|
||||
private string _mediumStjJson = null!;
|
||||
private string _mediumStjNoRefJson = null!;
|
||||
private string _mediumNewtonsoftJson = null!;
|
||||
private string _largeAyCodeJson = null!;
|
||||
private string _largeAyCodeNoRefJson = null!;
|
||||
private string _largeStjJson = null!;
|
||||
private string _largeStjNoRefJson = null!;
|
||||
private string _largeNewtonsoftJson = null!;
|
||||
|
||||
// Target objects for Populate benchmarks
|
||||
private TestOrder _populateTarget = null!;
|
||||
// STJ options
|
||||
private JsonSerializerOptions _stjWithRefs = null!;
|
||||
private JsonSerializerOptions _stjNoRefs = null!;
|
||||
|
||||
// Settings
|
||||
private JsonSerializerSettings _newtonsoftNoRefSettings = null!;
|
||||
// AyCode options
|
||||
private AcJsonSerializerOptions _ayCodeWithRefs = null!;
|
||||
private AcJsonSerializerOptions _ayCodeNoRefs = null!;
|
||||
|
||||
// Newtonsoft settings
|
||||
private JsonSerializerSettings _newtonsoftSettings = null!;
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
// Newtonsoft WITHOUT reference handling (baseline)
|
||||
_newtonsoftNoRefSettings = new JsonSerializerSettings
|
||||
{
|
||||
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
DefaultValueHandling = DefaultValueHandling.Ignore,
|
||||
Formatting = Formatting.None
|
||||
};
|
||||
// Small: ~20 objects (1 item × 1 pallet × 2 measurements × 3 points)
|
||||
_smallOrder = TestDataFactory.CreateBenchmarkOrder(
|
||||
itemCount: 1,
|
||||
palletsPerItem: 1,
|
||||
measurementsPerPallet: 2,
|
||||
pointsPerMeasurement: 3);
|
||||
|
||||
// Create benchmark data using shared factory
|
||||
// ~1500 objects: 5 items × 4 pallets × 3 measurements × 5 points = 300 points + containers
|
||||
_testOrder = TestDataFactory.CreateBenchmarkOrder(
|
||||
// Medium: ~300 objects (3 items × 2 pallets × 2 measurements × 5 points)
|
||||
_mediumOrder = TestDataFactory.CreateBenchmarkOrder(
|
||||
itemCount: 3,
|
||||
palletsPerItem: 2,
|
||||
measurementsPerPallet: 2,
|
||||
pointsPerMeasurement: 5);
|
||||
|
||||
// Large: ~1500 objects (5 items × 4 pallets × 3 measurements × 5 points)
|
||||
_largeOrder = TestDataFactory.CreateBenchmarkOrder(
|
||||
itemCount: 5,
|
||||
palletsPerItem: 4,
|
||||
measurementsPerPallet: 3,
|
||||
pointsPerMeasurement: 5);
|
||||
|
||||
// Pre-serialize for deserialization benchmarks
|
||||
_newtonsoftJson = JsonConvert.SerializeObject(_testOrder, _newtonsoftNoRefSettings);
|
||||
_ayCodeJson = _testOrder.ToJson();
|
||||
// STJ options with reference handling
|
||||
_stjWithRefs = new JsonSerializerOptions
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
PropertyNamingPolicy = null,
|
||||
WriteIndented = false,
|
||||
ReferenceHandler = ReferenceHandler.Preserve,
|
||||
MaxDepth = 256
|
||||
};
|
||||
|
||||
// Create target for populate benchmarks
|
||||
_populateTarget = new TestOrder();
|
||||
// STJ options without reference handling
|
||||
_stjNoRefs = new JsonSerializerOptions
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
PropertyNamingPolicy = null,
|
||||
WriteIndented = false,
|
||||
ReferenceHandler = ReferenceHandler.IgnoreCycles,
|
||||
MaxDepth = 256
|
||||
};
|
||||
|
||||
// AyCode options
|
||||
_ayCodeWithRefs = AcJsonSerializerOptions.Default;
|
||||
_ayCodeNoRefs = AcJsonSerializerOptions.WithoutReferenceHandling();
|
||||
|
||||
// Newtonsoft settings
|
||||
_newtonsoftSettings = new JsonSerializerSettings
|
||||
{
|
||||
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
|
||||
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
Formatting = Formatting.None
|
||||
};
|
||||
|
||||
// Pre-serialize for deserialization benchmarks
|
||||
_smallAyCodeJson = AcJsonSerializer.Serialize(_smallOrder, _ayCodeWithRefs);
|
||||
_smallAyCodeNoRefJson = AcJsonSerializer.Serialize(_smallOrder, _ayCodeNoRefs);
|
||||
_smallStjJson = JsonSerializer.Serialize(_smallOrder, _stjWithRefs);
|
||||
_smallStjNoRefJson = JsonSerializer.Serialize(_smallOrder, _stjNoRefs);
|
||||
_smallNewtonsoftJson = JsonConvert.SerializeObject(_smallOrder, _newtonsoftSettings);
|
||||
|
||||
_mediumAyCodeJson = AcJsonSerializer.Serialize(_mediumOrder, _ayCodeWithRefs);
|
||||
_mediumAyCodeNoRefJson = AcJsonSerializer.Serialize(_mediumOrder, _ayCodeNoRefs);
|
||||
_mediumStjJson = JsonSerializer.Serialize(_mediumOrder, _stjWithRefs);
|
||||
_mediumStjNoRefJson = JsonSerializer.Serialize(_mediumOrder, _stjNoRefs);
|
||||
_mediumNewtonsoftJson = JsonConvert.SerializeObject(_mediumOrder, _newtonsoftSettings);
|
||||
|
||||
_largeAyCodeJson = AcJsonSerializer.Serialize(_largeOrder, _ayCodeWithRefs);
|
||||
_largeAyCodeNoRefJson = AcJsonSerializer.Serialize(_largeOrder, _ayCodeNoRefs);
|
||||
_largeStjJson = JsonSerializer.Serialize(_largeOrder, _stjWithRefs);
|
||||
_largeStjNoRefJson = JsonSerializer.Serialize(_largeOrder, _stjNoRefs);
|
||||
_largeNewtonsoftJson = JsonConvert.SerializeObject(_largeOrder, _newtonsoftSettings);
|
||||
|
||||
// Output sizes for comparison
|
||||
var newtonsoftBytes = Encoding.UTF8.GetByteCount(_newtonsoftJson);
|
||||
var ayCodeBytes = Encoding.UTF8.GetByteCount(_ayCodeJson);
|
||||
|
||||
Console.WriteLine("=== JSON Size Comparison ===");
|
||||
Console.WriteLine($"Newtonsoft (skip defaults): {_newtonsoftJson.Length:N0} chars ({newtonsoftBytes:N0} bytes)");
|
||||
Console.WriteLine($"AyCode (refs+skip): {_ayCodeJson.Length:N0} chars ({ayCodeBytes:N0} bytes)");
|
||||
Console.WriteLine($"Size reduction: {(1.0 - (double)ayCodeBytes / newtonsoftBytes) * 100:F1}%");
|
||||
Console.WriteLine($"Small: AyCode(refs)={_smallAyCodeJson.Length:N0}, AyCode(noRef)={_smallAyCodeNoRefJson.Length:N0}, STJ(refs)={_smallStjJson.Length:N0}, STJ(noRef)={_smallStjNoRefJson.Length:N0}");
|
||||
Console.WriteLine($"Medium: AyCode(refs)={_mediumAyCodeJson.Length:N0}, AyCode(noRef)={_mediumAyCodeNoRefJson.Length:N0}, STJ(refs)={_mediumStjJson.Length:N0}, STJ(noRef)={_mediumStjNoRefJson.Length:N0}");
|
||||
Console.WriteLine($"Large: AyCode(refs)={_largeAyCodeJson.Length:N0}, AyCode(noRef)={_largeAyCodeNoRefJson.Length:N0}, STJ(refs)={_largeStjJson.Length:N0}, STJ(noRef)={_largeStjNoRefJson.Length:N0}");
|
||||
}
|
||||
|
||||
#region Serialization Benchmarks
|
||||
#region Serialize Large - With Refs
|
||||
|
||||
[Benchmark(Description = "Newtonsoft (no refs)")]
|
||||
[BenchmarkCategory("Serialize")]
|
||||
public string Serialize_Newtonsoft_NoRefs()
|
||||
=> JsonConvert.SerializeObject(_testOrder, _newtonsoftNoRefSettings);
|
||||
[Benchmark(Description = "AyCode Serialize")]
|
||||
[BenchmarkCategory("Serialize-Large-WithRefs")]
|
||||
public string Serialize_Large_AyCode_WithRefs()
|
||||
=> AcJsonSerializer.Serialize(_largeOrder, _ayCodeWithRefs);
|
||||
|
||||
[Benchmark(Description = "AyCode (with refs)")]
|
||||
[BenchmarkCategory("Serialize")]
|
||||
public string Serialize_AyCode_WithRefs()
|
||||
=> _testOrder.ToJson();
|
||||
|
||||
[Benchmark(Description = "AcJsonSerializer (custom)")]
|
||||
[BenchmarkCategory("Serialize")]
|
||||
public string Serialize_AcJsonSerializer()
|
||||
=> AcJsonSerializer.Serialize(_testOrder);
|
||||
[Benchmark(Description = "STJ Serialize", Baseline = true)]
|
||||
[BenchmarkCategory("Serialize-Large-WithRefs")]
|
||||
public string Serialize_Large_STJ_WithRefs()
|
||||
=> JsonSerializer.Serialize(_largeOrder, _stjWithRefs);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Deserialization Benchmarks
|
||||
#region Serialize Large - No Refs
|
||||
|
||||
[Benchmark(Description = "Newtonsoft (no refs)")]
|
||||
[BenchmarkCategory("Deserialize")]
|
||||
public TestOrder? Deserialize_Newtonsoft_NoRefs()
|
||||
=> JsonConvert.DeserializeObject<TestOrder>(_newtonsoftJson, _newtonsoftNoRefSettings);
|
||||
[Benchmark(Description = "AyCode Serialize")]
|
||||
[BenchmarkCategory("Serialize-Large-NoRefs")]
|
||||
public string Serialize_Large_AyCode_NoRefs()
|
||||
=> AcJsonSerializer.Serialize(_largeOrder, _ayCodeNoRefs);
|
||||
|
||||
[Benchmark(Description = "AyCode (with refs)")]
|
||||
[BenchmarkCategory("Deserialize")]
|
||||
public TestOrder? Deserialize_AyCode_WithRefs()
|
||||
=> _ayCodeJson.JsonTo<TestOrder>();
|
||||
|
||||
[Benchmark(Description = "AcJsonDeserializer (custom)")]
|
||||
[BenchmarkCategory("Deserialize")]
|
||||
public TestOrder? Deserialize_AcJsonDeserializer()
|
||||
=> AcJsonDeserializer.Deserialize<TestOrder>(_ayCodeJson);
|
||||
[Benchmark(Description = "STJ Serialize", Baseline = true)]
|
||||
[BenchmarkCategory("Serialize-Large-NoRefs")]
|
||||
public string Serialize_Large_STJ_NoRefs()
|
||||
=> JsonSerializer.Serialize(_largeOrder, _stjNoRefs);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Populate Benchmarks
|
||||
#region Serialize Medium - With Refs
|
||||
|
||||
[Benchmark(Description = "AcJsonDeserializer.Populate")]
|
||||
[BenchmarkCategory("Populate")]
|
||||
public void Populate_AcJsonDeserializer()
|
||||
[Benchmark(Description = "AyCode Serialize")]
|
||||
[BenchmarkCategory("Serialize-Medium-WithRefs")]
|
||||
public string Serialize_Medium_AyCode_WithRefs()
|
||||
=> AcJsonSerializer.Serialize(_mediumOrder, _ayCodeWithRefs);
|
||||
|
||||
[Benchmark(Description = "STJ Serialize", Baseline = true)]
|
||||
[BenchmarkCategory("Serialize-Medium-WithRefs")]
|
||||
public string Serialize_Medium_STJ_WithRefs()
|
||||
=> JsonSerializer.Serialize(_mediumOrder, _stjWithRefs);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Serialize Medium - No Refs
|
||||
|
||||
[Benchmark(Description = "AyCode Serialize")]
|
||||
[BenchmarkCategory("Serialize-Medium-NoRefs")]
|
||||
public string Serialize_Medium_AyCode_NoRefs()
|
||||
=> AcJsonSerializer.Serialize(_mediumOrder, _ayCodeNoRefs);
|
||||
|
||||
[Benchmark(Description = "STJ Serialize", Baseline = true)]
|
||||
[BenchmarkCategory("Serialize-Medium-NoRefs")]
|
||||
public string Serialize_Medium_STJ_NoRefs()
|
||||
=> JsonSerializer.Serialize(_mediumOrder, _stjNoRefs);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Small Data Deserialization - With Refs
|
||||
|
||||
[Benchmark(Description = "AyCode WithRefs")]
|
||||
[BenchmarkCategory("Deserialize-Small-WithRefs")]
|
||||
public TestOrder? Deserialize_Small_AyCode_WithRefs()
|
||||
=> AcJsonDeserializer.Deserialize<TestOrder>(_smallAyCodeJson, _ayCodeWithRefs);
|
||||
|
||||
[Benchmark(Description = "STJ WithRefs", Baseline = true)]
|
||||
[BenchmarkCategory("Deserialize-Small-WithRefs")]
|
||||
public TestOrder? Deserialize_Small_STJ_WithRefs()
|
||||
=> JsonSerializer.Deserialize<TestOrder>(_smallStjJson, _stjWithRefs);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Small Data Deserialization - No Refs
|
||||
|
||||
[Benchmark(Description = "AyCode NoRefs")]
|
||||
[BenchmarkCategory("Deserialize-Small-NoRefs")]
|
||||
public TestOrder? Deserialize_Small_AyCode_NoRefs()
|
||||
=> AcJsonDeserializer.Deserialize<TestOrder>(_smallAyCodeNoRefJson, _ayCodeNoRefs);
|
||||
|
||||
[Benchmark(Description = "STJ NoRefs", Baseline = true)]
|
||||
[BenchmarkCategory("Deserialize-Small-NoRefs")]
|
||||
public TestOrder? Deserialize_Small_STJ_NoRefs()
|
||||
=> JsonSerializer.Deserialize<TestOrder>(_smallStjNoRefJson, _stjNoRefs);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Medium Data Deserialization - With Refs
|
||||
|
||||
[Benchmark(Description = "AyCode WithRefs")]
|
||||
[BenchmarkCategory("Deserialize-Medium-WithRefs")]
|
||||
public TestOrder? Deserialize_Medium_AyCode_WithRefs()
|
||||
=> AcJsonDeserializer.Deserialize<TestOrder>(_mediumAyCodeJson, _ayCodeWithRefs);
|
||||
|
||||
[Benchmark(Description = "STJ WithRefs", Baseline = true)]
|
||||
[BenchmarkCategory("Deserialize-Medium-WithRefs")]
|
||||
public TestOrder? Deserialize_Medium_STJ_WithRefs()
|
||||
=> JsonSerializer.Deserialize<TestOrder>(_mediumStjJson, _stjWithRefs);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Medium Data Deserialization - No Refs
|
||||
|
||||
[Benchmark(Description = "AyCode NoRefs")]
|
||||
[BenchmarkCategory("Deserialize-Medium-NoRefs")]
|
||||
public TestOrder? Deserialize_Medium_AyCode_NoRefs()
|
||||
=> AcJsonDeserializer.Deserialize<TestOrder>(_mediumAyCodeNoRefJson, _ayCodeNoRefs);
|
||||
|
||||
[Benchmark(Description = "STJ NoRefs", Baseline = true)]
|
||||
[BenchmarkCategory("Deserialize-Medium-NoRefs")]
|
||||
public TestOrder? Deserialize_Medium_STJ_NoRefs()
|
||||
=> JsonSerializer.Deserialize<TestOrder>(_mediumStjNoRefJson, _stjNoRefs);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Large Data Deserialization - With Refs
|
||||
|
||||
[Benchmark(Description = "AyCode WithRefs")]
|
||||
[BenchmarkCategory("Deserialize-Large-WithRefs")]
|
||||
public TestOrder? Deserialize_Large_AyCode_WithRefs()
|
||||
=> AcJsonDeserializer.Deserialize<TestOrder>(_largeAyCodeJson, _ayCodeWithRefs);
|
||||
|
||||
[Benchmark(Description = "STJ WithRefs", Baseline = true)]
|
||||
[BenchmarkCategory("Deserialize-Large-WithRefs")]
|
||||
public TestOrder? Deserialize_Large_STJ_WithRefs()
|
||||
=> JsonSerializer.Deserialize<TestOrder>(_largeStjJson, _stjWithRefs);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Large Data Deserialization - No Refs
|
||||
|
||||
[Benchmark(Description = "AyCode NoRefs")]
|
||||
[BenchmarkCategory("Deserialize-Large-NoRefs")]
|
||||
public TestOrder? Deserialize_Large_AyCode_NoRefs()
|
||||
=> AcJsonDeserializer.Deserialize<TestOrder>(_largeAyCodeNoRefJson, _ayCodeNoRefs);
|
||||
|
||||
[Benchmark(Description = "STJ NoRefs", Baseline = true)]
|
||||
[BenchmarkCategory("Deserialize-Large-NoRefs")]
|
||||
public TestOrder? Deserialize_Large_STJ_NoRefs()
|
||||
=> JsonSerializer.Deserialize<TestOrder>(_largeStjNoRefJson, _stjNoRefs);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Populate Benchmarks - Small
|
||||
|
||||
[Benchmark(Description = "AyCode Populate")]
|
||||
[BenchmarkCategory("Populate-Small")]
|
||||
public void Populate_Small_AyCode()
|
||||
{
|
||||
// Create fresh target for each iteration to avoid state pollution
|
||||
var target = new TestOrder();
|
||||
AcJsonDeserializer.Populate(_ayCodeJson, target);
|
||||
AcJsonDeserializer.Populate(_smallAyCodeJson, target);
|
||||
}
|
||||
|
||||
[Benchmark(Description = "Newtonsoft PopulateObject")]
|
||||
[BenchmarkCategory("Populate")]
|
||||
public void Populate_Newtonsoft()
|
||||
[Benchmark(Description = "Newtonsoft PopulateObject", Baseline = true)]
|
||||
[BenchmarkCategory("Populate-Small")]
|
||||
public void Populate_Small_Newtonsoft()
|
||||
{
|
||||
// Create fresh target for each iteration to match the other benchmark
|
||||
var target = new TestOrder();
|
||||
JsonConvert.PopulateObject(_newtonsoftJson, target, _newtonsoftNoRefSettings);
|
||||
JsonConvert.PopulateObject(_smallNewtonsoftJson, target, _newtonsoftSettings);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region JSON Size Comparison
|
||||
#region Populate Benchmarks - Medium
|
||||
|
||||
[Benchmark(Description = "JSON Size - Newtonsoft")]
|
||||
[BenchmarkCategory("Size")]
|
||||
public int JsonSize_Newtonsoft() => _newtonsoftJson.Length;
|
||||
[Benchmark(Description = "AyCode Populate")]
|
||||
[BenchmarkCategory("Populate-Medium")]
|
||||
public void Populate_Medium_AyCode()
|
||||
{
|
||||
var target = new TestOrder();
|
||||
AcJsonDeserializer.Populate(_mediumAyCodeJson, target);
|
||||
}
|
||||
|
||||
[Benchmark(Description = "JSON Size - AyCode")]
|
||||
[BenchmarkCategory("Size")]
|
||||
public int JsonSize_AyCode() => _ayCodeJson.Length;
|
||||
[Benchmark(Description = "Newtonsoft PopulateObject", Baseline = true)]
|
||||
[BenchmarkCategory("Populate-Medium")]
|
||||
public void Populate_Medium_Newtonsoft()
|
||||
{
|
||||
var target = new TestOrder();
|
||||
JsonConvert.PopulateObject(_mediumNewtonsoftJson, target, _newtonsoftSettings);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Populate Benchmarks - Large
|
||||
|
||||
[Benchmark(Description = "AyCode Populate")]
|
||||
[BenchmarkCategory("Populate-Large")]
|
||||
public void Populate_Large_AyCode()
|
||||
{
|
||||
var target = new TestOrder();
|
||||
AcJsonDeserializer.Populate(_largeAyCodeJson, target);
|
||||
}
|
||||
|
||||
[Benchmark(Description = "Newtonsoft PopulateObject", Baseline = true)]
|
||||
[BenchmarkCategory("Populate-Large")]
|
||||
public void Populate_Large_Newtonsoft()
|
||||
{
|
||||
var target = new TestOrder();
|
||||
JsonConvert.PopulateObject(_largeNewtonsoftJson, target, _newtonsoftSettings);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Loading…
Reference in New Issue