493 lines
19 KiB
C#
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<T> 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);
|
|
}
|