AyCode.Core/AyCode.Core/Extensions/AcJsonSerializer.cs

493 lines
19 KiB
C#

using System.Buffers;
using System.Collections;
using System.Collections.Concurrent;
using System.Globalization;
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;
namespace AyCode.Core.Extensions;
/// <summary>
/// High-performance custom JSON serializer optimized for IId&lt;T&gt; reference handling.
/// 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, in AcJsonSerializerOptions options)
{
if (value == null) return "null";
var type = value.GetType();
if (TrySerializePrimitiveRuntime(value, type, out var primitiveJson))
return primitiveJson;
var context = SerializationContextPool.Get(options);
try
{
if (options.UseReferenceHandling)
ScanReferences(value, context, 0);
WriteValue(value, context, 0);
return context.GetResult();
}
finally
{
SerializationContextPool.Return(context);
}
}
[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);
switch (typeCode)
{
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 (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; }
json = "";
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static string SerializeString(string value)
{
if (!NeedsEscaping(value)) return string.Concat("\"", value, "\"");
var sb = new StringBuilder(value.Length + 8);
sb.Append('"');
WriteEscapedString(sb, value);
sb.Append('"');
return sb.ToString();
}
#region Reference Scanning
private static void ScanReferences(object? value, SerializationContext context, int depth)
{
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 && !ReferenceEquals(type, StringType))
{
foreach (var item in enumerable)
if (item != null) ScanReferences(item, context, depth + 1);
return;
}
var metadata = GetTypeMetadata(type);
var props = metadata.Properties;
var propCount = props.Length;
for (var i = 0; i < propCount; i++)
{
var propValue = props[i].GetValue(value);
if (propValue != null) ScanReferences(propValue, context, depth + 1);
}
}
#endregion
#region Serialization
private static void WriteValue(object? value, SerializationContext context, int depth)
{
if (value == null) { context.Writer.WriteNullValue(); return; }
var type = value.GetType();
if (TryWritePrimitive(value, type, context.Writer)) return;
if (depth > context.MaxDepth) { context.Writer.WriteNullValue(); return; }
if (value is IDictionary dictionary) { WriteDictionary(dictionary, context, depth); return; }
if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType)) { WriteArray(enumerable, context, depth); return; }
WriteObject(value, type, context, 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))
{
writer.WriteStartObject();
writer.WriteString(RefPropertyEncoded, refId);
writer.WriteEndObject();
return;
}
writer.WriteStartObject();
if (context.UseReferenceHandling && context.ShouldWriteId(value, out var id))
{
writer.WriteString(IdPropertyEncoded, id);
context.MarkAsWritten(value, id);
}
var metadata = GetTypeMetadata(type);
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 (IsDefaultValueFast(propValue, prop.PropertyTypeCode, prop.PropertyType)) continue;
writer.WritePropertyName(prop.JsonNameEncoded);
WriteValue(propValue, context, nextDepth);
}
writer.WriteEndObject();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteArray(IEnumerable enumerable, SerializationContext context, int depth)
{
var writer = context.Writer;
writer.WriteStartArray();
var nextDepth = depth + 1;
foreach (var item in enumerable)
{
WriteValue(item, context, nextDepth);
}
writer.WriteEndArray();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteDictionary(IDictionary dictionary, SerializationContext context, int depth)
{
var writer = context.Writer;
writer.WriteStartObject();
var nextDepth = depth + 1;
foreach (DictionaryEntry entry in dictionary)
{
writer.WritePropertyName(entry.Key?.ToString() ?? "");
WriteValue(entry.Value, context, nextDepth);
}
writer.WriteEndObject();
}
[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);
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 IsDefaultValueFast(object value, TypeCode typeCode, in Type 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 (propertyType.IsEnum) return Convert.ToInt32(value) == 0;
if (ReferenceEquals(propertyType, GuidType)) return (Guid)value == Guid.Empty;
return false;
}
#endregion
#region Type Metadata
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static TypeMetadata GetTypeMetadata(in Type type)
=> TypeMetadataCache.GetOrAdd(type, static t => new TypeMetadata(t));
private sealed class TypeMetadata
{
public PropertyAccessor[] Properties { get; }
public TypeMetadata(Type type)
{
Properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.CanRead &&
p.GetIndexParameters().Length == 0 &&
!HasJsonIgnoreAttribute(p))
.Select(p => new PropertyAccessor(p))
.ToArray();
}
}
private sealed class PropertyAccessor
{
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;
JsonNameEncoded = JsonEncodedText.Encode(prop.Name);
PropertyType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
PropertyTypeCode = Type.GetTypeCode(PropertyType);
_getter = CreateCompiledGetter(prop.DeclaringType!, prop);
}
private static Func<object, object?> CreateCompiledGetter(Type declaringType, PropertyInfo prop)
{
var objParam = Expression.Parameter(typeof(object), "obj");
var castExpr = Expression.Convert(objParam, declaringType);
var propAccess = Expression.Property(castExpr, prop);
var boxed = Expression.Convert(propAccess, typeof(object));
return Expression.Lambda<Func<object, object?>>(boxed, objParam).Compile();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public object? GetValue(object obj) => _getter(obj);
}
#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 : IDisposable
{
private readonly ArrayBufferWriter<byte> _buffer;
public Utf8JsonWriter Writer { get; private set; }
private Dictionary<object, int>? _scanOccurrences;
private Dictionary<object, string>? _writtenRefs;
private HashSet<object>? _multiReferenced;
private int _nextId;
public bool UseReferenceHandling { get; private set; }
public byte MaxDepth { get; private set; }
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);
}
}
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;
ref var count = ref System.Runtime.InteropServices.CollectionsMarshal.GetValueRefOrAddDefault(_scanOccurrences, obj, out var exists);
if (exists) { count++; _multiReferenced!.Add(obj); return false; }
count = 1;
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool ShouldWriteId(object obj, out string id)
{
if (_multiReferenced != null && _multiReferenced.Contains(obj) && !_writtenRefs!.ContainsKey(obj))
{
id = _nextId++.ToString();
return true;
}
id = "";
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 string GetResult()
{
Writer.Flush();
return Encoding.UTF8.GetString(_buffer.WrittenSpan);
}
public void Dispose()
{
Writer.Dispose();
}
}
#endregion
}
/// <summary>
/// Reference equality comparer for object identity comparison.
/// </summary>
internal sealed class ReferenceEqualityComparer : IEqualityComparer<object>
{
public static readonly ReferenceEqualityComparer Instance = new();
public new bool Equals(object? x, object? y) => ReferenceEquals(x, y);
public int GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj);
}