High-perf streaming JSON (de)serialization, refactor

Major upgrade to AcJsonSerializer/AcJsonDeserializer:
- Add Utf8JsonReader/Writer fast-paths for streaming, allocation-free (de)serialization when reference handling is not needed, matching STJ performance.
- Populate/merge now uses streaming for in-place updates.
- Type metadata caches TypeCode, UnderlyingType, and uses frozen dictionaries for hot property lookup.
- Context pooling reduces allocations and GC pressure.
- Primitive (de)serialization uses TypeCode-based fast paths; improved enum, Guid, DateTime, etc. handling.
- Shared UTF8 buffer pool for efficient encoding/decoding.
- Pre-encoded property names and STJ writer for output.
- Improved validation, error handling, and double-serialization detection.
- Expanded benchmarks: small/medium/large, with/without refs, AyCode vs STJ vs Newtonsoft, grouped by scenario.
- General code modernization: aggressive inlining, ref params, ReferenceEquals, improved naming, and comments.
- Unified IId<T> and collection element detection; consistent $id/$ref handling.

Brings AyCode JSON (de)serializer to near-parity with STJ for non-ref scenarios, while retaining advanced features and improving maintainability and performance.
This commit is contained in:
Loretta 2025-12-12 16:23:54 +01:00
parent ad426feba4
commit a945db9b09
5 changed files with 1742 additions and 683 deletions

View File

@ -1,9 +1,12 @@
using System.Buffers;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.Globalization;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using AyCode.Core.Helpers;
using AyCode.Core.Interfaces;
@ -30,23 +33,28 @@ public class AcJsonDeserializationException : Exception
/// <summary>
/// High-performance custom JSON deserializer optimized for IId&lt;T&gt; reference handling.
/// Supports MaxDepth and UseReferenceHandling options.
/// Uses Utf8JsonReader for streaming deserialization when possible (STJ approach).
/// </summary>
public static class AcJsonDeserializer
{
private static readonly ConcurrentDictionary<Type, DeserializeTypeMetadata> TypeMetadataCache = new();
// Pre-computed JSON property names for fast lookup (UTF-8 bytes)
private static readonly byte[] RefPropertyUtf8 = "$ref"u8.ToArray();
private static readonly byte[] IdPropertyUtf8 = "$id"u8.ToArray();
#region Public API
/// <summary>
/// Deserialize JSON string to a new object of type T with default options.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T? Deserialize<T>(string json) => Deserialize<T>(json, AcJsonSerializerOptions.Default);
/// <summary>
/// Deserialize JSON string to a new object of type T with specified options.
/// </summary>
public static T? Deserialize<T>(string json, AcJsonSerializerOptions options)
public static T? Deserialize<T>(string json, in AcJsonSerializerOptions options)
{
if (string.IsNullOrEmpty(json) || json == "null") return default;
@ -54,18 +62,30 @@ public static class AcJsonDeserializer
try
{
ValidateJson(json, targetType);
if (TryDeserializePrimitive(json, targetType, out var primitiveResult))
if (TryDeserializePrimitiveFast(json, targetType, out var primitiveResult))
return (T?)primitiveResult;
var context = new DeserializationContext(options);
ValidateJson(json, targetType);
// Fast path for no reference handling - use Utf8JsonReader (streaming, no DOM)
if (!options.UseReferenceHandling)
{
return DeserializeWithUtf8Reader<T>(json, options.MaxDepth);
}
// Reference handling requires DOM for forward references
using var doc = JsonDocument.Parse(json);
var result = ReadValue(doc.RootElement, targetType, context, 0);
context.ResolveReferences();
return (T?)result;
var context = DeserializationContextPool.Get(options);
try
{
var result = ReadValue(doc.RootElement, targetType, context, 0);
context.ResolveReferences();
return (T?)result;
}
finally
{
DeserializationContextPool.Return(context);
}
}
catch (AcJsonDeserializationException) { throw; }
catch (System.Text.Json.JsonException ex)
@ -81,37 +101,48 @@ public static class AcJsonDeserializer
/// <summary>
/// Deserialize JSON string to specified type with default options.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static object? Deserialize(string json, Type targetType) => Deserialize(json, targetType, AcJsonSerializerOptions.Default);
/// <summary>
/// Deserialize JSON string to specified type with specified options.
/// </summary>
public static object? Deserialize(string json, Type targetType, AcJsonSerializerOptions options)
public static object? Deserialize(string json, in Type targetType, in AcJsonSerializerOptions options)
{
if (string.IsNullOrEmpty(json) || json == "null") return null;
try
{
ValidateJson(json, targetType);
var firstChar = json[0];
var isArrayJson = firstChar == '[';
var isObjectJson = firstChar == '{';
var isArrayJson = json.Length > 0 && json[0] == '[';
var isObjectJson = json.Length > 0 && json[0] == '{';
var isCollectionType = targetType.IsArray || IsGenericCollectionType(targetType);
var isDictType = IsDictionaryType(targetType, out _, out _);
if (!isArrayJson && !isCollectionType && !(isObjectJson && isDictType))
if (!isArrayJson && !isObjectJson)
{
if (TryDeserializePrimitive(json, targetType, out var primitiveResult))
if (TryDeserializePrimitiveFast(json, targetType, out var primitiveResult))
return primitiveResult;
}
var context = new DeserializationContext(options);
ValidateJson(json, targetType);
// Fast path for no reference handling
if (!options.UseReferenceHandling)
{
return DeserializeWithUtf8ReaderNonGeneric(json, targetType, options.MaxDepth);
}
using var doc = JsonDocument.Parse(json);
var result = ReadValue(doc.RootElement, targetType, context, 0);
context.ResolveReferences();
return result;
var context = DeserializationContextPool.Get(options);
try
{
var result = ReadValue(doc.RootElement, targetType, context, 0);
context.ResolveReferences();
return result;
}
finally
{
DeserializationContextPool.Return(context);
}
}
catch (AcJsonDeserializationException) { throw; }
catch (System.Text.Json.JsonException ex)
@ -127,74 +158,115 @@ public static class AcJsonDeserializer
/// <summary>
/// Populate existing object with JSON data (merge mode) with default options.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Populate<T>(string json, T target) where T : class
=> Populate(json, target, AcJsonSerializerOptions.Default);
/// <summary>
/// Populate existing object with JSON data (merge mode) with specified options.
/// </summary>
public static void Populate<T>(string json, T target, AcJsonSerializerOptions options) where T : class
public static void Populate<T>(string json, T target, in AcJsonSerializerOptions options) where T : class
{
ArgumentNullException.ThrowIfNull(target);
if (string.IsNullOrEmpty(json) || json == "null") return;
Populate(json, (object)target, target.GetType(), options);
PopulateInternal(json, target, target.GetType(), options);
}
/// <summary>
/// Populate existing object with JSON data with default options.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Populate(string json, object target)
=> Populate(json, target, target.GetType(), AcJsonSerializerOptions.Default);
/// <summary>
/// Populate existing object with JSON data with specified options.
/// </summary>
public static void Populate(string json, object target, AcJsonSerializerOptions options)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Populate(string json, object target, in AcJsonSerializerOptions options)
=> Populate(json, target, target.GetType(), options);
/// <summary>
/// Populate existing object with JSON data with default options.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Populate(string json, object target, Type targetType)
=> Populate(json, target, targetType, AcJsonSerializerOptions.Default);
/// <summary>
/// Populate existing object with JSON data with specified options.
/// </summary>
public static void Populate(string json, object target, Type targetType, AcJsonSerializerOptions options)
public static void Populate(string json, object target, in Type targetType, in AcJsonSerializerOptions options)
{
ArgumentNullException.ThrowIfNull(target);
if (string.IsNullOrEmpty(json) || json == "null") return;
PopulateInternal(json, target, targetType, options);
}
private static void PopulateInternal(string json, object target, in Type targetType, in AcJsonSerializerOptions options)
{
try
{
ValidateJson(json, targetType);
var context = new DeserializationContext(options) { IsMergeMode = true };
using var doc = JsonDocument.Parse(json);
// Fast path for no reference handling - use Utf8JsonReader streaming
if (!options.UseReferenceHandling)
{
var firstChar = json[0];
if (firstChar == '[')
{
if (target is IList targetList)
PopulateListWithUtf8Reader(json, targetList, targetType, options.MaxDepth);
else
throw new AcJsonDeserializationException($"Cannot populate non-list target '{targetType.Name}' with JSON array", json, targetType);
return;
}
if (firstChar == '{')
{
var metadata = GetTypeMetadata(targetType);
PopulateObjectWithUtf8Reader(json, target, metadata, options.MaxDepth);
return;
}
throw new AcJsonDeserializationException($"Cannot populate object with JSON starting with '{firstChar}'", json, targetType);
}
// Reference handling requires DOM for forward references
using var doc = JsonDocument.Parse(json);
var rootElement = doc.RootElement;
if (rootElement.ValueKind == JsonValueKind.Array)
var context = DeserializationContextPool.Get(options);
context.IsMergeMode = true;
try
{
if (target is IList targetList)
PopulateList(rootElement, targetList, targetType, context, 0);
if (rootElement.ValueKind == JsonValueKind.Array)
{
if (target is IList targetList)
PopulateList(rootElement, targetList, targetType, context, 0);
else
throw new AcJsonDeserializationException($"Cannot populate non-list target '{targetType.Name}' with JSON array", json, targetType);
context.ResolveReferences();
return;
}
if (rootElement.ValueKind == JsonValueKind.Object)
{
var metadata = GetTypeMetadata(targetType);
PopulateObjectInternalMerge(rootElement, target, metadata, context, 0);
}
else
throw new AcJsonDeserializationException($"Cannot populate non-list target '{targetType.Name}' with JSON array", json, targetType);
throw new AcJsonDeserializationException($"Cannot populate object with JSON value of kind '{rootElement.ValueKind}'", json, targetType);
context.ResolveReferences();
return;
}
if (rootElement.ValueKind == JsonValueKind.Object)
finally
{
var metadata = GetTypeMetadata(targetType);
PopulateObjectInternalMerge(rootElement, target, metadata, context, 0);
DeserializationContextPool.Return(context);
}
else
throw new AcJsonDeserializationException($"Cannot populate object with JSON value of kind '{rootElement.ValueKind}'", json, targetType);
context.ResolveReferences();
}
catch (AcJsonDeserializationException) { throw; }
catch (System.Text.Json.JsonException ex)
@ -209,71 +281,202 @@ public static class AcJsonDeserializer
#endregion
#region Validation
#region Utf8JsonReader Fast Path (STJ-style streaming)
private static void ValidateJson(string json, Type targetType)
/// <summary>
/// Deserialize using Utf8JsonReader - streaming without DOM allocation.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static T? DeserializeWithUtf8Reader<T>(string json, byte maxDepth)
{
if (string.IsNullOrEmpty(json)) return;
if (json.Length > 2 && json[0] == '"' && json[^1] == '"')
var (buffer, length) = GetUtf8Bytes(json);
try
{
var inner = json[1..^1];
if (inner.Contains("\\\"") && (inner.Contains("{") || inner.Contains("[")))
throw new AcJsonDeserializationException(
$"Detected double-serialized JSON string. Target type: {targetType.Name}.",
json, targetType);
var reader = new Utf8JsonReader(buffer.AsSpan(0, length), new JsonReaderOptions { MaxDepth = maxDepth });
if (!reader.Read()) return default;
return (T?)ReadValueFromReader(ref reader, typeof(T), maxDepth, 0);
}
finally
{
ReturnUtf8Buffer(buffer);
}
var isArrayJson = json.Length > 0 && json[0] == '[';
var isObjectJson = json.Length > 0 && json[0] == '{';
var isCollectionType = targetType.IsArray || IsGenericCollectionType(targetType);
var isDictType = IsDictionaryType(targetType, out _, out _);
if (isArrayJson && !isCollectionType && !isDictType && targetType != typeof(object))
throw new AcJsonDeserializationException($"JSON is an array but target type '{targetType.Name}' is not a collection type.", json, targetType);
if (isObjectJson && isCollectionType && !isDictType)
throw new AcJsonDeserializationException($"JSON is an object but target type '{targetType.Name}' is a collection type (not dictionary).", json, targetType);
}
#endregion
#region Core Reading Methods
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? ReadValue(JsonElement element, Type targetType, DeserializationContext context, int depth)
private static object? DeserializeWithUtf8ReaderNonGeneric(string json, Type targetType, byte maxDepth)
{
return element.ValueKind switch
var (buffer, length) = GetUtf8Bytes(json);
try
{
JsonValueKind.Object => ReadObject(element, targetType, context, depth),
JsonValueKind.Array => ReadArray(element, targetType, context, depth),
JsonValueKind.Null => null,
_ => ReadPrimitive(element, targetType, element.ValueKind)
var reader = new Utf8JsonReader(buffer.AsSpan(0, length), new JsonReaderOptions { MaxDepth = maxDepth });
if (!reader.Read()) return null;
return ReadValueFromReader(ref reader, targetType, maxDepth, 0);
}
finally
{
ReturnUtf8Buffer(buffer);
}
}
private static object? ReadValueFromReader(ref Utf8JsonReader reader, Type targetType, byte maxDepth, int depth)
{
switch (reader.TokenType)
{
case JsonTokenType.Null:
return null;
case JsonTokenType.True:
return true;
case JsonTokenType.False:
return false;
case JsonTokenType.Number:
return ReadNumberFromReader(ref reader, targetType);
case JsonTokenType.String:
return ReadStringFromReader(ref reader, targetType);
case JsonTokenType.StartObject:
return ReadObjectFromReader(ref reader, targetType, maxDepth, depth);
case JsonTokenType.StartArray:
return ReadArrayFromReader(ref reader, targetType, maxDepth, depth);
default:
return null;
}
}
/// <summary>
/// Read number value using cached PropertySetterInfo (avoids Nullable.GetUnderlyingType + Type.GetTypeCode calls).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? ReadNumberFromReaderCached(ref Utf8JsonReader reader, PropertySetterInfo propInfo)
{
return propInfo.PropertyTypeCode switch
{
TypeCode.Int32 => reader.GetInt32(),
TypeCode.Int64 => reader.GetInt64(),
TypeCode.Double => reader.GetDouble(),
TypeCode.Decimal => reader.GetDecimal(),
TypeCode.Single => reader.GetSingle(),
TypeCode.Byte => reader.GetByte(),
TypeCode.Int16 => reader.GetInt16(),
TypeCode.UInt16 => reader.GetUInt16(),
TypeCode.UInt32 => reader.GetUInt32(),
TypeCode.UInt64 => reader.GetUInt64(),
TypeCode.SByte => reader.GetSByte(),
_ => propInfo.UnderlyingType.IsEnum ? Enum.ToObject(propInfo.UnderlyingType, reader.GetInt32()) : reader.GetDouble()
};
}
/// <summary>
/// Read string value using cached PropertySetterInfo (avoids Nullable.GetUnderlyingType calls).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? ReadStringFromReaderCached(ref Utf8JsonReader reader, PropertySetterInfo propInfo)
{
var type = propInfo.UnderlyingType;
if (ReferenceEquals(type, StringType)) return reader.GetString();
if (ReferenceEquals(type, DateTimeType)) return reader.GetDateTime();
if (ReferenceEquals(type, GuidType)) return reader.GetGuid();
if (ReferenceEquals(type, DateTimeOffsetType)) return reader.GetDateTimeOffset();
if (ReferenceEquals(type, TimeSpanType))
{
var str = reader.GetString();
return str != null ? TimeSpan.Parse(str, CultureInfo.InvariantCulture) : default(TimeSpan);
}
if (type.IsEnum)
{
var str = reader.GetString();
return str != null ? Enum.Parse(type, str) : null;
}
return reader.GetString();
}
/// <summary>
/// Read value from reader using cached PropertySetterInfo for faster type resolution.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? ReadValueFromReaderCached(ref Utf8JsonReader reader, PropertySetterInfo propInfo, byte maxDepth, int depth)
{
switch (reader.TokenType)
{
case JsonTokenType.Null:
return null;
case JsonTokenType.True:
return true;
case JsonTokenType.False:
return false;
case JsonTokenType.Number:
return ReadNumberFromReaderCached(ref reader, propInfo);
case JsonTokenType.String:
return ReadStringFromReaderCached(ref reader, propInfo);
case JsonTokenType.StartObject:
return ReadObjectFromReader(ref reader, propInfo.PropertyType, maxDepth, depth);
case JsonTokenType.StartArray:
return ReadArrayFromReader(ref reader, propInfo.PropertyType, maxDepth, depth);
default:
return null;
}
}
private static object? ReadNumberFromReader(ref Utf8JsonReader reader, Type targetType)
{
var type = Nullable.GetUnderlyingType(targetType) ?? targetType;
var typeCode = Type.GetTypeCode(type);
return typeCode switch
{
TypeCode.Int32 => reader.GetInt32(),
TypeCode.Int64 => reader.GetInt64(),
TypeCode.Double => reader.GetDouble(),
TypeCode.Decimal => reader.GetDecimal(),
TypeCode.Single => reader.GetSingle(),
TypeCode.Byte => reader.GetByte(),
TypeCode.Int16 => reader.GetInt16(),
TypeCode.UInt16 => reader.GetUInt16(),
TypeCode.UInt32 => reader.GetUInt32(),
TypeCode.UInt64 => reader.GetUInt64(),
TypeCode.SByte => reader.GetSByte(),
_ => type.IsEnum ? Enum.ToObject(type, reader.GetInt32()) : reader.GetDouble()
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? ReadObject(JsonElement element, Type targetType, DeserializationContext context, int depth)
private static object? ReadStringFromReader(ref Utf8JsonReader reader, Type targetType)
{
// Handle $ref even if reference handling is disabled (for compatibility)
if (context.UseReferenceHandling && element.TryGetProperty("$ref", out var refElement))
var type = Nullable.GetUnderlyingType(targetType) ?? targetType;
if (ReferenceEquals(type, StringType)) return reader.GetString();
if (ReferenceEquals(type, DateTimeType)) return reader.GetDateTime();
if (ReferenceEquals(type, GuidType)) return reader.GetGuid();
if (ReferenceEquals(type, DateTimeOffsetType)) return reader.GetDateTimeOffset();
if (ReferenceEquals(type, TimeSpanType))
{
var refId = refElement.GetString()!;
return context.TryGetReferencedObject(refId, out var refObj) ? refObj : new DeferredReference(refId, targetType);
var str = reader.GetString();
return str != null ? TimeSpan.Parse(str, CultureInfo.InvariantCulture) : default(TimeSpan);
}
if (type.IsEnum)
{
var str = reader.GetString();
return str != null ? Enum.Parse(type, str) : null;
}
return reader.GetString();
}
private static object? ReadObjectFromReader(ref Utf8JsonReader reader, Type targetType, byte maxDepth, int depth)
{
if (depth > maxDepth)
{
reader.Skip();
return null;
}
// Check depth limit
if (depth > context.MaxDepth) return null;
if (IsDictionaryType(targetType, out var keyType, out var valueType))
return ReadDictionary(element, keyType!, valueType!, context, depth);
return ReadDictionaryFromReader(ref reader, keyType!, valueType!, maxDepth, depth);
var metadata = GetTypeMetadata(targetType);
object? instance;
if (metadata.CompiledConstructor != null)
instance = metadata.CompiledConstructor.Invoke();
else
var instance = metadata.CompiledConstructor?.Invoke();
if (instance == null)
{
try { instance = Activator.CreateInstance(targetType); }
catch (MissingMethodException ex)
@ -286,172 +489,310 @@ public static class AcJsonDeserializer
if (instance == null) return null;
if (context.UseReferenceHandling && element.TryGetProperty("$id", out var idElement))
context.RegisterObject(idElement.GetString()!, instance);
PopulateObjectInternal(element, instance, metadata, context, depth);
var propsDict = metadata.PropertySettersFrozen;
var nextDepth = depth + 1;
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
break;
if (reader.TokenType != JsonTokenType.PropertyName)
continue;
var propName = reader.GetString();
if (propName == null || !reader.Read())
continue;
if (!propsDict.TryGetValue(propName, out var propInfo))
{
reader.Skip();
continue;
}
// Use cached version for faster type resolution
var value = ReadValueFromReaderCached(ref reader, propInfo, maxDepth, nextDepth);
propInfo.SetValue(instance, value);
}
return instance;
}
private static void PopulateObjectInternal(JsonElement element, object target, DeserializeTypeMetadata metadata, DeserializationContext context, int depth)
/// <summary>
/// Read object from reader when we're already at StartObject.
/// </summary>
private static object? ReadObjectFromReaderAtStart(ref Utf8JsonReader reader, Type targetType, DeserializeTypeMetadata metadata, byte maxDepth, int depth)
{
foreach (var jsonProp in element.EnumerateObject())
if (depth > maxDepth)
{
var propName = jsonProp.Name;
if (propName.Length > 0 && propName[0] == '$' && (propName == "$id" || propName == "$ref"))
continue;
if (!metadata.PropertySetters.TryGetValue(propName, out var propInfo)) continue;
var value = ReadValue(jsonProp.Value, propInfo.PropertyType, context, depth + 1);
if (value is DeferredReference deferred)
context.AddPropertyToResolve(target, propInfo, deferred.RefId);
else
propInfo.SetValue(target, value);
reader.Skip();
return null;
}
}
private static void PopulateObjectInternalMerge(JsonElement element, object target, DeserializeTypeMetadata metadata, DeserializationContext context, int depth)
{
foreach (var jsonProp in element.EnumerateObject())
var instance = metadata.CompiledConstructor?.Invoke();
if (instance == null)
{
var propName = jsonProp.Name;
if (propName.Length > 0 && propName[0] == '$' && (propName == "$id" || propName == "$ref"))
continue;
if (!metadata.PropertySetters.TryGetValue(propName, out var propInfo)) continue;
var propValue = jsonProp.Value;
var propValueKind = propValue.ValueKind;
// Check depth limit for nested objects/collections
if (depth + 1 > context.MaxDepth)
try { instance = Activator.CreateInstance(targetType); }
catch (MissingMethodException ex)
{
// At max depth, only set primitives
if (propValueKind != JsonValueKind.Object && propValueKind != JsonValueKind.Array)
{
var primitiveValue = ReadPrimitive(propValue, propInfo.PropertyType, propValueKind);
propInfo.SetValue(target, primitiveValue);
}
continue;
throw new AcJsonDeserializationException(
$"Cannot deserialize type '{targetType.FullName}' because it does not have a parameterless constructor.",
null, targetType, ex);
}
if (propInfo is { IsCollection: true, ElementIsIId: true } && propValueKind == JsonValueKind.Array)
{
var existingCollection = propInfo.GetValue(target);
if (existingCollection != null)
{
MergeIIdCollection(propValue, existingCollection, propInfo, context, depth);
continue;
}
}
if (propValueKind == JsonValueKind.Object)
{
if (context.UseReferenceHandling && propValue.TryGetProperty("$ref", out _))
{
var value = ReadValue(propValue, propInfo.PropertyType, context, depth + 1);
if (value is DeferredReference deferred)
context.AddPropertyToResolve(target, propInfo, deferred.RefId);
else
propInfo.SetValue(target, value);
continue;
}
if (!propInfo.PropertyType.IsPrimitive && propInfo.PropertyType != StringType)
{
var existingObj = propInfo.GetValue(target);
if (existingObj != null)
{
var nestedMetadata = GetTypeMetadata(propInfo.PropertyType);
PopulateObjectInternalMerge(propValue, existingObj, nestedMetadata, context, depth + 1);
continue;
}
}
}
var value2 = ReadValue(propValue, propInfo.PropertyType, context, depth + 1);
if (value2 is DeferredReference deferred2)
context.AddPropertyToResolve(target, propInfo, deferred2.RefId);
else
propInfo.SetValue(target, value2);
}
}
private static void PopulateList(JsonElement arrayElement, IList targetList, Type listType, DeserializationContext context, int depth)
{
// Check depth limit
if (depth > context.MaxDepth) return;
var elementType = GetCollectionElementType(listType);
if (elementType == null) return;
var acObservable = targetList as IAcObservableCollection;
acObservable?.BeginUpdate();
if (instance == null) return null;
try
var propsDict = metadata.PropertySettersFrozen;
var nextDepth = depth + 1;
while (reader.Read())
{
targetList.Clear();
if (reader.TokenType == JsonTokenType.EndObject)
break;
foreach (var item in arrayElement.EnumerateArray())
if (reader.TokenType != JsonTokenType.PropertyName)
continue;
var propName = reader.GetString();
if (propName == null || !reader.Read())
continue;
if (!propsDict.TryGetValue(propName, out var propInfo))
{
var value = ReadValue(item, elementType, context, depth + 1);
if (value != null)
targetList.Add(value);
reader.Skip();
continue;
}
// Use cached version for faster type resolution
var value = ReadValueFromReaderCached(ref reader, propInfo, maxDepth, nextDepth);
propInfo.SetValue(instance, value);
}
finally { acObservable?.EndUpdate(); }
return instance;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? ReadArray(JsonElement element, Type targetType, DeserializationContext context, int depth)
private static object? ReadArrayFromReader(ref Utf8JsonReader reader, Type targetType, byte maxDepth, int depth)
{
// Check depth limit
if (depth > context.MaxDepth) return null;
if (depth > maxDepth)
{
reader.Skip();
return null;
}
var elementType = GetCollectionElementType(targetType);
if (elementType == null) return null;
var nextDepth = depth + 1;
var list = GetOrCreateListFactory(elementType)();
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndArray)
break;
var value = ReadValueFromReader(ref reader, elementType, maxDepth, nextDepth);
if (value != null)
list.Add(value);
}
if (targetType.IsArray)
{
var list = GetOrCreateListFactory(elementType)();
foreach (var item in element.EnumerateArray())
list.Add(ReadValue(item, elementType, context, depth + 1));
var array = Array.CreateInstance(elementType, list.Count);
list.CopyTo(array, 0);
return array;
}
IList? targetList = null;
try
{
var instance = Activator.CreateInstance(targetType);
if (instance is IList list) targetList = list;
}
catch { /* Fallback to List<T> */ }
targetList ??= GetOrCreateListFactory(elementType)();
var acObservable = targetList as IAcObservableCollection;
acObservable?.BeginUpdate();
try
{
foreach (var item in element.EnumerateArray())
targetList.Add(ReadValue(item, elementType, context, depth + 1));
}
finally { acObservable?.EndUpdate(); }
return targetList;
return list;
}
private static void MergeIIdCollection(JsonElement arrayElement, object existingCollection, PropertySetterInfo propInfo, DeserializationContext context, int depth)
private static object ReadDictionaryFromReader(ref Utf8JsonReader reader, Type keyType, Type valueType, byte maxDepth, int depth)
{
var dictType = DictionaryGenericType.MakeGenericType(keyType, valueType);
var dict = (IDictionary)Activator.CreateInstance(dictType)!;
var nextDepth = depth + 1;
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
break;
if (reader.TokenType != JsonTokenType.PropertyName)
continue;
var keyStr = reader.GetString();
if (keyStr == null || !reader.Read())
continue;
object key;
if (ReferenceEquals(keyType, StringType)) key = keyStr;
else if (ReferenceEquals(keyType, IntType)) key = int.Parse(keyStr, CultureInfo.InvariantCulture);
else if (ReferenceEquals(keyType, LongType)) key = long.Parse(keyStr, CultureInfo.InvariantCulture);
else if (ReferenceEquals(keyType, GuidType)) key = Guid.Parse(keyStr);
else if (keyType.IsEnum) key = Enum.Parse(keyType, keyStr);
else key = Convert.ChangeType(keyStr, keyType, CultureInfo.InvariantCulture);
var value = ReadValueFromReader(ref reader, valueType, maxDepth, nextDepth);
dict.Add(key, value);
}
return dict;
}
/// <summary>
/// Populate object using Utf8JsonReader streaming (no DOM allocation).
/// </summary>
private static void PopulateObjectWithUtf8Reader(string json, object target, DeserializeTypeMetadata metadata, byte maxDepth)
{
var (buffer, length) = GetUtf8Bytes(json);
try
{
var reader = new Utf8JsonReader(buffer.AsSpan(0, length), new JsonReaderOptions { MaxDepth = maxDepth });
if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
return;
PopulateObjectMergeFromReader(ref reader, target, metadata, maxDepth, 0);
}
finally
{
ReturnUtf8Buffer(buffer);
}
}
/// <summary>
/// Populate list using Utf8JsonReader streaming (no DOM allocation).
/// </summary>
private static void PopulateListWithUtf8Reader(string json, IList targetList, in Type listType, byte maxDepth)
{
var (buffer, length) = GetUtf8Bytes(json);
try
{
var reader = new Utf8JsonReader(buffer.AsSpan(0, length), new JsonReaderOptions { MaxDepth = maxDepth });
if (!reader.Read() || reader.TokenType != JsonTokenType.StartArray)
return;
var elementType = GetCollectionElementType(listType);
if (elementType == null) return;
var acObservable = targetList as IAcObservableCollection;
acObservable?.BeginUpdate();
try
{
targetList.Clear();
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndArray)
break;
var value = ReadValueFromReader(ref reader, elementType, maxDepth, 1);
if (value != null)
targetList.Add(value);
}
}
finally { acObservable?.EndUpdate(); }
}
finally
{
ReturnUtf8Buffer(buffer);
}
}
/// <summary>
/// Populate object with merge semantics from Utf8JsonReader.
/// </summary>
private static void PopulateObjectMergeFromReader(ref Utf8JsonReader reader, object target, DeserializeTypeMetadata metadata, byte maxDepth, int depth)
{
var propsDict = metadata.PropertySettersFrozen;
var nextDepth = depth + 1;
var maxDepthReached = nextDepth > maxDepth;
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
break;
if (reader.TokenType != JsonTokenType.PropertyName)
continue;
var propName = reader.GetString();
if (propName == null || !reader.Read())
continue;
if (!propsDict.TryGetValue(propName, out var propInfo))
{
reader.Skip();
continue;
}
var tokenType = reader.TokenType;
if (maxDepthReached)
{
if (tokenType != JsonTokenType.StartObject && tokenType != JsonTokenType.StartArray)
{
// Use cached version for faster primitive reading
var primitiveValue = ReadPrimitiveFromReaderCached(ref reader, propInfo);
propInfo.SetValue(target, primitiveValue);
}
else
{
reader.Skip();
}
continue;
}
// Handle IId collection merge
if (propInfo.IsIIdCollection && tokenType == JsonTokenType.StartArray)
{
var existingCollection = propInfo.GetValue(target);
if (existingCollection != null)
{
MergeIIdCollectionFromReader(ref reader, existingCollection, propInfo, maxDepth, depth);
continue;
}
}
// Handle nested objects - merge into existing
if (tokenType == JsonTokenType.StartObject && !propInfo.PropertyType.IsPrimitive && !ReferenceEquals(propInfo.PropertyType, StringType))
{
var existingObj = propInfo.GetValue(target);
if (existingObj != null)
{
var nestedMetadata = GetTypeMetadata(propInfo.PropertyType);
PopulateObjectMergeFromReader(ref reader, existingObj, nestedMetadata, maxDepth, nextDepth);
continue;
}
}
// Use cached version for faster type resolution
var value = ReadValueFromReaderCached(ref reader, propInfo, maxDepth, nextDepth);
propInfo.SetValue(target, value);
}
}
/// <summary>
/// Read primitive value from Utf8JsonReader using cached PropertySetterInfo.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? ReadPrimitiveFromReaderCached(ref Utf8JsonReader reader, PropertySetterInfo propInfo)
{
var tokenType = reader.TokenType;
if (tokenType == JsonTokenType.Null) return null;
if (tokenType == JsonTokenType.True) return true;
if (tokenType == JsonTokenType.False) return false;
if (tokenType == JsonTokenType.Number) return ReadNumberFromReaderCached(ref reader, propInfo);
if (tokenType == JsonTokenType.String) return ReadStringFromReaderCached(ref reader, propInfo);
return null;
}
/// <summary>
/// Merge IId collection from Utf8JsonReader streaming.
/// </summary>
private static void MergeIIdCollectionFromReader(ref Utf8JsonReader reader, object existingCollection, PropertySetterInfo propInfo, byte maxDepth, int depth)
{
var elementType = propInfo.ElementType!;
var idGetter = propInfo.ElementIdGetter!;
@ -481,6 +822,364 @@ public static class AcJsonDeserializer
}
}
var nextDepth = depth + 1;
var elementMetadata = GetTypeMetadata(elementType);
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndArray)
break;
if (reader.TokenType != JsonTokenType.StartObject)
{
var primitiveValue = ReadValueFromReader(ref reader, elementType, maxDepth, nextDepth);
if (primitiveValue != null) existingList.Add(primitiveValue);
continue;
}
// For objects, we need to read the full object to check the Id
var newItem = ReadObjectFromReaderAtStart(ref reader, elementType, elementMetadata, maxDepth, nextDepth);
if (newItem == null) continue;
// Check if this item already exists by Id
var itemId = propInfo.ElementIdGetter?.Invoke(newItem);
if (itemId != null && !IsDefaultValue(itemId, idType) && existingById != null)
{
if (existingById.TryGetValue(itemId, out var existingItem))
{
// Copy properties from newItem to existingItem
CopyProperties(newItem, existingItem, elementMetadata);
continue;
}
}
existingList.Add(newItem);
}
}
finally { acObservable?.EndUpdate(); }
}
/// <summary>
/// Copy properties from source to target using metadata.
/// </summary>
private static void CopyProperties(object source, object target, DeserializeTypeMetadata metadata)
{
foreach (var prop in metadata.PropertySettersFrozen.Values)
{
var value = prop.GetValue(source);
if (value != null)
prop.SetValue(target, value);
}
}
/// <summary>
/// Read primitive value from Utf8JsonReader.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? ReadPrimitiveFromReader(ref Utf8JsonReader reader, Type targetType)
{
var tokenType = reader.TokenType;
if (tokenType == JsonTokenType.Null) return null;
if (tokenType == JsonTokenType.True) return true;
if (tokenType == JsonTokenType.False) return false;
if (tokenType == JsonTokenType.Number) return ReadNumberFromReader(ref reader, targetType);
if (tokenType == JsonTokenType.String) return ReadStringFromReader(ref reader, targetType);
return null;
}
#endregion
#region Validation
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ValidateJson(string json, in Type targetType)
{
if (json.Length < 2) return;
var firstChar = json[0];
// Quick check for double-serialized JSON
if (firstChar == '"' && json[^1] == '"')
{
var inner = json.AsSpan(1, json.Length - 2);
if (inner.Contains("\\\"", StringComparison.Ordinal) &&
(inner.Contains("{", StringComparison.Ordinal) || inner.Contains("[", StringComparison.Ordinal)))
throw new AcJsonDeserializationException(
$"Detected double-serialized JSON string. Target type: {targetType.Name}.",
json, targetType);
}
var isArrayJson = firstChar == '[';
var isObjectJson = firstChar == '{';
if (isArrayJson)
{
var isCollectionType = targetType.IsArray || IsGenericCollectionType(targetType);
var isDictType = IsDictionaryType(targetType, out _, out _);
if (!isCollectionType && !isDictType && targetType != typeof(object))
throw new AcJsonDeserializationException($"JSON is an array but target type '{targetType.Name}' is not a collection type.", json, targetType);
}
else if (isObjectJson)
{
var isCollectionType = targetType.IsArray || IsGenericCollectionType(targetType);
var isDictType = IsDictionaryType(targetType, out _, out _);
if (isCollectionType && !isDictType)
throw new AcJsonDeserializationException($"JSON is an object but target type '{targetType.Name}' is a collection type (not dictionary).", json, targetType);
}
}
#endregion
#region With Reference Handling (JsonElement Path)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? ReadValue(in JsonElement element, in Type targetType, DeserializationContext context, int depth)
{
var kind = element.ValueKind;
if (kind == JsonValueKind.Object) return ReadObject(element, targetType, context, depth);
if (kind == JsonValueKind.Array) return ReadArray(element, targetType, context, depth);
if (kind == JsonValueKind.Null || kind == JsonValueKind.Undefined) return null;
return ReadPrimitive(element, targetType, kind);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? ReadObject(in JsonElement element, in Type targetType, DeserializationContext context, int depth)
{
// Check for $ref first
if (element.TryGetProperty(RefPropertyUtf8, out var refElement))
{
var refId = refElement.GetString()!;
return context.TryGetReferencedObject(refId, out var refObj) ? refObj : new DeferredReference(refId, targetType);
}
if (depth > context.MaxDepth) return null;
if (IsDictionaryType(targetType, out var keyType, out var valueType))
return ReadDictionary(element, keyType!, valueType!, context, depth);
var metadata = GetTypeMetadata(targetType);
var instance = metadata.CompiledConstructor?.Invoke();
if (instance == null)
{
try { instance = Activator.CreateInstance(targetType); }
catch (MissingMethodException ex)
{
throw new AcJsonDeserializationException(
$"Cannot deserialize type '{targetType.FullName}' because it does not have a parameterless constructor.",
null, targetType, ex);
}
}
if (instance == null) return null;
// Check for $id and register
if (element.TryGetProperty(IdPropertyUtf8, out var idElement))
context.RegisterObject(idElement.GetString()!, instance);
PopulateObjectInternal(element, instance, metadata, context, depth);
return instance;
}
private static void PopulateObjectInternal(in JsonElement element, object target, DeserializeTypeMetadata metadata, DeserializationContext context, int depth)
{
var propsDict = metadata.PropertySettersFrozen;
var nextDepth = depth + 1;
foreach (var jsonProp in element.EnumerateObject())
{
var propName = jsonProp.Name;
// Skip $ properties
if (propName.Length > 0 && propName[0] == '$') continue;
if (!propsDict.TryGetValue(propName, out var propInfo)) continue;
var value = ReadValue(jsonProp.Value, propInfo.PropertyType, context, nextDepth);
if (value is DeferredReference deferred)
context.AddPropertyToResolve(target, propInfo, deferred.RefId);
else
propInfo.SetValue(target, value);
}
}
private static void PopulateObjectInternalMerge(in JsonElement element, object target, DeserializeTypeMetadata metadata, DeserializationContext context, int depth)
{
var propsDict = metadata.PropertySettersFrozen;
var nextDepth = depth + 1;
var maxDepthReached = nextDepth > context.MaxDepth;
foreach (var jsonProp in element.EnumerateObject())
{
var propName = jsonProp.Name;
// Skip $ properties
if (propName.Length > 0 && propName[0] == '$') continue;
if (!propsDict.TryGetValue(propName, out var propInfo)) continue;
var propValue = jsonProp.Value;
var propValueKind = propValue.ValueKind;
if (maxDepthReached)
{
if (propValueKind != JsonValueKind.Object && propValueKind != JsonValueKind.Array)
{
var primitiveValue = ReadPrimitive(propValue, propInfo.PropertyType, propValueKind);
propInfo.SetValue(target, primitiveValue);
}
continue;
}
// Handle IId collection merge
if (propInfo.IsIIdCollection && propValueKind == JsonValueKind.Array)
{
var existingCollection = propInfo.GetValue(target);
if (existingCollection != null)
{
MergeIIdCollection(propValue, existingCollection, propInfo, context, depth);
continue;
}
}
// Handle nested objects
if (propValueKind == JsonValueKind.Object)
{
// Check for $ref
if (propValue.TryGetProperty(RefPropertyUtf8, out _))
{
var value = ReadValue(propValue, propInfo.PropertyType, context, nextDepth);
if (value is DeferredReference deferred)
context.AddPropertyToResolve(target, propInfo, deferred.RefId);
else
propInfo.SetValue(target, value);
continue;
}
// Merge into existing object
if (!propInfo.PropertyType.IsPrimitive && !ReferenceEquals(propInfo.PropertyType, StringType))
{
var existingObj = propInfo.GetValue(target);
if (existingObj != null)
{
var nestedMetadata = GetTypeMetadata(propInfo.PropertyType);
PopulateObjectInternalMerge(propValue, existingObj, nestedMetadata, context, nextDepth);
continue;
}
}
}
var value2 = ReadValue(propValue, propInfo.PropertyType, context, nextDepth);
if (value2 is DeferredReference deferred2)
context.AddPropertyToResolve(target, propInfo, deferred2.RefId);
else
propInfo.SetValue(target, value2);
}
}
private static void PopulateList(in JsonElement arrayElement, IList targetList, in Type listType, DeserializationContext context, int depth)
{
if (depth > context.MaxDepth) return;
var elementType = GetCollectionElementType(listType);
if (elementType == null) return;
var acObservable = targetList as IAcObservableCollection;
acObservable?.BeginUpdate();
try
{
targetList.Clear();
var nextDepth = depth + 1;
foreach (var item in arrayElement.EnumerateArray())
{
var value = ReadValue(item, elementType, context, nextDepth);
if (value != null)
targetList.Add(value);
}
}
finally { acObservable?.EndUpdate(); }
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? ReadArray(in JsonElement element, in Type targetType, DeserializationContext context, int depth)
{
if (depth > context.MaxDepth) return null;
var elementType = GetCollectionElementType(targetType);
if (elementType == null) return null;
var nextDepth = depth + 1;
if (targetType.IsArray)
{
var list = GetOrCreateListFactory(elementType)();
foreach (var item in element.EnumerateArray())
list.Add(ReadValue(item, elementType, context, nextDepth));
var array = Array.CreateInstance(elementType, list.Count);
list.CopyTo(array, 0);
return array;
}
IList? targetList = null;
try
{
var instance = Activator.CreateInstance(targetType);
if (instance is IList list) targetList = list;
}
catch { /* Fallback to List<T> */ }
targetList ??= GetOrCreateListFactory(elementType)();
var acObservable = targetList as IAcObservableCollection;
acObservable?.BeginUpdate();
try
{
foreach (var item in element.EnumerateArray())
targetList.Add(ReadValue(item, elementType, context, nextDepth));
}
finally { acObservable?.EndUpdate(); }
return targetList;
}
private static void MergeIIdCollection(in JsonElement arrayElement, object existingCollection, PropertySetterInfo propInfo, DeserializationContext context, int depth)
{
var elementType = propInfo.ElementType!;
var idGetter = propInfo.ElementIdGetter!;
var idType = propInfo.ElementIdType!;
var existingList = (IList)existingCollection;
var count = existingList.Count;
var acObservable = existingList as IAcObservableCollection;
acObservable?.BeginUpdate();
try
{
Dictionary<object, object>? existingById = null;
if (count > 0)
{
existingById = new Dictionary<object, object>(count);
for (var i = 0; i < count; i++)
{
var item = existingList[i];
if (item != null)
{
var id = idGetter(item);
if (id != null && !IsDefaultValue(id, idType))
existingById[id] = item;
}
}
}
var nextDepth = depth + 1;
foreach (var jsonItem in arrayElement.EnumerateArray())
{
if (jsonItem.ValueKind != JsonValueKind.Object) continue;
@ -494,38 +1193,70 @@ public static class AcJsonDeserializer
if (existingById.TryGetValue(itemId, out var existingItem))
{
var itemMetadata = GetTypeMetadata(elementType);
PopulateObjectInternalMerge(jsonItem, existingItem, itemMetadata, context, depth + 1);
PopulateObjectInternalMerge(jsonItem, existingItem, itemMetadata, context, nextDepth);
continue;
}
}
var newItem = ReadValue(jsonItem, elementType, context, depth + 1);
var newItem = ReadValue(jsonItem, elementType, context, nextDepth);
if (newItem != null) existingList.Add(newItem);
}
}
finally { acObservable?.EndUpdate(); }
}
private static object ReadDictionary(in JsonElement element, in Type keyType, in Type valueType, DeserializationContext context, int depth)
{
var dictType = DictionaryGenericType.MakeGenericType(keyType, valueType);
var dict = (IDictionary)Activator.CreateInstance(dictType)!;
var nextDepth = depth + 1;
foreach (var prop in element.EnumerateObject())
{
var name = prop.Name;
if (name.Length > 0 && name[0] == '$') continue;
object key;
if (ReferenceEquals(keyType, StringType)) key = name;
else if (ReferenceEquals(keyType, IntType)) key = int.Parse(name, CultureInfo.InvariantCulture);
else if (ReferenceEquals(keyType, LongType)) key = long.Parse(name, CultureInfo.InvariantCulture);
else if (ReferenceEquals(keyType, GuidType)) key = Guid.Parse(name);
else if (keyType.IsEnum) key = Enum.Parse(keyType, name);
else key = Convert.ChangeType(name, keyType, CultureInfo.InvariantCulture);
dict.Add(key, ReadValue(prop.Value, valueType, context, nextDepth));
}
return dict;
}
#endregion
#region Primitive Reading
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? ReadPrimitive(JsonElement element, Type targetType, JsonValueKind valueKind)
private static object? ReadPrimitive(in JsonElement element, in Type targetType, JsonValueKind valueKind)
{
var type = Nullable.GetUnderlyingType(targetType) ?? targetType;
if (valueKind == JsonValueKind.Number)
{
if (ReferenceEquals(type, IntType)) return element.GetInt32();
if (ReferenceEquals(type, LongType)) return element.GetInt64();
if (ReferenceEquals(type, DoubleType)) return element.GetDouble();
if (ReferenceEquals(type, DecimalType)) return element.GetDecimal();
if (ReferenceEquals(type, FloatType)) return element.GetSingle();
if (type == ByteType) return element.GetByte();
if (type == ShortType) return element.GetInt16();
if (type == UShortType) return element.GetUInt16();
if (type == UIntType) return element.GetUInt32();
if (type == ULongType) return element.GetUInt64();
if (type == SByteType) return element.GetSByte();
if (type.IsEnum) return Enum.ToObject(type, element.GetInt32());
return null;
var typeCode = Type.GetTypeCode(type);
return typeCode switch
{
TypeCode.Int32 => element.GetInt32(),
TypeCode.Int64 => element.GetInt64(),
TypeCode.Double => element.GetDouble(),
TypeCode.Decimal => element.GetDecimal(),
TypeCode.Single => element.GetSingle(),
TypeCode.Byte => element.GetByte(),
TypeCode.Int16 => element.GetInt16(),
TypeCode.UInt16 => element.GetUInt16(),
TypeCode.UInt32 => element.GetUInt32(),
TypeCode.UInt64 => element.GetUInt64(),
TypeCode.SByte => element.GetSByte(),
_ => type.IsEnum ? Enum.ToObject(type, element.GetInt32()) : null
};
}
if (valueKind == JsonValueKind.String)
@ -533,8 +1264,8 @@ public static class AcJsonDeserializer
if (ReferenceEquals(type, StringType)) return element.GetString();
if (ReferenceEquals(type, DateTimeType)) return element.GetDateTime();
if (ReferenceEquals(type, GuidType)) return element.GetGuid();
if (type == DateTimeOffsetType) return element.GetDateTimeOffset();
if (type == TimeSpanType) return TimeSpan.Parse(element.GetString()!, CultureInfo.InvariantCulture);
if (ReferenceEquals(type, DateTimeOffsetType)) return element.GetDateTimeOffset();
if (ReferenceEquals(type, TimeSpanType)) return TimeSpan.Parse(element.GetString()!, CultureInfo.InvariantCulture);
if (type.IsEnum) return Enum.Parse(type, element.GetString()!);
return null;
}
@ -545,56 +1276,62 @@ public static class AcJsonDeserializer
return null;
}
private static object ReadDictionary(JsonElement element, Type keyType, Type valueType, DeserializationContext context, int depth)
{
var dictType = DictionaryGenericType.MakeGenericType(keyType, valueType);
var dict = (IDictionary)Activator.CreateInstance(dictType)!;
foreach (var prop in element.EnumerateObject())
{
if (prop.Name.StartsWith('$')) continue;
object key;
if (keyType == StringType) key = prop.Name;
else if (keyType == IntType) key = int.Parse(prop.Name, CultureInfo.InvariantCulture);
else if (keyType == LongType) key = long.Parse(prop.Name, CultureInfo.InvariantCulture);
else if (keyType == GuidType) key = Guid.Parse(prop.Name);
else if (keyType.IsEnum) key = Enum.Parse(keyType, prop.Name);
else key = Convert.ChangeType(prop.Name, keyType, CultureInfo.InvariantCulture);
dict.Add(key, ReadValue(prop.Value, valueType, context, depth + 1));
}
return dict;
}
#endregion
#region Primitive Deserialization
private static bool TryDeserializePrimitive(string json, Type targetType, out object? result)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool TryDeserializePrimitiveFast(string json, in Type targetType, out object? result)
{
var type = Nullable.GetUnderlyingType(targetType) ?? targetType;
if (ReferenceEquals(type, StringType))
// Handle enums first
if (type.IsEnum)
{
using var doc = JsonDocument.Parse(json);
result = doc.RootElement.GetString();
if (json.Length > 0 && json[0] == '"')
{
using var doc = JsonDocument.Parse(json);
result = Enum.Parse(type, doc.RootElement.GetString()!);
}
else result = Enum.ToObject(type, int.Parse(json, CultureInfo.InvariantCulture));
return true;
}
if (ReferenceEquals(type, IntType)) { result = int.Parse(json, CultureInfo.InvariantCulture); return true; }
if (ReferenceEquals(type, LongType)) { result = long.Parse(json, CultureInfo.InvariantCulture); return true; }
if (ReferenceEquals(type, BoolType)) { result = json == "true"; return true; }
if (ReferenceEquals(type, DoubleType)) { result = double.Parse(json, CultureInfo.InvariantCulture); return true; }
if (ReferenceEquals(type, DecimalType)) { result = decimal.Parse(json, CultureInfo.InvariantCulture); return true; }
if (ReferenceEquals(type, FloatType)) { result = float.Parse(json, CultureInfo.InvariantCulture); return true; }
var typeCode = Type.GetTypeCode(type);
if (ReferenceEquals(type, DateTimeType))
switch (typeCode)
{
using var doc = JsonDocument.Parse(json);
result = doc.RootElement.GetDateTime();
return true;
case TypeCode.Int32: result = int.Parse(json, CultureInfo.InvariantCulture); return true;
case TypeCode.Int64: result = long.Parse(json, CultureInfo.InvariantCulture); return true;
case TypeCode.Boolean: result = json == "true"; return true;
case TypeCode.Double: result = double.Parse(json, CultureInfo.InvariantCulture); return true;
case TypeCode.Decimal: result = decimal.Parse(json, CultureInfo.InvariantCulture); return true;
case TypeCode.Single: result = float.Parse(json, CultureInfo.InvariantCulture); return true;
case TypeCode.String:
using (var doc = JsonDocument.Parse(json))
{
result = doc.RootElement.GetString();
return true;
}
case TypeCode.DateTime:
using (var doc = JsonDocument.Parse(json))
{
result = doc.RootElement.GetDateTime();
return true;
}
case TypeCode.Byte: result = byte.Parse(json, CultureInfo.InvariantCulture); return true;
case TypeCode.Int16: result = short.Parse(json, CultureInfo.InvariantCulture); return true;
case TypeCode.UInt16: result = ushort.Parse(json, CultureInfo.InvariantCulture); return true;
case TypeCode.UInt32: result = uint.Parse(json, CultureInfo.InvariantCulture); return true;
case TypeCode.UInt64: result = ulong.Parse(json, CultureInfo.InvariantCulture); return true;
case TypeCode.SByte: result = sbyte.Parse(json, CultureInfo.InvariantCulture); return true;
case TypeCode.Char:
using (var doc = JsonDocument.Parse(json))
{
var s = doc.RootElement.GetString();
result = s?.Length > 0 ? s[0] : '\0';
return true;
}
}
if (ReferenceEquals(type, GuidType))
@ -604,32 +1341,17 @@ public static class AcJsonDeserializer
return true;
}
if (type == DateTimeOffsetType) { using var doc = JsonDocument.Parse(json); result = doc.RootElement.GetDateTimeOffset(); return true; }
if (type == TimeSpanType) { using var doc = JsonDocument.Parse(json); result = TimeSpan.Parse(doc.RootElement.GetString()!, CultureInfo.InvariantCulture); return true; }
if (type.IsEnum)
if (ReferenceEquals(type, DateTimeOffsetType))
{
if (json.StartsWith('"'))
{
using var doc = JsonDocument.Parse(json);
result = Enum.Parse(type, doc.RootElement.GetString()!);
}
else result = Enum.ToObject(type, int.Parse(json, CultureInfo.InvariantCulture));
using var doc = JsonDocument.Parse(json);
result = doc.RootElement.GetDateTimeOffset();
return true;
}
if (type == ByteType) { result = byte.Parse(json, CultureInfo.InvariantCulture); return true; }
if (type == ShortType) { result = short.Parse(json, CultureInfo.InvariantCulture); return true; }
if (type == UShortType) { result = ushort.Parse(json, CultureInfo.InvariantCulture); return true; }
if (type == UIntType) { result = uint.Parse(json, CultureInfo.InvariantCulture); return true; }
if (type == ULongType) { result = ulong.Parse(json, CultureInfo.InvariantCulture); return true; }
if (type == SByteType) { result = sbyte.Parse(json, CultureInfo.InvariantCulture); return true; }
if (type == CharType)
if (ReferenceEquals(type, TimeSpanType))
{
using var doc = JsonDocument.Parse(json);
var s = doc.RootElement.GetString();
result = s?.Length > 0 ? s[0] : '\0';
result = TimeSpan.Parse(doc.RootElement.GetString()!, CultureInfo.InvariantCulture);
return true;
}
@ -639,15 +1361,46 @@ public static class AcJsonDeserializer
#endregion
#region Context Pool
private static class DeserializationContextPool
{
private static readonly ConcurrentQueue<DeserializationContext> Pool = new();
private const int MaxPoolSize = 16;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static DeserializationContext Get(in AcJsonSerializerOptions options)
{
if (Pool.TryDequeue(out var context))
{
context.Reset(options);
return context;
}
return new DeserializationContext(options);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Return(DeserializationContext context)
{
if (Pool.Count < MaxPoolSize)
{
context.Clear();
Pool.Enqueue(context);
}
}
}
#endregion
#region Type Metadata
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static DeserializeTypeMetadata GetTypeMetadata(Type type)
private static DeserializeTypeMetadata GetTypeMetadata(in Type type)
=> TypeMetadataCache.GetOrAdd(type, static t => new DeserializeTypeMetadata(t));
private sealed class DeserializeTypeMetadata
{
public Dictionary<string, PropertySetterInfo> PropertySetters { get; }
public FrozenDictionary<string, PropertySetterInfo> PropertySettersFrozen { get; }
public Func<object>? CompiledConstructor { get; }
public DeserializeTypeMetadata(Type type)
@ -670,20 +1423,28 @@ public static class AcJsonDeserializer
propsList.Add(p);
}
PropertySetters = new Dictionary<string, PropertySetterInfo>(propsList.Count, StringComparer.OrdinalIgnoreCase);
var propertySetters = new Dictionary<string, PropertySetterInfo>(propsList.Count, StringComparer.OrdinalIgnoreCase);
foreach (var prop in propsList)
PropertySetters[prop.Name] = new PropertySetterInfo(prop, type);
{
propertySetters[prop.Name] = new PropertySetterInfo(prop, type);
}
// Create frozen dictionary for faster lookup in hot paths
PropertySettersFrozen = propertySetters.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
}
}
private sealed class PropertySetterInfo
{
public Type PropertyType { get; }
public bool IsCollection { get; }
public bool ElementIsIId { get; }
public Type? ElementType { get; }
public Type? ElementIdType { get; }
public Func<object, object?>? ElementIdGetter { get; }
public readonly Type PropertyType;
public readonly Type UnderlyingType; // Cached: Nullable.GetUnderlyingType(PropertyType) ?? PropertyType
public readonly TypeCode PropertyTypeCode; // Cached TypeCode for fast primitive reading
public readonly bool IsNullable;
public readonly bool IsIIdCollection;
public readonly Type? ElementType;
public readonly Type? ElementIdType;
public readonly Func<object, object?>? ElementIdGetter;
private readonly Action<object, object?> _setter;
private readonly Func<object, object?> _getter;
@ -691,21 +1452,26 @@ public static class AcJsonDeserializer
public PropertySetterInfo(PropertyInfo prop, Type declaringType)
{
PropertyType = prop.PropertyType;
var underlying = Nullable.GetUnderlyingType(PropertyType);
IsNullable = underlying != null;
UnderlyingType = underlying ?? PropertyType;
PropertyTypeCode = Type.GetTypeCode(UnderlyingType);
_setter = CreateCompiledSetter(declaringType, prop);
_getter = CreateCompiledGetter(declaringType, prop);
ElementType = GetCollectionElementType(PropertyType);
IsCollection = ElementType != null && ElementType != typeof(object) &&
var isCollection = ElementType != null && ElementType != typeof(object) &&
typeof(IEnumerable).IsAssignableFrom(PropertyType) &&
PropertyType != StringType;
!ReferenceEquals(PropertyType, StringType);
if (IsCollection && ElementType != null)
if (isCollection && ElementType != null)
{
var (isId, idType) = GetIdInfo(ElementType);
if (isId)
var idInfo = GetIdInfo(ElementType);
if (idInfo.IsId)
{
ElementIsIId = true;
ElementIdType = idType;
IsIIdCollection = true;
ElementIdType = idInfo.IdType;
var idProp = ElementType.GetProperty("Id");
if (idProp != null)
ElementIdGetter = CreateCompiledGetter(ElementType, idProp);
@ -762,16 +1528,26 @@ public static class AcJsonDeserializer
private Dictionary<string, object>? _idToObject;
private List<PropertyToResolve>? _propertiesToResolve;
public bool IsMergeMode { get; init; }
public bool UseReferenceHandling { get; }
public byte MaxDepth { get; }
public bool IsMergeMode { get; set; }
public bool UseReferenceHandling { get; private set; }
public byte MaxDepth { get; private set; }
public DeserializationContext() : this(AcJsonSerializerOptions.Default) { }
public DeserializationContext(in AcJsonSerializerOptions options)
{
Reset(options);
}
public DeserializationContext(AcJsonSerializerOptions options)
public void Reset(in AcJsonSerializerOptions options)
{
UseReferenceHandling = options.UseReferenceHandling;
MaxDepth = options.MaxDepth;
IsMergeMode = false;
}
public void Clear()
{
_idToObject?.Clear();
_propertiesToResolve?.Clear();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]

View File

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

View File

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

View File

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

View File

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